From 5e73fc4bee3be2e3c6f10a9bcad8007677e5f5c5 Mon Sep 17 00:00:00 2001 From: Morlay Date: Sat, 7 Sep 2024 09:54:32 +0800 Subject: [PATCH] Support handling classDecl fromJson(String s) in JsonDecodable --- pkg/json/lib/json.dart | 191 ++++++++++++++++++++++------------------- 1 file changed, 101 insertions(+), 90 deletions(-) diff --git a/pkg/json/lib/json.dart b/pkg/json/lib/json.dart index bcf237f7a7aa..73a4461561a6 100644 --- a/pkg/json/lib/json.dart +++ b/pkg/json/lib/json.dart @@ -36,27 +36,27 @@ macro class JsonCodable /// Declares the `fromJson` constructor and `toJson` method, but does not /// implement them. @override - Future buildDeclarationsForClass( - ClassDeclaration clazz, MemberDeclarationBuilder builder) async { + Future buildDeclarationsForClass(ClassDeclaration clazz, + MemberDeclarationBuilder builder) async { final mapStringObject = await _setup(clazz, builder); await ( - _declareFromJson(clazz, builder, mapStringObject), - _declareToJson(clazz, builder, mapStringObject), + _declareFromJson(clazz, builder, mapStringObject), + _declareToJson(clazz, builder, mapStringObject), ).wait; } /// Provides the actual definitions of the `fromJson` constructor and `toJson` /// method, which were declared in the previous phase. @override - Future buildDefinitionForClass( - ClassDeclaration clazz, TypeDefinitionBuilder builder) async { + Future buildDefinitionForClass(ClassDeclaration clazz, + TypeDefinitionBuilder builder) async { final introspectionData = - await _SharedIntrospectionData.build(builder, clazz); + await _SharedIntrospectionData.build(builder, clazz); await ( - _buildFromJson(clazz, builder, introspectionData), - _buildToJson(clazz, builder, introspectionData), + _buildFromJson(clazz, builder, introspectionData), + _buildToJson(clazz, builder, introspectionData), ).wait; } } @@ -83,8 +83,8 @@ macro class JsonEncodable /// Declares the `toJson` method, but does not implement it. @override - Future buildDeclarationsForClass( - ClassDeclaration clazz, MemberDeclarationBuilder builder) async { + Future buildDeclarationsForClass(ClassDeclaration clazz, + MemberDeclarationBuilder builder) async { final mapStringObject = await _setup(clazz, builder); await _declareToJson(clazz, builder, mapStringObject); } @@ -92,10 +92,10 @@ macro class JsonEncodable /// Provides the actual definition of the `toJson` method, which was declared /// in the previous phase. @override - Future buildDefinitionForClass( - ClassDeclaration clazz, TypeDefinitionBuilder builder) async { + Future buildDefinitionForClass(ClassDeclaration clazz, + TypeDefinitionBuilder builder) async { final introspectionData = - await _SharedIntrospectionData.build(builder, clazz); + await _SharedIntrospectionData.build(builder, clazz); await _buildToJson(clazz, builder, introspectionData); } } @@ -122,8 +122,8 @@ macro class JsonDecodable /// Declares the `fromJson` constructor but does not implement it. @override - Future buildDeclarationsForClass( - ClassDeclaration clazz, MemberDeclarationBuilder builder) async { + Future buildDeclarationsForClass(ClassDeclaration clazz, + MemberDeclarationBuilder builder) async { final mapStringObject = await _setup(clazz, builder); await _declareFromJson(clazz, builder, mapStringObject); } @@ -131,10 +131,10 @@ macro class JsonDecodable /// Provides the actual definition of the `from` constructor, which was /// declared in the previous phase. @override - Future buildDefinitionForClass( - ClassDeclaration clazz, TypeDefinitionBuilder builder) async { + Future buildDefinitionForClass(ClassDeclaration clazz, + TypeDefinitionBuilder builder) async { final introspectionData = - await _SharedIntrospectionData.build(builder, clazz); + await _SharedIntrospectionData.build(builder, clazz); await _buildFromJson(clazz, builder, introspectionData); } } @@ -149,14 +149,14 @@ mixin _Shared { builder.report(Diagnostic( DiagnosticMessage( 'Only fields with explicit types are allowed on serializable ' - 'classes, please add a type.', + 'classes, please add a type.', target: type.asDiagnosticTarget), Severity.error)); } else { builder.report(Diagnostic( DiagnosticMessage( 'Only fields with named types are allowed on serializable ' - 'classes.', + 'classes.', target: type.asDiagnosticTarget), Severity.error)); } @@ -166,19 +166,19 @@ mixin _Shared { /// Does some basic validation on [clazz], and shared setup logic. /// /// Returns a code representation of the [Map] class. - Future _setup( - ClassDeclaration clazz, MemberDeclarationBuilder builder) async { + Future _setup(ClassDeclaration clazz, + MemberDeclarationBuilder builder) async { if (clazz.typeParameters.isNotEmpty) { throw DiagnosticException(Diagnostic(DiagnosticMessage( - // TODO: Target the actual type parameter, issue #55611 - 'Cannot be applied to classes with generic type parameters'), + // TODO: Target the actual type parameter, issue #55611 + 'Cannot be applied to classes with generic type parameters'), Severity.error)); } final (map, string, object) = await ( - builder.resolveIdentifier(_dartCore, 'Map'), - builder.resolveIdentifier(_dartCore, 'String'), - builder.resolveIdentifier(_dartCore, 'Object'), + builder.resolveIdentifier(_dartCore, 'Map'), + builder.resolveIdentifier(_dartCore, 'String'), + builder.resolveIdentifier(_dartCore, 'Object'), ).wait; return NamedTypeAnnotationCode(name: map, typeArguments: [ NamedTypeAnnotationCode(name: string), @@ -190,13 +190,12 @@ mixin _Shared { /// Shared logic for macros that want to generate a `fromJson` constructor. mixin _FromJson on _Shared { /// Builds the actual `fromJson` constructor. - Future _buildFromJson( - ClassDeclaration clazz, + Future _buildFromJson(ClassDeclaration clazz, TypeDefinitionBuilder typeBuilder, _SharedIntrospectionData introspectionData) async { final constructors = await typeBuilder.constructorsOf(clazz); final fromJson = - constructors.firstWhereOrNull((c) => c.identifier.name == 'fromJson'); + constructors.firstWhereOrNull((c) => c.identifier.name == 'fromJson'); if (fromJson == null) return; await _checkValidFromJson(fromJson, introspectionData, typeBuilder); final builder = await typeBuilder.buildConstructor(fromJson.identifier); @@ -208,7 +207,7 @@ mixin _FromJson on _Shared { if (superclassDeclaration != null && !superclassDeclaration.isExactly('Object', _dartCore)) { final superclassConstructors = - await builder.constructorsOf(superclassDeclaration); + await builder.constructorsOf(superclassDeclaration); for (final superConstructor in superclassConstructors) { if (superConstructor.identifier.name == 'fromJson') { await _checkValidFromJson( @@ -221,8 +220,8 @@ mixin _FromJson on _Shared { throw DiagnosticException(Diagnostic( DiagnosticMessage( 'Serialization of classes that extend other classes is only ' - 'supported if those classes have a valid ' - '`fromJson(Map json)` constructor.', + 'supported if those classes have a valid ' + '`fromJson(Map json)` constructor.', target: introspectionData.clazz.superclass?.asDiagnosticTarget), Severity.error)); } @@ -266,16 +265,16 @@ mixin _FromJson on _Shared { /// /// Returns `true` if the check succeeded (there was no `fromJson`) and false /// if it didn't (a diagnostic was emitted). - Future _checkNoFromJson( - DeclarationBuilder builder, ClassDeclaration clazz) async { + Future _checkNoFromJson(DeclarationBuilder builder, + ClassDeclaration clazz) async { final constructors = await builder.constructorsOf(clazz); final fromJson = - constructors.firstWhereOrNull((c) => c.identifier.name == 'fromJson'); + constructors.firstWhereOrNull((c) => c.identifier.name == 'fromJson'); if (fromJson != null) { builder.report(Diagnostic( DiagnosticMessage( 'Cannot generate a fromJson constructor due to this existing ' - 'one.', + 'one.', target: fromJson.asDiagnosticTarget), Severity.error)); return false; @@ -285,19 +284,18 @@ mixin _FromJson on _Shared { /// Checks that [constructor] is a valid `fromJson` constructor, and throws a /// [DiagnosticException] if not. - Future _checkValidFromJson( - ConstructorDeclaration constructor, + Future _checkValidFromJson(ConstructorDeclaration constructor, _SharedIntrospectionData introspectionData, DefinitionBuilder builder) async { if (constructor.namedParameters.isNotEmpty || constructor.positionalParameters.length != 1 || !(await (await builder - .resolve(constructor.positionalParameters.single.type.code)) + .resolve(constructor.positionalParameters.single.type.code)) .isExactly(introspectionData.jsonMapType))) { throw DiagnosticException(Diagnostic( DiagnosticMessage( 'Expected exactly one parameter, with the type ' - 'Map', + 'Map', target: constructor.asDiagnosticTarget), Severity.error)); } @@ -305,8 +303,7 @@ mixin _FromJson on _Shared { /// Returns a [Code] object which is an expression that converts a JSON map /// (referenced by [jsonReference]) into an instance of type [type]. - Future _convertTypeFromJson( - TypeAnnotation rawType, + Future _convertTypeFromJson(TypeAnnotation rawType, Code jsonReference, DefinitionBuilder builder, _SharedIntrospectionData introspectionData) async { @@ -325,10 +322,10 @@ mixin _FromJson on _Shared { var nullCheck = type.isNullable ? RawCode.fromParts([ - jsonReference, - // `null` is a reserved word, we can just use it. - ' == null ? null : ', - ]) + jsonReference, + // `null` is a reserved word, we can just use it. + ' == null ? null : ', + ]) : null; // Check for the supported core types, and deserialize them accordingly. @@ -387,6 +384,24 @@ mixin _FromJson on _Shared { .firstWhereOrNull((c) => c.identifier.name == 'fromJson') ?.identifier; if (fromJson != null) { + // to support fromJson(String s) or others not Map + final fromJsonFirstParam = constructors + .firstWhereOrNull((c) => c.identifier.name == 'fromJson') + ?.positionalParameters + .firstOrNull; + + if (fromJsonFirstParam?.type != null) { + return RawCode.fromParts([ + if (nullCheck != null) nullCheck, + fromJson, + '(', + jsonReference, + ' as ', + fromJsonFirstParam!.type.code, + ')', + ]); + } + return RawCode.fromParts([ if (nullCheck != null) nullCheck, fromJson, @@ -402,7 +417,7 @@ mixin _FromJson on _Shared { builder.report(Diagnostic( DiagnosticMessage( 'Unable to deserialize type, it must be a native JSON type or a ' - 'type with a `fromJson(Map json)` constructor.', + 'type with a `fromJson(Map json)` constructor.', target: type.asDiagnosticTarget), Severity.error)); return RawCode.fromString( @@ -411,8 +426,7 @@ mixin _FromJson on _Shared { /// Declares a `fromJson` constructor in [clazz], if one does not exist /// already. - Future _declareFromJson( - ClassDeclaration clazz, + Future _declareFromJson(ClassDeclaration clazz, MemberDeclarationBuilder builder, NamedTypeAnnotationCode mapStringObject) async { if (!(await _checkNoFromJson(builder, clazz))) return; @@ -431,13 +445,12 @@ mixin _FromJson on _Shared { /// Shared logic for macros that want to generate a `toJson` method. mixin _ToJson on _Shared { /// Builds the actual `toJson` method. - Future _buildToJson( - ClassDeclaration clazz, + Future _buildToJson(ClassDeclaration clazz, TypeDefinitionBuilder typeBuilder, _SharedIntrospectionData introspectionData) async { final methods = await typeBuilder.methodsOf(clazz); final toJson = - methods.firstWhereOrNull((c) => c.identifier.name == 'toJson'); + methods.firstWhereOrNull((c) => c.identifier.name == 'toJson'); if (toJson == null) return; if (!(await _checkValidToJson(toJson, introspectionData, typeBuilder))) { return; @@ -466,8 +479,8 @@ mixin _ToJson on _Shared { builder.report(Diagnostic( DiagnosticMessage( 'Serialization of classes that extend other classes is only ' - 'supported if those classes have a valid ' - '`Map toJson()` method.', + 'supported if those classes have a valid ' + '`Map toJson()` method.', target: introspectionData.clazz.superclass?.asDiagnosticTarget), Severity.error)); return; @@ -479,13 +492,14 @@ mixin _ToJson on _Shared { '{\n final json = ', if (superclassHasToJson) 'super.toJson()' - else ...[ - '<', - introspectionData.stringCode, - ', ', - introspectionData.objectCode.asNullable, - '>{}', - ], + else + ...[ + '<', + introspectionData.stringCode, + ', ', + introspectionData.objectCode.asNullable, + '>{}', + ], ';\n ', ]; @@ -532,11 +546,11 @@ mixin _ToJson on _Shared { /// /// Returns `true` if the check succeeded (there was no `toJson`) and false /// if it didn't (a diagnostic was emitted). - Future _checkNoToJson( - DeclarationBuilder builder, ClassDeclaration clazz) async { + Future _checkNoToJson(DeclarationBuilder builder, + ClassDeclaration clazz) async { final methods = await builder.methodsOf(clazz); final toJson = - methods.firstWhereOrNull((m) => m.identifier.name == 'toJson'); + methods.firstWhereOrNull((m) => m.identifier.name == 'toJson'); if (toJson != null) { builder.report(Diagnostic( DiagnosticMessage( @@ -550,8 +564,7 @@ mixin _ToJson on _Shared { /// Checks that [method] is a valid `toJson` method, and throws a /// [DiagnosticException] if not. - Future _checkValidToJson( - MethodDeclaration method, + Future _checkValidToJson(MethodDeclaration method, _SharedIntrospectionData introspectionData, DefinitionBuilder builder) async { if (method.namedParameters.isNotEmpty || @@ -561,7 +574,7 @@ mixin _ToJson on _Shared { builder.report(Diagnostic( DiagnosticMessage( 'Expected no parameters, and a return type of ' - 'Map', + 'Map', target: method.asDiagnosticTarget), Severity.error)); return false; @@ -571,8 +584,7 @@ mixin _ToJson on _Shared { /// Returns a [Code] object which is an expression that converts an instance /// of type [type] (referenced by [valueReference]) into a JSON map. - Future _convertTypeToJson( - TypeAnnotation rawType, + Future _convertTypeToJson(TypeAnnotation rawType, Code valueReference, DefinitionBuilder builder, _SharedIntrospectionData introspectionData) async { @@ -591,10 +603,10 @@ mixin _ToJson on _Shared { var nullCheck = type.isNullable ? RawCode.fromParts([ - valueReference, - // `null` is a reserved word, we can just use it. - ' == null ? null : ', - ]) + valueReference, + // `null` is a reserved word, we can just use it. + ' == null ? null : ', + ]) : null; // Check for the supported core types, and serialize them accordingly. @@ -644,7 +656,7 @@ mixin _ToJson on _Shared { builder.report(Diagnostic( DiagnosticMessage( 'Unable to serialize type, it must be a native JSON type or a ' - 'type with a `Map toJson()` method.', + 'type with a `Map toJson()` method.', target: type.asDiagnosticTarget), Severity.error)); return RawCode.fromString( @@ -652,8 +664,7 @@ mixin _ToJson on _Shared { } /// Declares a `toJson` method in [clazz], if one does not exist already. - Future _declareToJson( - ClassDeclaration clazz, + Future _declareToJson(ClassDeclaration clazz, MemberDeclarationBuilder builder, NamedTypeAnnotationCode mapStringObject) async { if (!(await _checkNoToJson(builder, clazz))) return; @@ -711,11 +722,11 @@ final class _SharedIntrospectionData { static Future<_SharedIntrospectionData> build( DeclarationPhaseIntrospector builder, ClassDeclaration clazz) async { final (list, map, mapEntry, object, string) = await ( - builder.resolveIdentifier(_dartCore, 'List'), - builder.resolveIdentifier(_dartCore, 'Map'), - builder.resolveIdentifier(_dartCore, 'MapEntry'), - builder.resolveIdentifier(_dartCore, 'Object'), - builder.resolveIdentifier(_dartCore, 'String'), + builder.resolveIdentifier(_dartCore, 'List'), + builder.resolveIdentifier(_dartCore, 'Map'), + builder.resolveIdentifier(_dartCore, 'MapEntry'), + builder.resolveIdentifier(_dartCore, 'Object'), + builder.resolveIdentifier(_dartCore, 'String'), ).wait; final objectCode = NamedTypeAnnotationCode(name: object); final nullableObjectCode = objectCode.asNullable; @@ -729,11 +740,11 @@ final class _SharedIntrospectionData { final stringCode = NamedTypeAnnotationCode(name: string); final superclass = clazz.superclass; final (fields, jsonMapType, superclassDecl) = await ( - builder.fieldsOf(clazz), - builder.resolve(jsonMapCode), - superclass == null - ? Future.value(null) - : builder.typeDeclarationOf(superclass.identifier), + builder.fieldsOf(clazz), + builder.resolve(jsonMapCode), + superclass == null + ? Future.value(null) + : builder.typeDeclarationOf(superclass.identifier), ).wait; return _SharedIntrospectionData( @@ -803,7 +814,7 @@ extension on NamedTypeAnnotation { builder.report(Diagnostic( DiagnosticMessage( 'Only fields with named types are allowed on serializable ' - 'classes', + 'classes', target: asDiagnosticTarget), Severity.error)); return null; @@ -814,7 +825,7 @@ extension on NamedTypeAnnotation { builder.report(Diagnostic( DiagnosticMessage( 'Only classes are supported as field types for serializable ' - 'classes', + 'classes', target: asDiagnosticTarget), Severity.error)); return null;