From 49c6fd20925532341c5ec34274912cbb9e3e6b52 Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Fri, 17 May 2024 12:44:46 +0100 Subject: [PATCH 1/4] Support property name shadowing global types such as Type, String, Int, Map Also preserve type aliases in generated files Fixes #15 --- .gitignore | 1 + .../ex63_property_shadow_global_type.dart | 56 +++++++++++++++ morphy/lib/src/MorphyGenerator.dart | 21 ++++-- .../src/common/GeneratorForAnnotationX.dart | 20 +++++- morphy/lib/src/common/helpers.dart | 69 +++++++++++++++++-- morphy/lib/src/helpers.dart | 26 +++---- morphy/test/helpers_test.dart | 40 +++++------ 7 files changed, 185 insertions(+), 48 deletions(-) create mode 100644 example/test/ex63_property_shadow_global_type.dart diff --git a/.gitignore b/.gitignore index 46b7540..d4aca37 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ *.morphy.dart *.morphy2.dart +*.g.dart # Flutter/Dart/Pub related **/doc/api/ diff --git a/example/test/ex63_property_shadow_global_type.dart b/example/test/ex63_property_shadow_global_type.dart new file mode 100644 index 0000000..cac8d69 --- /dev/null +++ b/example/test/ex63_property_shadow_global_type.dart @@ -0,0 +1,56 @@ +import 'package:morphy_annotation/morphy_annotation.dart'; +import 'package:test/test.dart'; + +part 'ex63_property_shadow_global_type.morphy.dart'; +part 'ex63_property_shadow_global_type.g.dart'; + +//nullable private getters and private morphy class + +main() { + test("1", () { + final test = Test( + String: 1, + Object: 2, + List: 3, + Map: 4, + Never: (5, 6), + Type: (7, named: 8), + bool: [2, 'abc'], + hashObjects: [4, 5], + identical: [6, 7], + ); + expect(test.String, 1); + expect(test.bool, [2, 'abc']); + expect(Test.fromJson(test.toJson()).toJson(), test.toJson()); + + final test2 = Test2( + PositionalFunction: (a, [b = 3, c = 5]) => a + b + c + ); + final f = test2.PositionalFunction; + expect(f(5, 5), 15); + }); +} + +typedef Int = int; +typedef _List = List; +typedef IntList = List; + +@Morphy(generateJson: true) +abstract class $Test { + Int get String; + Int get Object; + Int get List; + Int get Map; + (Int, Int)? get Never; + (Int, {Int named}) get Type; + _List<(Int, Int)>? get int; + _List get bool; + IntList get hashObjects; + _List get identical; +} + +@Morphy(generateJson: false) +abstract class $Test2 { + Int Function(Int p1, {Int n1, required Int n2})? get Function; + Int Function(Int p1, [Int p2, Int p3]) get PositionalFunction; +} \ No newline at end of file diff --git a/morphy/lib/src/MorphyGenerator.dart b/morphy/lib/src/MorphyGenerator.dart index c2fbac8..55d2fff 100644 --- a/morphy/lib/src/MorphyGenerator.dart +++ b/morphy/lib/src/MorphyGenerator.dart @@ -60,9 +60,9 @@ class MorphyGenerator extends GeneratorForAnnotationX // InterfaceWithComment( e.element.name, - e.typeArguments.map((e) => e.toString()).toList(), + e.typeArguments.map(typeToString).toList(), e.element.typeParameters.map((x) => x.name).toList(), - e.element.fields.map((e) => NameType(e.name, e.type.toString())).toList(), + e.element.fields.map((e) => NameType(e.name, typeToString(e.type))).toList(), comment: e.element.documentationComment, )) // .toList(); @@ -72,7 +72,10 @@ class MorphyGenerator extends GeneratorForAnnotationX NameTypeClassComment(e.name, e.bound == null ? null : e.bound.toString(), null)) // + .map((e) { + final bound = e.bound; + return NameTypeClassComment(e.name, bound == null ? null : typeToString(bound), null); + }) // .toList(); var allFieldsDistinct = getDistinctFields(allFields, interfaces); @@ -93,8 +96,10 @@ class MorphyGenerator extends GeneratorForAnnotationX // - NameType(x.name, x.bound == null ? null : x.bound.toString())) + .map((TypeParameterElement x) { + final bound = x.bound; + return NameType(x.name, bound == null ? null : typeToString(bound)); + }) .toList(), getAllFields(el.allSupertypes, el).where((x) => x.name != "hashCode").toList(), true, @@ -110,8 +115,10 @@ class MorphyGenerator extends GeneratorForAnnotationX // - NameType(x.name, x.bound == null ? null : x.bound.toString())) + .map((TypeParameterElement x) { + final bound = x.bound; + return NameType(x.name, bound == null ? null : typeToString(bound)); + }) .toList(), getAllFields(e.element.allSupertypes, e.element as ClassElement).where((x) => x.name != "hashCode").toList(), ); diff --git a/morphy/lib/src/common/GeneratorForAnnotationX.dart b/morphy/lib/src/common/GeneratorForAnnotationX.dart index e09c6d4..c42df2f 100644 --- a/morphy/lib/src/common/GeneratorForAnnotationX.dart +++ b/morphy/lib/src/common/GeneratorForAnnotationX.dart @@ -3,10 +3,13 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:developer'; import 'package:analyzer/dart/element/element.dart'; // ignore: import_of_legacy_library_into_null_safe import 'package:build/build.dart'; +import 'package:morphy/src/MorphyGenerator.dart'; +import 'package:morphy_annotation/morphy_annotation.dart'; // ignore: import_of_legacy_library_into_null_safe import 'package:source_gen/source_gen.dart'; // ignore: import_of_legacy_library_into_null_safe @@ -40,7 +43,22 @@ abstract class GeneratorForAnnotationX extends Generator { @override FutureOr generate(LibraryReader library, BuildStep buildStep) async { - final values = Set(); + final values = this is MorphyGenerator + ? { + """ +typedef __String = String; +typedef __Object = Object; +typedef __List = List; +typedef __Map = Map; +typedef __Never = Never; +typedef __Type = Type; +typedef __int = int; +typedef __bool = bool; +const __hashObjects = hashObjects; +const __identical = identical; +""" + } + : {}; var classElements = library.allElements // .whereType() diff --git a/morphy/lib/src/common/helpers.dart b/morphy/lib/src/common/helpers.dart index 756b4b1..69c4e4e 100644 --- a/morphy/lib/src/common/helpers.dart +++ b/morphy/lib/src/common/helpers.dart @@ -1,5 +1,5 @@ -// ignore: import_of_legacy_library_into_null_safe import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:dartx/dartx.dart'; @@ -28,7 +28,7 @@ MethodDetails getMethodDetailsForFunctionType( FunctionTypedElement fn, TMeta1 GetMetaData(ParameterElement parameterElement), ) { - var returnType = fn.returnType.toString(); + var returnType = typeToString(fn.returnType); var paramsPositional2 = fn.parameters.where((x) => x.isPositional); var paramsNamed2 = fn.parameters.where((x) => x.isNamed); @@ -36,7 +36,7 @@ MethodDetails getMethodDetailsForFunctionType( var paramsPositional = paramsPositional2 .map((x) => NameTypeClassCommentData( x.name.toString(), - x.type.toString(), + typeToString(x.type), null, comment: x.documentationComment, meta1: GetMetaData(x), @@ -45,7 +45,7 @@ MethodDetails getMethodDetailsForFunctionType( var paramsNamed = paramsNamed2 .map((x) => NameTypeClassCommentData( x.name.toString(), - x.type.toString(), + typeToString(x.type), null, comment: x.documentationComment, meta1: GetMetaData(x), @@ -53,7 +53,10 @@ MethodDetails getMethodDetailsForFunctionType( .toList(); var typeParameters2 = fn.typeParameters // - .map((e) => GenericsNameType(e.name, e.bound == null ? null : e.bound.toString())) + .map((e) { + final bound = e.bound; + return GenericsNameType(e.name, bound == null ? null : typeToString(bound)); + }) .toList(); return MethodDetails(fn.documentationComment, fn.name ?? "", paramsPositional, paramsNamed, typeParameters2, returnType); @@ -63,7 +66,7 @@ List getAllFields(List interfaceTypes, Clas var superTypeFields = interfaceTypes // .where((x) => x.element.name != "Object") .flatMap((st) => st.element.fields.map((f) => // - NameTypeClassComment(f.name, f.type.toString(), st.element.name, comment: f.getter?.documentationComment))) + NameTypeClassComment(f.name, typeToString(f.type), st.element.name, comment: f.getter?.documentationComment))) .toList(); // if(element is ClassElement){ @@ -75,8 +78,60 @@ List getAllFields(List interfaceTypes, Clas // } var classFields = element.fields.map((f) => // - NameTypeClassComment(f.name, f.type.toString(), element.name, comment: f.getter?.documentationComment)).toList(); + NameTypeClassComment(f.name, typeToString(f.type), element.name, comment: f.getter?.documentationComment)).toList(); //distinct, will keep classFields over superTypeFields return (classFields + superTypeFields).distinctBy((x) => x.name).toList(); } + +String typeToString(DartType type) { + final alias = type.alias; + final manual = alias != null + ? aliasToString(alias) + : type is FunctionType + ? functionToString(type) + : type is RecordType + ? recordToString(type) + : type is ParameterizedType + ? genericToString(type) + : null; + final nullMarker = type.nullabilitySuffix == NullabilitySuffix.question ? '?' + : type.nullabilitySuffix == NullabilitySuffix.star ? '*' + : ''; + return manual != null ? "$manual$nullMarker" : type.toString(); +} + +String aliasToString(InstantiatedTypeAliasElement alias) => "${alias.element.name}${alias.typeArguments.isEmpty ? '' : "<${alias.typeArguments.map(typeToString).join(', ')}>"}"; + +String functionToString(FunctionType type) { + final generics = type.typeFormals.isNotEmpty ? "<${ + type.typeFormals + .map((param) { + final bound = param.bound; + return "${param.name}${bound == null ? "" : " = ${typeToString(bound)}"}"; + }) + .join(', ') + }>" : ''; + final normal = type.normalParameterNames.mapIndexed( + (index, name) => "${typeToString(type.normalParameterTypes[index])} $name" + ).join(', '); + final named = type.namedParameterTypes.mapEntries((entry) => "${entry.value.element!.hasRequired ? 'required ' : ''}${typeToString(entry.value)} ${entry.key}").join(', '); + final optional = type.optionalParameterNames.mapIndexed( + (index, name) => "${typeToString(type.optionalParameterTypes[index])} $name" + ).join(', '); + return "${typeToString(type.returnType)} Function$generics(${ + [if (normal.isNotEmpty) normal, if (named.isNotEmpty) "{$named}", if (optional.isNotEmpty) "[$optional]"].join(', ') + })"; +} + +String recordToString(RecordType type) { + final positional = type.positionalFields.map((e) => typeToString(e.type)).join(', '); + final named = type.namedFields.map((e) => "${typeToString(e.type)} ${e.name}").join(', '); + final trailing = type.positionalFields.length == 1 && type.namedFields.length == 0 ? ',' : ''; + return "(${[if (positional.isNotEmpty) positional, if (named.isNotEmpty) "{$named}"].join(', ')}$trailing)"; +} + +String genericToString(ParameterizedType type) { + final arguments = type.typeArguments.isEmpty ? '' : "<${type.typeArguments.map(typeToString).join(', ')}>"; + return "${type.element!.name}$arguments"; +} diff --git a/morphy/lib/src/helpers.dart b/morphy/lib/src/helpers.dart index c35993b..8eb4e5c 100644 --- a/morphy/lib/src/helpers.dart +++ b/morphy/lib/src/helpers.dart @@ -191,11 +191,11 @@ String getInitialiser(List fields) { String getToString(List fields, String className) { if (fields.isEmpty) { - return """String toString() => "($className-)"""; + return """__String toString() => "($className-)"""; } var items = fields.map((e) => "${e.name}:\${${e.name}.toString()}").joinToString(separator: "|"); - return """String toString() => "($className-$items)";"""; + return """__String toString() => "($className-$items)";"""; } String getHashCode(List fields) { @@ -204,13 +204,13 @@ String getHashCode(List fields) { } var items = fields.map((e) => "${e.name}.hashCode").joinToString(separator: ", "); - return """int get hashCode => hashObjects([$items]);"""; + return """__int get hashCode => __hashObjects([$items]);"""; } String getEquals(List fields, String className) { var sb = StringBuffer(); - sb.write("bool operator ==(Object other) => identical(this, other) || other is $className && runtimeType == other.runtimeType"); + sb.write("__bool operator ==(__Object other) => __identical(this, other) || other is $className && runtimeType == other.runtimeType"); sb.writeln(fields.isEmpty ? "" : " &&"); @@ -391,7 +391,7 @@ String getConstructorName(String trimmedClassName, bool hasCustomConstructor) { String generateFromJsonHeader(String className) { var _className = "${className.replaceFirst("\$", "")}"; - return "factory ${_className.replaceFirst("\$", "")}.fromJson(Map json) {"; + return "factory ${_className.replaceFirst("\$", "")}.fromJson(__Map<__String, dynamic> json) {"; } String generateFromJsonBody(String className, List generics, List interfaces) { @@ -435,7 +435,7 @@ String generateFromJsonBody(String className, List generics, List generics) { if (className.startsWith("\$\$")) { - return "Map toJson_2([Map? fns]);"; + return "__Map<__String, dynamic> toJson_2([__Map<__Type, __Object? Function(__Never)>? fns]);"; } var _className = "${className.replaceFirst("\$", "")}"; @@ -445,7 +445,7 @@ String generateToJson(String className, List generics) { .join("\n"); var toJsonParams = generics // - .map((e) => " fn_${e.name} as Object? Function(${e.name})") + .map((e) => " fn_${e.name} as __Object? Function(${e.name})") .join(",\n"); var recordType = generics // @@ -454,16 +454,16 @@ String generateToJson(String className, List generics) { var result = """ // ignore: unused_field - Map _fns = {}; + __Map<__Type, __Object? Function(__Never)> _fns = {}; - Map toJson_2([Map? fns]){ + __Map<__String, dynamic> toJson_2([__Map<__Type, __Object? Function(__Never)>? fns]){ this._fns = fns ?? {}; return toJson(); } - Map toJson() { + __Map<__String, dynamic> toJson() { $getGenericFn - final Map data = _\$${_className}ToJson(this, + final __Map<__String, dynamic> data = _\$${_className}ToJson(this, $toJsonParams); // Adding custom key-value pair data['_className_'] = '$_className'; @@ -479,11 +479,11 @@ String createJsonSingleton(String classNameTrim, List generics) { if (generics.length == 0) // return ""; - var objects = generics.map((e) => "Object").join(", "); + var objects = generics.map((e) => "__Object").join(", "); var result = """ class ${classNameTrim}_Generics_Sing { - Map, $classNameTrim<${objects}> Function(Map)> fns = {}; + __Map<__List<__String>, $classNameTrim<${objects}> Function(__Map<__String, dynamic>)> fns = {}; factory ${classNameTrim}_Generics_Sing() => _singleton; static final ${classNameTrim}_Generics_Sing _singleton = ${classNameTrim}_Generics_Sing._internal(); diff --git a/morphy/test/helpers_test.dart b/morphy/test/helpers_test.dart index a5cecf6..f689117 100644 --- a/morphy/test/helpers_test.dart +++ b/morphy/test/helpers_test.dart @@ -324,7 +324,7 @@ void main() { test("1g", () { var result = getToString([], "MyClass"); - expectS(result.toString(), """String toString() => "(MyClass-)"""); + expectS(result.toString(), """__String toString() => "(MyClass-)"""); }); test("2g", () { @@ -334,7 +334,7 @@ void main() { NameTypeClassComment("c", "String", null), ], "MyClass"); - expectS(result.toString(), """String toString() => "(MyClass-a:\${a.toString()}|b:\${b.toString()}|c:\${c.toString()})";"""); + expectS(result.toString(), """__String toString() => "(MyClass-a:\${a.toString()}|b:\${b.toString()}|c:\${c.toString()})";"""); }); }); @@ -354,7 +354,7 @@ void main() { expectS( result.toString(), // - """int get hashCode => hashObjects([a.hashCode, b.hashCode, c.hashCode]);"""); + """__int get hashCode => __hashObjects([a.hashCode, b.hashCode, c.hashCode]);"""); }); }); @@ -362,7 +362,7 @@ void main() { test("1i", () { var result = getEquals([], "A"); - var expected = """bool operator ==(Object other) => identical(this, other) || other is A && runtimeType == other.runtimeType + var expected = """__bool operator ==(__Object other) => __identical(this, other) || other is A && runtimeType == other.runtimeType ;"""; expectS(result, expected); @@ -375,7 +375,7 @@ void main() { NameTypeClassComment("c", "String", null), ], "C"); - var expected = """bool operator ==(Object other) => identical(this, other) || other is C && runtimeType == other.runtimeType && + var expected = """__bool operator ==(__Object other) => __identical(this, other) || other is C && runtimeType == other.runtimeType && a == other.a && b == other.b && c == other.c;"""; expectS(result, expected); @@ -1381,7 +1381,7 @@ c: (this as C).c, group("generateFromJsonHeader", () { test("1r ", () { var result = generateFromJsonHeader("\$Pet"); - expectS(result, "factory Pet.fromJson(Map json) {"); + expectS(result, "factory Pet.fromJson(__Map<__String, dynamic> json) {"); }); }); @@ -1510,16 +1510,16 @@ c: (this as C).c, var result = generateToJson("\$Pet", []); var expected = """// ignore: unused_field\n - Map _fns = {}; + __Map<__Type, __Object? Function(__Never)> _fns = {}; - Map toJson_2([Map? fns]){ + __Map<__String, dynamic> toJson_2([__Map<__Type, __Object? Function(__Never)>? fns]){ this._fns = fns ?? {}; return toJson(); } - Map toJson() { + __Map<__String, dynamic> toJson() { - final Map data = _\$PetToJson(this, + final __Map<__String, dynamic> data = _\$PetToJson(this, ); // Adding custom key-value pair data['_className_'] = 'Pet'; @@ -1542,21 +1542,21 @@ c: (this as C).c, ); var expected = """// ignore: unused_field\n - Map _fns = {}; + __Map<__Type, __Object? Function(__Never)> _fns = {}; - Map toJson_2([Map? fns]){ + __Map<__String, dynamic> toJson_2([__Map<__Type, __Object? Function(__Never)>? fns]){ this._fns = fns ?? {}; return toJson(); } - Map toJson() { + __Map<__String, dynamic> toJson() { var fn_T = getGenericToJsonFn(_fns, T); var fn_T2 = getGenericToJsonFn(_fns, T2); var fn_T3 = getGenericToJsonFn(_fns, T3); - final Map data = _\$PetToJson(this, - fn_T as Object? Function(T), - fn_T2 as Object? Function(T2), - fn_T3 as Object? Function(T3)); + final __Map<__String, dynamic> data = _\$PetToJson(this, + fn_T as __Object? Function(T), + fn_T2 as __Object? Function(T2), + fn_T3 as __Object? Function(T3)); // Adding custom key-value pair data['_className_'] = 'Pet'; data['_T_'] = T.toString(); @@ -1573,7 +1573,7 @@ c: (this as C).c, var result = generateToJson("\$\$Pet", []); var expected = """ - Map toJson_2([Map? fns]); + __Map<__String, dynamic> toJson_2([__Map<__Type, __Object? Function(__Never)>? fns]); """; expectS(result, expected); @@ -1591,7 +1591,7 @@ c: (this as C).c, var expected = """ class B_Generics_Sing { - Map, B Function(Map)> fns = {}; + __Map<__List<__String>, B<__Object> Function(__Map<__String, dynamic>)> fns = {}; factory B_Generics_Sing() => _singleton; static final B_Generics_Sing _singleton = B_Generics_Sing._internal(); @@ -1615,7 +1615,7 @@ class B_Generics_Sing { var expected = """ class C_Generics_Sing { - Map, C Function(Map)> fns = {}; + __Map<__List<__String>, C<__Object, __Object, __Object> Function(__Map<__String, dynamic>)> fns = {}; factory C_Generics_Sing() => _singleton; static final C_Generics_Sing _singleton = C_Generics_Sing._internal(); From a2b274186cdc9675ddf074338f7f5bdd95021a91 Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Fri, 17 May 2024 13:01:52 +0100 Subject: [PATCH 2/4] add warning suppression for underscored types --- morphy/lib/morphy2Builder.dart | 1 + morphy/lib/morphyBuilder.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/morphy/lib/morphy2Builder.dart b/morphy/lib/morphy2Builder.dart index 2e00e75..3a3b2a3 100644 --- a/morphy/lib/morphy2Builder.dart +++ b/morphy/lib/morphy2Builder.dart @@ -8,4 +8,5 @@ Builder morphy2Builder(BuilderOptions options) => // header: ''' // ignore_for_file: UNNECESSARY_CAST // ignore_for_file: unused_element +// ignore_for_file: library_private_types_in_public_api '''); diff --git a/morphy/lib/morphyBuilder.dart b/morphy/lib/morphyBuilder.dart index cc72b7f..7af0b43 100644 --- a/morphy/lib/morphyBuilder.dart +++ b/morphy/lib/morphyBuilder.dart @@ -8,4 +8,5 @@ Builder morphyBuilder(BuilderOptions options) => // header: ''' // ignore_for_file: UNNECESSARY_CAST // ignore_for_file: unused_element +// ignore_for_file: library_private_types_in_public_api '''); From 941eb6b4e5ce322d7ea68e25f74aea9e10e9497f Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Fri, 17 May 2024 13:13:51 +0100 Subject: [PATCH 3/4] only add typedefs if there are some outputs --- morphy/lib/src/common/GeneratorForAnnotationX.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/morphy/lib/src/common/GeneratorForAnnotationX.dart b/morphy/lib/src/common/GeneratorForAnnotationX.dart index c42df2f..3e87fc7 100644 --- a/morphy/lib/src/common/GeneratorForAnnotationX.dart +++ b/morphy/lib/src/common/GeneratorForAnnotationX.dart @@ -3,7 +3,6 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; -import 'dart:developer'; import 'package:analyzer/dart/element/element.dart'; // ignore: import_of_legacy_library_into_null_safe @@ -43,8 +42,7 @@ abstract class GeneratorForAnnotationX extends Generator { @override FutureOr generate(LibraryReader library, BuildStep buildStep) async { - final values = this is MorphyGenerator - ? { + const typedefs = """ typedef __String = String; typedef __Object = Object; @@ -57,8 +55,8 @@ typedef __bool = bool; const __hashObjects = hashObjects; const __identical = identical; """ - } - : {}; + ; + final values = Set(); var classElements = library.allElements // .whereType() @@ -77,7 +75,10 @@ const __identical = identical; } } - return values.join('\n\n'); + return [ + if (this is MorphyGenerator && values.isNotEmpty) typedefs, + ...values + ].join('\n\n'); } /// Implement to return source code to generate for [element]. From 87bcafda087a6ba32844eaf84bf1bc1b41b70ad8 Mon Sep 17 00:00:00 2001 From: Michael Tsang Date: Fri, 17 May 2024 13:43:08 +0100 Subject: [PATCH 4/4] __List is not always used in generated files --- morphy/lib/src/common/GeneratorForAnnotationX.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/morphy/lib/src/common/GeneratorForAnnotationX.dart b/morphy/lib/src/common/GeneratorForAnnotationX.dart index 3e87fc7..98b1ff4 100644 --- a/morphy/lib/src/common/GeneratorForAnnotationX.dart +++ b/morphy/lib/src/common/GeneratorForAnnotationX.dart @@ -46,6 +46,7 @@ abstract class GeneratorForAnnotationX extends Generator { """ typedef __String = String; typedef __Object = Object; +// ignore: unused_element typedef __List = List; typedef __Map = Map; typedef __Never = Never;