diff --git a/pkgs/swift2objc/CHANGELOG.md b/pkgs/swift2objc/CHANGELOG.md index 04ad45c7c3..89a1460958 100644 --- a/pkgs/swift2objc/CHANGELOG.md +++ b/pkgs/swift2objc/CHANGELOG.md @@ -2,6 +2,9 @@ - Added support for inout parameters. Preserve inout types in AST and generate correct & pass-by-reference invocation syntax. +- Added support for Swift subscripts. Non-representable subscripts (static, + throwing, async, or with multiple/optional parameters) are automatically + transformed into `@objc` methods. ## 0.1.0 diff --git a/pkgs/swift2objc/lib/src/ast/_core/interfaces/compound_declaration.dart b/pkgs/swift2objc/lib/src/ast/_core/interfaces/compound_declaration.dart index 5754e76a1e..5ff4fc0df8 100644 --- a/pkgs/swift2objc/lib/src/ast/_core/interfaces/compound_declaration.dart +++ b/pkgs/swift2objc/lib/src/ast/_core/interfaces/compound_declaration.dart @@ -5,6 +5,7 @@ import '../../declarations/compounds/members/initializer_declaration.dart'; import '../../declarations/compounds/members/method_declaration.dart'; import '../../declarations/compounds/members/property_declaration.dart'; +import '../../declarations/compounds/members/subscript_declaration.dart'; import 'declaration.dart'; import 'nestable_declaration.dart'; import 'protocol_conformable.dart'; @@ -22,5 +23,6 @@ abstract interface class CompoundDeclaration InnerNestableDeclaration { abstract List properties; abstract List methods; + abstract List subscripts; abstract List initializers; } diff --git a/pkgs/swift2objc/lib/src/ast/declarations/compounds/class_declaration.dart b/pkgs/swift2objc/lib/src/ast/declarations/compounds/class_declaration.dart index 79685ebaa4..0db423e712 100644 --- a/pkgs/swift2objc/lib/src/ast/declarations/compounds/class_declaration.dart +++ b/pkgs/swift2objc/lib/src/ast/declarations/compounds/class_declaration.dart @@ -14,6 +14,7 @@ import '../built_in/built_in_declaration.dart'; import 'members/initializer_declaration.dart'; import 'members/method_declaration.dart'; import 'members/property_declaration.dart'; +import 'members/subscript_declaration.dart'; import 'protocol_declaration.dart'; /// Describes the declaration of a Swift class. @@ -40,6 +41,9 @@ class ClassDeclaration extends AstNode @override List methods; + @override + List subscripts; + @override List> conformedProtocols; @@ -82,6 +86,7 @@ class ClassDeclaration extends AstNode this.lineNumber, this.properties = const [], this.methods = const [], + this.subscripts = const [], this.nestingParent, this.nestedDeclarations = const [], this.conformedProtocols = const [], @@ -107,6 +112,7 @@ class ClassDeclaration extends AstNode super.visitChildren(visitor); visitor.visitAll(properties); visitor.visitAll(methods); + visitor.visitAll(subscripts); visitor.visitAll(conformedProtocols); visitor.visitAll(typeParams); visitor.visit(superClass); diff --git a/pkgs/swift2objc/lib/src/ast/declarations/compounds/enum_declaration.dart b/pkgs/swift2objc/lib/src/ast/declarations/compounds/enum_declaration.dart index 17f7aaae84..357146f46f 100644 --- a/pkgs/swift2objc/lib/src/ast/declarations/compounds/enum_declaration.dart +++ b/pkgs/swift2objc/lib/src/ast/declarations/compounds/enum_declaration.dart @@ -14,6 +14,7 @@ import '../../ast_node.dart'; import 'members/initializer_declaration.dart'; import 'members/method_declaration.dart'; import 'members/property_declaration.dart'; +import 'members/subscript_declaration.dart'; import 'protocol_declaration.dart'; /// Describes the declaration of a Swift enum. @@ -41,6 +42,9 @@ class EnumDeclaration extends AstNode implements CompoundDeclaration { @override List methods; + @override + List subscripts; + @override List initializers; @@ -65,6 +69,7 @@ class EnumDeclaration extends AstNode implements CompoundDeclaration { required this.cases, required this.properties, required this.methods, + this.subscripts = const [], required this.initializers, this.conformedProtocols = const [], this.typeParams = const [], @@ -81,6 +86,7 @@ class EnumDeclaration extends AstNode implements CompoundDeclaration { visitor.visitAll(cases); visitor.visitAll(properties); visitor.visitAll(methods); + visitor.visitAll(subscripts); visitor.visitAll(initializers); visitor.visitAll(typeParams); visitor.visitAll(conformedProtocols); diff --git a/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/subscript_declaration.dart b/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/subscript_declaration.dart new file mode 100644 index 0000000000..02e95f34de --- /dev/null +++ b/pkgs/swift2objc/lib/src/ast/declarations/compounds/members/subscript_declaration.dart @@ -0,0 +1,83 @@ +// 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 '../../../../config.dart'; +import '../../../_core/interfaces/availability.dart'; +import '../../../_core/interfaces/can_async.dart'; +import '../../../_core/interfaces/can_throw.dart'; +import '../../../_core/interfaces/declaration.dart'; +import '../../../_core/interfaces/objc_annotatable.dart'; +import '../../../_core/shared/parameter.dart'; +import '../../../_core/shared/referred_type.dart'; +import '../../../ast_node.dart'; +import 'property_declaration.dart'; + +/// Describes a subscript declaration for a Swift compound entity +class SubscriptDeclaration extends AstNode + implements Declaration, ObjCAnnotatable, CanThrow, CanAsync { + @override + String id; + + @override + String name = 'subscript'; + + @override + InputConfig? source; + + @override + final int? lineNumber; + + @override + List availability; + + @override + bool hasObjCAnnotation; + + ReferredType returnType; + + List params; + + @override + bool throws; + + @override + bool async; + + bool mutating; + + bool hasSetter; + + PropertyStatements? getter; + PropertyStatements? setter; + + bool isStatic; + + SubscriptDeclaration({ + required this.id, + required this.source, + required this.availability, + required this.returnType, + required this.params, + this.lineNumber, + this.hasSetter = false, + this.hasObjCAnnotation = false, + this.getter, + this.setter, + this.isStatic = false, + this.throws = false, + this.async = false, + this.mutating = false, + }); + + @override + void visit(Visitation visitation) => + visitation.visitSubscriptDeclaration(this); + + @override + void visitChildren(Visitor visitor) { + super.visitChildren(visitor); + visitor.visitAll(params); + visitor.visit(returnType); + } +} diff --git a/pkgs/swift2objc/lib/src/ast/declarations/compounds/protocol_declaration.dart b/pkgs/swift2objc/lib/src/ast/declarations/compounds/protocol_declaration.dart index e73c5208e2..ea6f581a4f 100644 --- a/pkgs/swift2objc/lib/src/ast/declarations/compounds/protocol_declaration.dart +++ b/pkgs/swift2objc/lib/src/ast/declarations/compounds/protocol_declaration.dart @@ -11,6 +11,7 @@ import '../../ast_node.dart'; import 'members/initializer_declaration.dart'; import 'members/method_declaration.dart'; import 'members/property_declaration.dart'; +import 'members/subscript_declaration.dart'; /// Describes the declaration of a Swift protocol. class ProtocolDeclaration extends AstNode implements CompoundDeclaration { @@ -35,6 +36,9 @@ class ProtocolDeclaration extends AstNode implements CompoundDeclaration { @override List methods; + @override + List subscripts; + @override List> conformedProtocols; @@ -58,6 +62,7 @@ class ProtocolDeclaration extends AstNode implements CompoundDeclaration { required this.availability, required this.properties, required this.methods, + required this.subscripts, required this.initializers, required this.conformedProtocols, required this.typeParams, @@ -73,7 +78,9 @@ class ProtocolDeclaration extends AstNode implements CompoundDeclaration { void visitChildren(Visitor visitor) { super.visitChildren(visitor); visitor.visitAll(properties); + visitor.visitAll(properties); visitor.visitAll(methods); + visitor.visitAll(subscripts); visitor.visitAll(conformedProtocols); visitor.visitAll(typeParams); visitor.visitAll(initializers); diff --git a/pkgs/swift2objc/lib/src/ast/declarations/compounds/struct_declaration.dart b/pkgs/swift2objc/lib/src/ast/declarations/compounds/struct_declaration.dart index eaa8332e27..438ce2ed51 100644 --- a/pkgs/swift2objc/lib/src/ast/declarations/compounds/struct_declaration.dart +++ b/pkgs/swift2objc/lib/src/ast/declarations/compounds/struct_declaration.dart @@ -11,6 +11,7 @@ import '../../ast_node.dart'; import 'members/initializer_declaration.dart'; import 'members/method_declaration.dart'; import 'members/property_declaration.dart'; +import 'members/subscript_declaration.dart'; import 'protocol_declaration.dart'; /// Describes the declaration of a Swift struct. @@ -36,6 +37,9 @@ class StructDeclaration extends AstNode implements CompoundDeclaration { @override List methods; + @override + List subscripts; + @override List> conformedProtocols; @@ -59,6 +63,7 @@ class StructDeclaration extends AstNode implements CompoundDeclaration { required this.availability, this.properties = const [], this.methods = const [], + this.subscripts = const [], this.initializers = const [], this.conformedProtocols = const [], this.typeParams = const [], @@ -73,7 +78,9 @@ class StructDeclaration extends AstNode implements CompoundDeclaration { void visitChildren(Visitor visitor) { super.visitChildren(visitor); visitor.visitAll(properties); + visitor.visitAll(properties); visitor.visitAll(methods); + visitor.visitAll(subscripts); visitor.visitAll(conformedProtocols); visitor.visitAll(typeParams); visitor.visitAll(initializers); diff --git a/pkgs/swift2objc/lib/src/ast/visitor.dart b/pkgs/swift2objc/lib/src/ast/visitor.dart index a143d72590..71c270667d 100644 --- a/pkgs/swift2objc/lib/src/ast/visitor.dart +++ b/pkgs/swift2objc/lib/src/ast/visitor.dart @@ -15,6 +15,7 @@ import 'declarations/compounds/enum_declaration.dart'; import 'declarations/compounds/members/initializer_declaration.dart'; import 'declarations/compounds/members/method_declaration.dart'; import 'declarations/compounds/members/property_declaration.dart'; +import 'declarations/compounds/members/subscript_declaration.dart'; import 'declarations/compounds/protocol_declaration.dart'; import 'declarations/compounds/struct_declaration.dart'; import 'declarations/globals/globals.dart'; @@ -96,6 +97,8 @@ abstract class Visitation { visitVariableDeclaration(node); void visitGlobalVariableDeclaration(GlobalVariableDeclaration node) => visitVariableDeclaration(node); + void visitSubscriptDeclaration(SubscriptDeclaration node) => + visitDeclaration(node); void visitCompoundDeclaration(CompoundDeclaration node) => visitDeclaration(node); void visitClassDeclaration(ClassDeclaration node) => diff --git a/pkgs/swift2objc/lib/src/generator/generators/class_generator.dart b/pkgs/swift2objc/lib/src/generator/generators/class_generator.dart index 5ce8f850ec..6d559c2a87 100644 --- a/pkgs/swift2objc/lib/src/generator/generators/class_generator.dart +++ b/pkgs/swift2objc/lib/src/generator/generators/class_generator.dart @@ -8,6 +8,7 @@ import '../../ast/declarations/compounds/class_declaration.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 '../../ast/declarations/compounds/members/subscript_declaration.dart'; import '../_core/utils.dart'; import '../generator.dart'; @@ -19,6 +20,7 @@ List generateClass(ClassDeclaration declaration) { ...[ _generateClassWrappedInstance(declaration), ..._generateClassProperties(declaration), + ..._generateClassSubscripts(declaration), ..._generateInitializers(declaration), ..._generateClassMethods(declaration), ..._generateNestedDeclarations(declaration), @@ -144,6 +146,48 @@ List _generateClassProperties(ClassDeclaration declaration) => [ ..._generateClassProperty(property), ]; +List _generateClassSubscripts(ClassDeclaration declaration) => [ + for (final subscript in declaration.subscripts) + ..._generateClassSubscript(subscript), +]; + +List _generateClassSubscript(SubscriptDeclaration subscript) { + final header = StringBuffer(); + + if (subscript.hasObjCAnnotation) { + header.write('@objc '); + } + + if (subscript.isStatic) { + header.write('static '); + } + + final params = generateParameters(subscript.params); + header.write( + 'public subscript($params) -> ${subscript.returnType.swiftType} {', + ); + + final getterLines = [ + 'get ${generateAnnotations(subscript)}{', + ...(subscript.getter?.statements.indent() ?? []), + '}', + ]; + + final setterLines = [ + 'set {', + ...(subscript.setter?.statements.indent() ?? []), + '}', + ]; + + return [ + ...generateAvailability(subscript), + header.toString(), + ...getterLines.indent(), + if (subscript.hasSetter) ...setterLines.indent(), + '}\n', + ]; +} + List _generateClassProperty(PropertyDeclaration property) { final header = StringBuffer(); diff --git a/pkgs/swift2objc/lib/src/parser/_core/token_list.dart b/pkgs/swift2objc/lib/src/parser/_core/token_list.dart index 3d1be38714..9c8d330ab1 100644 --- a/pkgs/swift2objc/lib/src/parser/_core/token_list.dart +++ b/pkgs/swift2objc/lib/src/parser/_core/token_list.dart @@ -32,7 +32,7 @@ class TokenList extends Iterable { @visibleForTesting static Iterable splitToken(Json token) sync* { - const splittables = ['(', ')', '?', ',', '->']; + const splittables = ['(', ')', '?', ',', '->', '[', ']', ':']; Json textToken(String text) => Json({'kind': 'text', 'spelling': text}, token.pathSegments); diff --git a/pkgs/swift2objc/lib/src/parser/_core/utils.dart b/pkgs/swift2objc/lib/src/parser/_core/utils.dart index 29bbbc3f1c..84c3dfeb5a 100644 --- a/pkgs/swift2objc/lib/src/parser/_core/utils.dart +++ b/pkgs/swift2objc/lib/src/parser/_core/utils.dart @@ -82,8 +82,12 @@ bool parseSymbolHasObjcAnnotation(Json symbolJson) => (json) => matchFragment(json, 'attribute', '@objc'), ); -bool parseIsOverriding(Json symbolJson) => symbolJson['declarationFragments'] - .any((json) => matchFragment(json, 'keyword', 'override')); +bool parseIsOverriding(Json symbolJson) => + findKeywordInFragments(symbolJson['declarationFragments'], 'override'); + +bool findKeywordInFragments(Json json, String keyword) { + return json.any((frag) => matchFragment(frag, 'keyword', keyword)); +} List parseAvailability(Json symbolJson) { final availability = symbolJson['availability']; @@ -165,3 +169,14 @@ ReferredType parseTypeAfterSeparator( assert(suffix.isEmpty, '$suffix'); return type; } + +ReferredType parseReturnType( + Context context, + Json symbolJson, + ParsedSymbolgraph symbolgraph, +) { + final returnJson = TokenList(symbolJson['functionSignature']['returns']); + final (returnType, unparsed) = parseType(context, symbolgraph, returnJson); + assert(unparsed.isEmpty, '$returnJson\n\n$returnType\n\n$unparsed\n'); + return returnType; +} diff --git a/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_compound_declaration.dart b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_compound_declaration.dart index aa5b17a2d8..6aebe63876 100644 --- a/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_compound_declaration.dart +++ b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_compound_declaration.dart @@ -10,6 +10,7 @@ import '../../../ast/declarations/compounds/class_declaration.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 '../../../ast/declarations/compounds/members/subscript_declaration.dart'; import '../../../ast/declarations/compounds/struct_declaration.dart'; import '../../../config.dart'; import '../../../context.dart'; @@ -78,6 +79,9 @@ ParsedCompound parseCompoundDeclaration( (m) => m.fullName, ), ); + compound.subscripts.addAll( + memberDeclarations.removeWhereType(), + ); compound.nestedDeclarations.addAll( memberDeclarations.removeWhereType(), ); @@ -109,6 +113,7 @@ ClassDeclaration parseClassDeclaration( properties: [], methods: [], initializers: [], + subscripts: [], nestedDeclarations: [], ), ).compound; @@ -136,6 +141,7 @@ StructDeclaration parseStructDeclaration( properties: [], methods: [], initializers: [], + subscripts: [], nestedDeclarations: [], ), ).compound; diff --git a/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_enum_declaration.dart b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_enum_declaration.dart index 397da2962a..d69654c33a 100644 --- a/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_enum_declaration.dart +++ b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_enum_declaration.dart @@ -34,6 +34,7 @@ EnumDeclaration parseEnumDeclaration( properties: [], methods: [], initializers: [], + subscripts: [], nestedDeclarations: [], ), ); 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 495d66e632..17ec858c33 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 @@ -3,7 +3,6 @@ // BSD-style license that can be found in the LICENSE file. import '../../../ast/_core/shared/parameter.dart'; -import '../../../ast/_core/shared/referred_type.dart'; import '../../../ast/declarations/compounds/members/method_declaration.dart'; import '../../../ast/declarations/globals/globals.dart'; import '../../../context.dart'; @@ -28,7 +27,7 @@ GlobalFunctionDeclaration parseGlobalFunctionDeclaration( name: parseSymbolName(symbol.json), source: symbol.source, availability: parseAvailability(symbol.json), - returnType: _parseFunctionReturnType(context, symbol.json, symbolgraph), + returnType: parseReturnType(context, symbol.json, symbolgraph), params: info.params, throws: info.throws, async: info.async, @@ -54,7 +53,7 @@ MethodDeclaration parseMethodDeclaration( source: symbol.source, lineNumber: parseLineNumber(symbol.json), availability: parseAvailability(symbol.json), - returnType: _parseFunctionReturnType(context, symbol.json, symbolgraph), + returnType: parseReturnType(context, symbol.json, symbolgraph), params: info.params, hasObjCAnnotation: parseSymbolHasObjcAnnotation(symbol.json), isStatic: isStatic, @@ -78,6 +77,7 @@ ParsedFunctionInfo parseFunctionInfo( ParsedSymbolgraph symbolgraph, { bool isEnumCase = false, bool isOperator = false, + bool isSubscript = false, }) { // `declarationFragments` describes each part of the function declaration, // things like the `func` keyword, brackets, spaces, etc. @@ -115,7 +115,10 @@ ParsedFunctionInfo parseFunctionInfo( while (true) { final keyword = maybeConsume('keyword'); if (keyword != null) { - if (keyword == 'func' || keyword == 'init' || keyword == 'case') { + if (keyword == 'func' || + keyword == 'init' || + keyword == 'case' || + keyword == 'subscript') { if (keyword == 'func' && isOperator) { final ws1 = maybeConsume('text'); final op = maybeConsume('identifier'); @@ -160,7 +163,7 @@ ParsedFunctionInfo parseFunctionInfo( if (sep != ':') { throw malformedInitializerException; } - } else if (isOperator) { + } else if (isOperator || isSubscript) { internalParam = maybeConsume('internalParam'); if (internalParam == null) { throw malformedInitializerException; @@ -178,14 +181,14 @@ ParsedFunctionInfo parseFunctionInfo( parameters.add( Parameter( - name: isOperator ? (internalParam ?? '') : (externalParam ?? ''), + name: externalParam ?? (isSubscript ? '_' : (internalParam ?? '')), internalName: isOperator ? null : internalParam, type: type, ), ); final end = maybeConsume('text'); - if (end == ')') break; + if (end != null && end.startsWith(')')) break; if (end != ',') { throw malformedInitializerException; } @@ -199,12 +202,33 @@ ParsedFunctionInfo parseFunctionInfo( while (true) { final keyword = maybeConsume('keyword'); if (keyword == null) { - if (maybeConsume('text') != '') break; + final text = maybeConsume('text'); + if (text == null) break; + if (text == '') continue; + + // For subscripts, 'throws' and 'async' can be inside the { get ... } + // block. We'll just collect all keywords in the fragments if it's a + // subscript. + if (isSubscript) continue; + break; } else { annotations.add(keyword); } } + if (isSubscript) { + // If it's a subscript, we just look for 'throws' and 'async' anywhere in + // the fragments after the parameters. + while (tokens.isNotEmpty) { + final keyword = maybeConsume('keyword'); + if (keyword != null) { + annotations.add(keyword); + } else { + tokens = tokens.slice(1); + } + } + } + return ( params: parameters, throws: annotations.contains('throws'), @@ -212,14 +236,3 @@ ParsedFunctionInfo parseFunctionInfo( mutating: prefixAnnotations.contains('mutating'), ); } - -ReferredType _parseFunctionReturnType( - Context context, - Json symbolJson, - ParsedSymbolgraph symbolgraph, -) { - final returnJson = TokenList(symbolJson['functionSignature']['returns']); - final (returnType, unparsed) = parseType(context, symbolgraph, returnJson); - assert(unparsed.isEmpty, '$returnJson\n\n$returnType\n\n$unparsed\n'); - return returnType; -} diff --git a/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_subscript_declaration.dart b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_subscript_declaration.dart new file mode 100644 index 0000000000..d2954d2e92 --- /dev/null +++ b/pkgs/swift2objc/lib/src/parser/parsers/declaration_parsers/parse_subscript_declaration.dart @@ -0,0 +1,42 @@ +// 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/declarations/compounds/members/subscript_declaration.dart'; +import '../../../context.dart'; +import '../../_core/parsed_symbolgraph.dart'; +import '../../_core/utils.dart'; +import 'parse_function_declaration.dart'; + +SubscriptDeclaration parseSubscriptDeclaration( + Context context, + ParsedSymbol symbol, + ParsedSymbolgraph symbolgraph, { + bool isStatic = false, +}) { + final declarationFragments = symbol.json['declarationFragments']; + final functionInfo = parseFunctionInfo( + context, + declarationFragments, + symbolgraph, + isSubscript: true, + ); + final hasSetter = findKeywordInFragments(declarationFragments, 'set'); + + return SubscriptDeclaration( + id: parseSymbolId(symbol.json), + source: symbol.source, + lineNumber: parseLineNumber(symbol.json), + availability: parseAvailability(symbol.json), + returnType: parseReturnType(context, symbol.json, symbolgraph), + params: functionInfo.params, + hasObjCAnnotation: parseSymbolHasObjcAnnotation(symbol.json), + isStatic: isStatic, + throws: functionInfo.throws, + async: functionInfo.async, + mutating: functionInfo.mutating, + hasSetter: hasSetter, + getter: null, + setter: null, + ); +} 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 124a0fdb49..f1360bd9d0 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 @@ -63,11 +63,21 @@ ReferredType _parseVariableType( Context context, Json symbolJson, ParsedSymbolgraph symbolgraph, -) => parseTypeAfterSeparator( - context, - TokenList(symbolJson['names']['subHeading']), - symbolgraph, -); +) { + try { + return parseTypeAfterSeparator( + context, + TokenList(symbolJson['names']['subHeading']), + symbolgraph, + ); + } catch (e) { + final id = symbolJson['identifier']['precise']; + throw Exception( + 'Failed to parse variable type for $id: $e\n' + 'SubHeading: ${symbolJson['names']['subHeading']}', + ); + } +} bool _parseVariableIsConstant(Json fragmentsJson) { final declarationKeyword = fragmentsJson.firstWhere( diff --git a/pkgs/swift2objc/lib/src/parser/parsers/parse_declarations.dart b/pkgs/swift2objc/lib/src/parser/parsers/parse_declarations.dart index afc702dd8e..144255c6e3 100644 --- a/pkgs/swift2objc/lib/src/parser/parsers/parse_declarations.dart +++ b/pkgs/swift2objc/lib/src/parser/parsers/parse_declarations.dart @@ -12,6 +12,7 @@ import 'declaration_parsers/parse_compound_declaration.dart'; import 'declaration_parsers/parse_enum_declaration.dart'; import 'declaration_parsers/parse_function_declaration.dart'; import 'declaration_parsers/parse_initializer_declaration.dart'; +import 'declaration_parsers/parse_subscript_declaration.dart'; import 'declaration_parsers/parse_typealias_declaration.dart'; import 'declaration_parsers/parse_variable_declaration.dart'; @@ -70,6 +71,18 @@ Declaration parseDeclaration( parsedSymbol, symbolgraph, ), + 'swift.subscript' => parseSubscriptDeclaration( + context, + parsedSymbol, + symbolgraph, + isStatic: false, + ), + 'swift.type.subscript' => parseSubscriptDeclaration( + context, + parsedSymbol, + symbolgraph, + isStatic: true, + ), 'swift.method' => parseMethodDeclaration( context, parsedSymbol, diff --git a/pkgs/swift2objc/lib/src/transformer/_core/primitive_wrappers.dart b/pkgs/swift2objc/lib/src/transformer/_core/primitive_wrappers.dart index 921bff3801..cc5245f37c 100644 --- a/pkgs/swift2objc/lib/src/transformer/_core/primitive_wrappers.dart +++ b/pkgs/swift2objc/lib/src/transformer/_core/primitive_wrappers.dart @@ -50,17 +50,32 @@ ReferredType _createWrapperClass(DeclaredType primitiveType) { bool shouldWrapPrimitives, TransformationState state, ) { - if (type is! DeclaredType || !shouldWrapPrimitives) { + if (!shouldWrapPrimitives) { return (type, false); } - final wrapper = _getPrimitiveWrapper(type); - if (wrapper == null) { - return (type, false); + if (type is DeclaredType) { + final wrapper = _getPrimitiveWrapper(type); + if (wrapper == null) { + return (type, false); + } + + state.map[type.declaration] = (wrapper as DeclaredType).declaration; + return (wrapper, true); + } + + if (type is OptionalType) { + final (wrappedChild, hasWrappedChild) = maybeGetPrimitiveWrapper( + type.child, + shouldWrapPrimitives, + state, + ); + if (hasWrappedChild) { + return (OptionalType(wrappedChild), true); + } } - state.map[type.declaration] = (wrapper as DeclaredType).declaration; - return (wrapper, true); + return (type, false); } ReferredType? _getPrimitiveWrapper(DeclaredType other) { diff --git a/pkgs/swift2objc/lib/src/transformer/_core/utils.dart b/pkgs/swift2objc/lib/src/transformer/_core/utils.dart index 8a0c852bff..7cce2dfaf8 100644 --- a/pkgs/swift2objc/lib/src/transformer/_core/utils.dart +++ b/pkgs/swift2objc/lib/src/transformer/_core/utils.dart @@ -195,3 +195,38 @@ extension SortById on Iterable { return a.id.compareTo(b.id); }); } + +String generateInvocationParams( + UniqueNamer localNamer, + List originalParams, + List transformedParams, +) { + assert(originalParams.length == transformedParams.length); + + final argumentsList = []; + for (var i = 0; i < originalParams.length; i++) { + final originalParam = originalParams[i]; + final transformedParam = transformedParams[i]; + + final transformedParamName = localNamer.makeUnique( + transformedParam.internalName ?? transformedParam.name, + ); + + final (unwrappedParamValue, unwrappedType) = maybeUnwrapValue( + transformedParam.type, + transformedParamName, + ); + + assert(unwrappedType.sameAs(originalParam.type)); + final invocationValue = originalParam.type is InoutType + ? '&$unwrappedParamValue' + : unwrappedParamValue; + + argumentsList.add( + originalParam.name.isEmpty || originalParam.name == '_' + ? invocationValue + : '${originalParam.name}: $invocationValue', + ); + } + return argumentsList.join(', '); +} diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart index 1ce9bff8fa..1b57f4f19e 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_compound.dart @@ -11,6 +11,7 @@ import '../../ast/declarations/compounds/class_declaration.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 '../../ast/declarations/compounds/members/subscript_declaration.dart'; import '../../ast/declarations/compounds/struct_declaration.dart'; import '../../parser/_core/utils.dart'; import '../_core/unique_namer.dart'; @@ -18,6 +19,7 @@ import '../_core/utils.dart'; import '../transform.dart'; import 'transform_function.dart'; import 'transform_initializer.dart'; +import 'transform_subscript.dart'; import 'transform_variable.dart'; ClassDeclaration transformCompound( @@ -104,6 +106,17 @@ ClassDeclaration transformCompound( .nonNulls .toList(); + final transformedSubscripts = originalCompound.subscripts + .expand( + (subscript) => transformSubscript( + subscript, + wrappedCompoundInstance, + parentNamer, + state, + ), + ) + .toList(); + transformedCompound.properties = transformedProperties .removeWhereType() .sortedById(); @@ -116,10 +129,16 @@ ClassDeclaration transformCompound( ...transformedMethods, ...transformedProperties.removeWhereType(), ...transformedInitializers.removeWhereType(), + ...transformedSubscripts.removeWhereType(), ].sortedById(); + transformedCompound.subscripts = transformedSubscripts + .removeWhereType() + .sortedById(); + assert(transformedProperties.isEmpty); assert(transformedInitializers.isEmpty); + assert(transformedSubscripts.isEmpty); } return transformedCompound; diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart index 6cd7edbf26..a0d4e89b0f 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_function.dart @@ -143,41 +143,6 @@ MethodDeclaration _transformFunction( return transformedMethod; } -String generateInvocationParams( - UniqueNamer localNamer, - List originalParams, - List transformedParams, -) { - assert(originalParams.length == transformedParams.length); - - final argumentsList = []; - for (var i = 0; i < originalParams.length; i++) { - final originalParam = originalParams[i]; - final transformedParam = transformedParams[i]; - - final transformedParamName = localNamer.makeUnique( - transformedParam.internalName ?? transformedParam.name, - ); - - final (unwrappedParamValue, unwrappedType) = maybeUnwrapValue( - transformedParam.type, - transformedParamName, - ); - - assert(unwrappedType.sameAs(originalParam.type)); - final invocationValue = originalParam.type is InoutType - ? '&$unwrappedParamValue' - : unwrappedParamValue; - - argumentsList.add( - originalParam.name.isEmpty || originalParam.name == '_' - ? invocationValue - : '${originalParam.name}: $invocationValue', - ); - } - return argumentsList.join(', '); -} - List _generateStatements( FunctionDeclaration originalFunction, MethodDeclaration transformedMethod, diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart index 11e3aa781f..3022a02427 100644 --- a/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_initializer.dart @@ -9,8 +9,8 @@ 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 '../_core/unique_namer.dart'; +import '../_core/utils.dart'; import '../transform.dart'; -import 'transform_function.dart'; import 'transform_referred_type.dart'; Declaration transformInitializer( diff --git a/pkgs/swift2objc/lib/src/transformer/transformers/transform_subscript.dart b/pkgs/swift2objc/lib/src/transformer/transformers/transform_subscript.dart new file mode 100644 index 0000000000..89182fe1ec --- /dev/null +++ b/pkgs/swift2objc/lib/src/transformer/transformers/transform_subscript.dart @@ -0,0 +1,204 @@ +import '../../ast/_core/interfaces/declaration.dart'; +import '../../ast/_core/shared/parameter.dart'; +import '../../ast/_core/shared/referred_type.dart'; +import '../../ast/declarations/built_in/built_in_declaration.dart'; +import '../../ast/declarations/compounds/members/method_declaration.dart'; +import '../../ast/declarations/compounds/members/property_declaration.dart'; +import '../../ast/declarations/compounds/members/subscript_declaration.dart'; +import '../../parser/_core/utils.dart'; +import '../_core/unique_namer.dart'; +import '../_core/utils.dart'; +import '../transform.dart'; +import 'transform_referred_type.dart'; + +List transformSubscript( + SubscriptDeclaration original, + PropertyDeclaration wrappedClassInstance, + UniqueNamer globalNamer, + TransformationState state, +) { + final transformedParams = [ + for (var i = 0; i < original.params.length; ++i) + _transformParam(i, original.params[i], globalNamer, state), + ]; + + final localNamer = UniqueNamer(); + final resultName = localNamer.makeUnique('result'); + + final (wrapperResult, type) = maybeWrapValue( + original.returnType, + resultName, + globalNamer, + state, + ); + + final isRepresentable = + !original.isStatic && + !original.throws && + !original.async && + original.params.length == 1 && + (original.params[0].type.sameAs(intType) || + (original.params[0].type.isObjCRepresentable && + original.params[0].type is! OptionalType)); + + if (isRepresentable) { + final transformedSubscript = SubscriptDeclaration( + id: original.id, + source: original.source, + availability: original.availability, + returnType: type, + params: transformedParams, + hasObjCAnnotation: true, + isStatic: original.isStatic, + throws: original.throws, + async: original.async, + mutating: original.mutating, + hasSetter: original.hasSetter, + ); + + transformedSubscript.getter = PropertyStatements( + _generateGetterStatements( + original, + transformedSubscript, + wrappedClassInstance, + globalNamer, + localNamer, + resultName, + wrapperResult, + ), + ); + + if (original.hasSetter) { + transformedSubscript.setter = PropertyStatements( + _generateSetterStatements( + original, + transformedSubscript, + wrappedClassInstance, + globalNamer, + ), + ); + } + + return [transformedSubscript]; + } + + // Not representable as a subscript, so transform into a method. + final prefix = [ + if (original.throws) 'try', + if (original.async) 'await', + ].join(' '); + + final arguments = generateInvocationParams( + localNamer, + original.params, + transformedParams, + ); + + final methodSource = original.isStatic + ? wrappedClassInstance.type.swiftType + : wrappedClassInstance.name; + + final getter = MethodDeclaration( + id: original.id.addIdSuffix('getter'), + name: 'getValue', + source: original.source, + availability: original.availability, + returnType: type, + params: transformedParams, + hasObjCAnnotation: true, + isStatic: original.isStatic, + throws: original.throws, + async: original.async, + statements: [ + 'let $resultName = $prefix $methodSource[$arguments]', + 'return $wrapperResult', + ], + ); + + if (!original.hasSetter) { + return [getter]; + } + + final (unwrappedNewValue, _) = maybeUnwrapValue(type, 'newValue'); + + final setter = MethodDeclaration( + id: original.id.addIdSuffix('setter'), + name: 'setValue', + source: original.source, + availability: original.availability, + returnType: voidType, + params: [ + ...transformedParams, + Parameter(name: 'newValue', internalName: null, type: type), + ], + hasObjCAnnotation: true, + isStatic: original.isStatic, + throws: original.throws, + async: original.async, + statements: ['$methodSource[$arguments] = $unwrappedNewValue'], + ); + + return [getter, setter]; +} + +Parameter _transformParam( + int index, + Parameter p, + UniqueNamer globalNamer, + TransformationState state, +) => Parameter( + name: p.name.isEmpty ? '_' : p.name, + internalName: p.name.isEmpty && p.internalName == null + ? 'arg$index' + : p.internalName, + type: transformReferredType(p.type, globalNamer, state), +); + +List _generateGetterStatements( + SubscriptDeclaration original, + SubscriptDeclaration transformed, + PropertyDeclaration wrappedClassInstance, + UniqueNamer globalNamer, + UniqueNamer localNamer, + String resultName, + String wrappedResult, +) { + final arguments = generateInvocationParams( + localNamer, + original.params, + transformed.params, + ); + + final methodSource = original.isStatic + ? wrappedClassInstance.type.swiftType + : wrappedClassInstance.name; + + final call = '$methodSource[$arguments]'; + + return ['let $resultName = $call', 'return $wrappedResult']; +} + +List _generateSetterStatements( + SubscriptDeclaration original, + SubscriptDeclaration transformed, + PropertyDeclaration wrappedClassInstance, + UniqueNamer globalNamer, +) { + final localNamer = UniqueNamer(); + final arguments = generateInvocationParams( + localNamer, + original.params, + transformed.params, + ); + + final (unwrappedNewValue, unwrappedType) = maybeUnwrapValue( + transformed.returnType, + 'newValue', + ); + + final methodSource = original.isStatic + ? wrappedClassInstance.type.swiftType + : wrappedClassInstance.name; + + return ['$methodSource[$arguments] = $unwrappedNewValue']; +} diff --git a/pkgs/swift2objc/test/integration/subscripts_input.swift b/pkgs/swift2objc/test/integration/subscripts_input.swift new file mode 100644 index 0000000000..fa943565fd --- /dev/null +++ b/pkgs/swift2objc/test/integration/subscripts_input.swift @@ -0,0 +1,82 @@ +import Foundation +public class SubscriptClass { + public var stored: String = "" + + public subscript(index: Int) -> String { + get { + return stored + } + set { + stored = newValue + } + } +} + +public class MultipleArgsSubscript { + public subscript(row: Int, col: Int) -> Int { + get { + return row + col + } + } +} + +public class ZeroArgsSubscript { + public subscript() -> Int { + get { + return 42 + } + } +} + +public class StaticSubscript { + public static subscript(name: String) -> String { + get { + return "Hello, " + name + } + } +} + +public class NonTrivialTypesSubscript { + public subscript(other: SubscriptClass) -> SubscriptClass { + get { + return other + } + } +} + +@objc public class OptionalSubscript: NSObject { + @objc public subscript(opt: String?) -> String? { + get { + return opt == nil ? nil : "Value: \(opt!)" + } + } +} + +public struct SubscriptStruct { + public var value: Int = 0 + + public subscript(index: Int) -> Int { + get { + return value + index + } + set { + value = index + newValue + } + } +} + +public class ThrowingSubscript { + public subscript(index: Int) -> String { + get throws { + return "OK" + } + } +} + +public class AsyncSubscript { + public subscript(index: Int) -> String { + get async { + return "Async OK" + } + } +} diff --git a/pkgs/swift2objc/test/integration/subscripts_output.swift b/pkgs/swift2objc/test/integration/subscripts_output.swift new file mode 100644 index 0000000000..fb44dd88cd --- /dev/null +++ b/pkgs/swift2objc/test/integration/subscripts_output.swift @@ -0,0 +1,162 @@ +// Test preamble text + +import Foundation + +@objc public class OptionalSubscriptWrapper: NSObject { + var wrappedInstance: OptionalSubscript + + init(_ wrappedInstance: OptionalSubscript) { + self.wrappedInstance = wrappedInstance + } + + @objc public func getValue(_ opt: String?) -> String? { + let result = wrappedInstance[opt] + return result + } + +} + +@objc public class AsyncSubscriptWrapper: NSObject { + var wrappedInstance: AsyncSubscript + + init(_ wrappedInstance: AsyncSubscript) { + self.wrappedInstance = wrappedInstance + } + + @objc public func getValue(_ index: Int) async -> String { + let result = await wrappedInstance[index] + return result + } + +} + +@objc public class SubscriptClassWrapper: NSObject { + var wrappedInstance: SubscriptClass + + @objc public var stored: String { + get { + wrappedInstance.stored + } + set { + wrappedInstance.stored = newValue + } + } + + @objc public subscript(_ index: Int) -> String { + get { + let result = wrappedInstance[index] + return result + } + set { + wrappedInstance[index] = newValue + } + } + + init(_ wrappedInstance: SubscriptClass) { + self.wrappedInstance = wrappedInstance + } + +} + +@objc public class StaticSubscriptWrapper: NSObject { + var wrappedInstance: StaticSubscript + + init(_ wrappedInstance: StaticSubscript) { + self.wrappedInstance = wrappedInstance + } + + @objc static public func getValue(_ name: String) -> String { + let result = StaticSubscript[name] + return result + } + +} + +@objc public class SubscriptStructWrapper: NSObject { + var wrappedInstance: SubscriptStruct + + @objc public var value: Int { + get { + wrappedInstance.value + } + set { + wrappedInstance.value = newValue + } + } + + @objc public subscript(_ index: Int) -> Int { + get { + let result = wrappedInstance[index] + return result + } + set { + wrappedInstance[index] = newValue + } + } + + init(_ wrappedInstance: SubscriptStruct) { + self.wrappedInstance = wrappedInstance + } + + @objc public init(value: Int) { + wrappedInstance = SubscriptStruct(value: value) + } + +} + +@objc public class ThrowingSubscriptWrapper: NSObject { + var wrappedInstance: ThrowingSubscript + + init(_ wrappedInstance: ThrowingSubscript) { + self.wrappedInstance = wrappedInstance + } + + @objc public func getValue(_ index: Int) throws -> String { + let result = try wrappedInstance[index] + return result + } + +} + +@objc public class ZeroArgsSubscriptWrapper: NSObject { + var wrappedInstance: ZeroArgsSubscript + + init(_ wrappedInstance: ZeroArgsSubscript) { + self.wrappedInstance = wrappedInstance + } + + @objc public func getValue() -> Int { + let result = wrappedInstance[] + return result + } + +} + +@objc public class MultipleArgsSubscriptWrapper: NSObject { + var wrappedInstance: MultipleArgsSubscript + + init(_ wrappedInstance: MultipleArgsSubscript) { + self.wrappedInstance = wrappedInstance + } + + @objc public func getValue(_ row: Int, _ col: Int) -> Int { + let result = wrappedInstance[row, col] + return result + } + +} + +@objc public class NonTrivialTypesSubscriptWrapper: NSObject { + var wrappedInstance: NonTrivialTypesSubscript + + init(_ wrappedInstance: NonTrivialTypesSubscript) { + self.wrappedInstance = wrappedInstance + } + + @objc public func getValue(_ other: SubscriptClassWrapper) -> SubscriptClassWrapper { + let result = wrappedInstance[other.wrappedInstance] + return SubscriptClassWrapper(result) + } + +} +