| // Copyright 2013 The Flutter Authors. 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:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'package:source_gen/source_gen.dart'; |
| import 'package:source_helper/source_helper.dart'; |
| |
| /// The name of the generated, private helper for converting [String] to [bool]. |
| const String boolConverterHelperName = r'_$boolConverter'; |
| |
| /// The name of the generated, private helper for handling nullable value |
| /// conversion. |
| const String convertMapValueHelperName = r'_$convertMapValue'; |
| |
| /// The name of the generated, private helper for converting [Duration] to |
| /// [bool]. |
| const String durationDecoderHelperName = r'_$duractionConverter'; |
| |
| /// The name of the generated, private helper for converting [String] to [Enum]. |
| const String enumExtensionHelperName = r'_$fromName'; |
| |
| /// The property/parameter name used to represent the `extra` data that may |
| /// be passed to a route. |
| const String extraFieldName = r'$extra'; |
| |
| /// Shared start of error message related to a likely code issue. |
| const String likelyIssueMessage = 'Should never get here! File an issue!'; |
| |
| const List<_TypeHelper> _helpers = <_TypeHelper>[ |
| _TypeHelperBigInt(), |
| _TypeHelperBool(), |
| _TypeHelperDateTime(), |
| _TypeHelperDouble(), |
| _TypeHelperEnum(), |
| _TypeHelperInt(), |
| _TypeHelperNum(), |
| _TypeHelperString(), |
| _TypeHelperUri(), |
| _TypeHelperIterable(), |
| ]; |
| |
| /// Returns the decoded [String] value for [element], if its type is supported. |
| /// |
| /// Otherwise, throws an [InvalidGenerationSourceError]. |
| String decodeParameter(ParameterElement element) { |
| if (element.isExtraField) { |
| return 'state.${_stateValueAccess(element)}'; |
| } |
| |
| final DartType paramType = element.type; |
| for (final _TypeHelper helper in _helpers) { |
| if (helper._matchesType(paramType)) { |
| String decoded = helper._decode(element); |
| if (element.isOptional && element.hasDefaultValue) { |
| if (element.type.isNullableType) { |
| throw NullableDefaultValueError(element); |
| } |
| decoded += ' ?? ${element.defaultValueCode!}'; |
| } |
| return decoded; |
| } |
| } |
| |
| throw InvalidGenerationSourceError( |
| 'The parameter type ' |
| '`${paramType.getDisplayString(withNullability: false)}` is not supported.', |
| element: element, |
| ); |
| } |
| |
| /// Returns the encoded [String] value for [element], if its type is supported. |
| /// |
| /// Otherwise, throws an [InvalidGenerationSourceError]. |
| String encodeField(PropertyAccessorElement element) { |
| for (final _TypeHelper helper in _helpers) { |
| if (helper._matchesType(element.returnType)) { |
| return helper._encode(element.name, element.returnType); |
| } |
| } |
| |
| throw InvalidGenerationSourceError( |
| 'The return type `${element.returnType}` is not supported.', |
| element: element, |
| ); |
| } |
| |
| /// Gets the name of the `const` map generated to help encode [Enum] types. |
| // TODO(stuartmorgan): Remove this ignore once 'analyze' can be set to |
| // 5.2+ (when Flutter 3.4+ is on stable). |
| // ignore: deprecated_member_use |
| String enumMapName(InterfaceType type) => '_\$${type.element.name}EnumMap'; |
| |
| String _stateValueAccess(ParameterElement element) { |
| if (element.isExtraField) { |
| return 'extra as ${element.type.getDisplayString(withNullability: element.isOptional)}'; |
| } |
| |
| if (element.isRequired) { |
| return 'pathParameters[${escapeDartString(element.name)}]!'; |
| } |
| |
| if (element.isOptional) { |
| return 'queryParameters[${escapeDartString(element.name.kebab)}]'; |
| } |
| |
| throw InvalidGenerationSourceError( |
| '$likelyIssueMessage (param not required or optional)', |
| element: element, |
| ); |
| } |
| |
| abstract class _TypeHelper { |
| const _TypeHelper(); |
| |
| /// Decodes the value from its string representation in the URL. |
| String _decode(ParameterElement parameterElement); |
| |
| /// Encodes the value from its string representation in the URL. |
| String _encode(String fieldName, DartType type); |
| |
| bool _matchesType(DartType type); |
| } |
| |
| class _TypeHelperBigInt extends _TypeHelperWithHelper { |
| const _TypeHelperBigInt(); |
| |
| @override |
| String helperName(DartType paramType) => 'BigInt.parse'; |
| |
| @override |
| String _encode(String fieldName, DartType type) => |
| '$fieldName${type.ensureNotNull}.toString()'; |
| |
| @override |
| bool _matchesType(DartType type) => |
| const TypeChecker.fromRuntime(BigInt).isAssignableFromType(type); |
| } |
| |
| class _TypeHelperBool extends _TypeHelperWithHelper { |
| const _TypeHelperBool(); |
| |
| @override |
| String helperName(DartType paramType) => boolConverterHelperName; |
| |
| @override |
| String _encode(String fieldName, DartType type) => |
| '$fieldName${type.ensureNotNull}.toString()'; |
| |
| @override |
| bool _matchesType(DartType type) => type.isDartCoreBool; |
| } |
| |
| class _TypeHelperDateTime extends _TypeHelperWithHelper { |
| const _TypeHelperDateTime(); |
| |
| @override |
| String helperName(DartType paramType) => 'DateTime.parse'; |
| |
| @override |
| String _encode(String fieldName, DartType type) => |
| '$fieldName${type.ensureNotNull}.toString()'; |
| |
| @override |
| bool _matchesType(DartType type) => |
| const TypeChecker.fromRuntime(DateTime).isAssignableFromType(type); |
| } |
| |
| class _TypeHelperDouble extends _TypeHelperWithHelper { |
| const _TypeHelperDouble(); |
| |
| @override |
| String helperName(DartType paramType) => 'double.parse'; |
| |
| @override |
| String _encode(String fieldName, DartType type) => |
| '$fieldName${type.ensureNotNull}.toString()'; |
| |
| @override |
| bool _matchesType(DartType type) => type.isDartCoreDouble; |
| } |
| |
| class _TypeHelperEnum extends _TypeHelperWithHelper { |
| const _TypeHelperEnum(); |
| |
| @override |
| String helperName(DartType paramType) => |
| '${enumMapName(paramType as InterfaceType)}.$enumExtensionHelperName'; |
| |
| @override |
| String _encode(String fieldName, DartType type) => |
| '${enumMapName(type as InterfaceType)}[$fieldName${type.ensureNotNull}]'; |
| |
| @override |
| bool _matchesType(DartType type) => type.isEnum; |
| } |
| |
| class _TypeHelperInt extends _TypeHelperWithHelper { |
| const _TypeHelperInt(); |
| |
| @override |
| String helperName(DartType paramType) => 'int.parse'; |
| |
| @override |
| String _encode(String fieldName, DartType type) => |
| '$fieldName${type.ensureNotNull}.toString()'; |
| |
| @override |
| bool _matchesType(DartType type) => type.isDartCoreInt; |
| } |
| |
| class _TypeHelperNum extends _TypeHelperWithHelper { |
| const _TypeHelperNum(); |
| |
| @override |
| String helperName(DartType paramType) => 'num.parse'; |
| |
| @override |
| String _encode(String fieldName, DartType type) => |
| '$fieldName${type.ensureNotNull}.toString()'; |
| |
| @override |
| bool _matchesType(DartType type) => type.isDartCoreNum; |
| } |
| |
| class _TypeHelperString extends _TypeHelper { |
| const _TypeHelperString(); |
| |
| @override |
| String _decode(ParameterElement parameterElement) => |
| 'state.${_stateValueAccess(parameterElement)}'; |
| |
| @override |
| String _encode(String fieldName, DartType type) => fieldName; |
| |
| @override |
| bool _matchesType(DartType type) => type.isDartCoreString; |
| } |
| |
| class _TypeHelperUri extends _TypeHelperWithHelper { |
| const _TypeHelperUri(); |
| |
| @override |
| String helperName(DartType paramType) => 'Uri.parse'; |
| |
| @override |
| String _encode(String fieldName, DartType type) => |
| '$fieldName${type.ensureNotNull}.toString()'; |
| |
| @override |
| bool _matchesType(DartType type) => |
| const TypeChecker.fromRuntime(Uri).isAssignableFromType(type); |
| } |
| |
| class _TypeHelperIterable extends _TypeHelper { |
| const _TypeHelperIterable(); |
| |
| @override |
| String _decode(ParameterElement parameterElement) { |
| if (parameterElement.type is ParameterizedType) { |
| final DartType iterableType = |
| (parameterElement.type as ParameterizedType).typeArguments.first; |
| |
| // get a type converter for values in iterable |
| String entriesTypeDecoder = '(e) => e'; |
| for (final _TypeHelper helper in _helpers) { |
| if (helper._matchesType(iterableType) && |
| helper is _TypeHelperWithHelper) { |
| entriesTypeDecoder = helper.helperName(iterableType); |
| } |
| } |
| |
| // get correct type for iterable |
| String iterableCaster = ''; |
| if (const TypeChecker.fromRuntime(List) |
| .isAssignableFromType(parameterElement.type)) { |
| iterableCaster = '.toList()'; |
| } else if (const TypeChecker.fromRuntime(Set) |
| .isAssignableFromType(parameterElement.type)) { |
| iterableCaster = '.toSet()'; |
| } |
| |
| return ''' |
| state.queryParametersAll[ |
| ${escapeDartString(parameterElement.name.kebab)}] |
| ?.map($entriesTypeDecoder)$iterableCaster'''; |
| } |
| return ''' |
| state.queryParametersAll[${escapeDartString(parameterElement.name.kebab)}]'''; |
| } |
| |
| @override |
| String _encode(String fieldName, DartType type) { |
| final String nullAwareAccess = type.isNullableType ? '?' : ''; |
| if (type is ParameterizedType) { |
| final DartType iterableType = type.typeArguments.first; |
| |
| // get a type encoder for values in iterable |
| String entriesTypeEncoder = ''; |
| for (final _TypeHelper helper in _helpers) { |
| if (helper._matchesType(iterableType)) { |
| entriesTypeEncoder = ''' |
| $nullAwareAccess.map((e) => ${helper._encode('e', iterableType)}).toList()'''; |
| } |
| } |
| return ''' |
| $fieldName$entriesTypeEncoder'''; |
| } |
| |
| return ''' |
| $fieldName$nullAwareAccess.map((e) => e.toString()).toList()'''; |
| } |
| |
| @override |
| bool _matchesType(DartType type) => |
| const TypeChecker.fromRuntime(Iterable).isAssignableFromType(type); |
| } |
| |
| abstract class _TypeHelperWithHelper extends _TypeHelper { |
| const _TypeHelperWithHelper(); |
| |
| String helperName(DartType paramType); |
| |
| @override |
| String _decode(ParameterElement parameterElement) { |
| final DartType paramType = parameterElement.type; |
| |
| if (!parameterElement.isRequired) { |
| return '$convertMapValueHelperName(' |
| '${escapeDartString(parameterElement.name.kebab)}, ' |
| 'state.queryParameters, ' |
| '${helperName(paramType)})'; |
| } |
| return '${helperName(paramType)}' |
| '(state.${_stateValueAccess(parameterElement)})'; |
| } |
| } |
| |
| /// Extension helpers on [DartType]. |
| extension DartTypeExtension on DartType { |
| /// Convenient helper for nullability checks. |
| String get ensureNotNull => isNullableType ? '!' : ''; |
| } |
| |
| /// Extension helpers on [ParameterElement]. |
| extension ParameterElementExtension on ParameterElement { |
| /// Convenient helper on top of [isRequiredPositional] and [isRequiredNamed]. |
| bool get isRequired => isRequiredPositional || isRequiredNamed; |
| |
| /// Returns `true` if `this` has a name that matches [extraFieldName]; |
| bool get isExtraField => name == extraFieldName; |
| } |
| |
| /// An error thrown when a default value is used with a nullable type. |
| class NullableDefaultValueError extends InvalidGenerationSourceError { |
| /// An error thrown when a default value is used with a nullable type. |
| NullableDefaultValueError( |
| Element element, |
| ) : super( |
| 'Default value used with a nullable type. Only non-nullable type can have a default value.', |
| todo: 'Remove the default value or make the type non-nullable.', |
| element: element, |
| ); |
| } |