diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index 7dfd69d4499..a8a79ca41d2 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.7.5 + +- Fixes trailing `?` in the location when a go route has an empty default value. + ## 2.7.4 - Fixes an issue by removing unnecessary `const` in StatefulShellRouteData generation. diff --git a/packages/go_router_builder/example/lib/all_types.g.dart b/packages/go_router_builder/example/lib/all_types.g.dart index cb672ab6c8b..001ff5e46b7 100644 --- a/packages/go_router_builder/example/lib/all_types.g.dart +++ b/packages/go_router_builder/example/lib/all_types.g.dart @@ -598,44 +598,47 @@ extension $IterableRouteWithDefaultValuesExtension String get location => GoRouteData.$location( '/iterable-route-with-default-values', queryParams: { - if (intIterableField != const [0]) + if (!_$iterablesEqual(intIterableField, const [0])) 'int-iterable-field': intIterableField.map((e) => e.toString()).toList(), - if (doubleIterableField != const [0, 1, 2]) + if (!_$iterablesEqual(doubleIterableField, const [0, 1, 2])) 'double-iterable-field': doubleIterableField.map((e) => e.toString()).toList(), - if (stringIterableField != const ['defaultValue']) + if (!_$iterablesEqual( + stringIterableField, const ['defaultValue'])) 'string-iterable-field': stringIterableField.map((e) => e).toList(), - if (boolIterableField != const [false]) + if (!_$iterablesEqual(boolIterableField, const [false])) 'bool-iterable-field': boolIterableField.map((e) => e.toString()).toList(), - if (enumIterableField != - const [SportDetails.tennis, SportDetails.hockey]) + if (!_$iterablesEqual(enumIterableField, + const [SportDetails.tennis, SportDetails.hockey])) 'enum-iterable-field': enumIterableField.map((e) => _$SportDetailsEnumMap[e]).toList(), - if (intListField != const [0]) + if (!_$iterablesEqual(intListField, const [0])) 'int-list-field': intListField.map((e) => e.toString()).toList(), - if (doubleListField != const [1, 2, 3]) + if (!_$iterablesEqual(doubleListField, const [1, 2, 3])) 'double-list-field': doubleListField.map((e) => e.toString()).toList(), - if (stringListField != - const ['defaultValue0', 'defaultValue1']) + if (!_$iterablesEqual(stringListField, + const ['defaultValue0', 'defaultValue1'])) 'string-list-field': stringListField.map((e) => e).toList(), - if (boolListField != const [true]) + if (!_$iterablesEqual(boolListField, const [true])) 'bool-list-field': boolListField.map((e) => e.toString()).toList(), - if (enumListField != const [SportDetails.football]) + if (!_$iterablesEqual( + enumListField, const [SportDetails.football])) 'enum-list-field': enumListField.map((e) => _$SportDetailsEnumMap[e]).toList(), - if (intSetField != const {0, 1}) + if (!_$iterablesEqual(intSetField, const {0, 1})) 'int-set-field': intSetField.map((e) => e.toString()).toList(), - if (doubleSetField != const {}) + if (!_$iterablesEqual(doubleSetField, const {})) 'double-set-field': doubleSetField.map((e) => e.toString()).toList(), - if (stringSetField != const {'defaultValue'}) + if (!_$iterablesEqual(stringSetField, const {'defaultValue'})) 'string-set-field': stringSetField.map((e) => e).toList(), - if (boolSetField != const {true, false}) + if (!_$iterablesEqual(boolSetField, const {true, false})) 'bool-set-field': boolSetField.map((e) => e.toString()).toList(), - if (enumSetField != const {SportDetails.hockey}) + if (!_$iterablesEqual( + enumSetField, const {SportDetails.hockey})) 'enum-set-field': enumSetField.map((e) => _$SportDetailsEnumMap[e]).toList(), }, @@ -675,3 +678,17 @@ extension on Map { T _$fromName(String value) => entries.singleWhere((element) => element.value == value).key; } + +bool _$iterablesEqual(Iterable? iterable1, Iterable? iterable2) { + if (identical(iterable1, iterable2)) return true; + if (iterable1 == null || iterable2 == null) return false; + final iterator1 = iterable1.iterator; + final iterator2 = iterable2.iterator; + while (true) { + final hasNext1 = iterator1.moveNext(); + final hasNext2 = iterator2.moveNext(); + if (hasNext1 != hasNext2) return false; + if (!hasNext1) return true; + if (iterator1.current != iterator2.current) return false; + } +} diff --git a/packages/go_router_builder/example/test/location_test.dart b/packages/go_router_builder/example/test/location_test.dart new file mode 100644 index 00000000000..fc33e931f59 --- /dev/null +++ b/packages/go_router_builder/example/test/location_test.dart @@ -0,0 +1,49 @@ +// 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:flutter_test/flutter_test.dart'; +import 'package:go_router_builder_example/all_types.dart'; + +void main() { + test('IterableRouteWithDefaultValues', () { + expect( + const IterableRouteWithDefaultValues().location, + '/iterable-route-with-default-values', + ); + + // Needs to not be a const to test + // https://github.com/flutter/flutter/issues/127825. + final Set doubleSetField = {}; + expect( + IterableRouteWithDefaultValues( + doubleSetField: doubleSetField, + ).location, + '/iterable-route-with-default-values', + ); + + expect( + IterableRouteWithDefaultValues( + doubleSetField: {0.0, 1.0}, + ).location, + '/iterable-route-with-default-values?double-set-field=0.0&double-set-field=1.0', + ); + + // Needs to not be a const to test + // https://github.com/flutter/flutter/issues/127825. + final Set intSetField = {0, 1}; + expect( + IterableRouteWithDefaultValues( + intSetField: intSetField, + ).location, + '/iterable-route-with-default-values', + ); + + expect( + const IterableRouteWithDefaultValues( + intSetField: {0, 1, 2}, + ).location, + '/iterable-route-with-default-values?int-set-field=0&int-set-field=1&int-set-field=2', + ); + }); +} diff --git a/packages/go_router_builder/lib/src/route_config.dart b/packages/go_router_builder/lib/src/route_config.dart index b773c533ac1..b35ce4abb4f 100644 --- a/packages/go_router_builder/lib/src/route_config.dart +++ b/packages/go_router_builder/lib/src/route_config.dart @@ -313,7 +313,9 @@ class GoRouteConfig extends RouteBaseConfig { if (param.type.isNullableType) { throw NullableDefaultValueError(param); } - conditions.add('$parameterName != ${param.defaultValueCode!}'); + conditions.add( + compareField(param, parameterName, param.defaultValueCode!), + ); } else if (param.type.isNullableType) { conditions.add('$parameterName != null'); } @@ -734,6 +736,7 @@ const Map helperNames = { convertMapValueHelperName: _convertMapValueHelper, boolConverterHelperName: _boolConverterHelper, enumExtensionHelperName: _enumConverterHelper, + iterablesEqualHelperName: _iterableEqualsHelper, }; const String _convertMapValueHelper = ''' @@ -765,3 +768,18 @@ extension on Map { T $enumExtensionHelperName(String value) => entries.singleWhere((element) => element.value == value).key; }'''; + +const String _iterableEqualsHelper = ''' +bool $iterablesEqualHelperName(Iterable? iterable1, Iterable? iterable2) { + if (identical(iterable1, iterable2)) return true; + if (iterable1 == null || iterable2 == null) return false; + final iterator1 = iterable1.iterator; + final iterator2 = iterable2.iterator; + while (true) { + final hasNext1 = iterator1.moveNext(); + final hasNext2 = iterator2.moveNext(); + if (hasNext1 != hasNext2) return false; + if (!hasNext1) return true; + if (iterator1.current != iterator2.current) return false; + } +}'''; diff --git a/packages/go_router_builder/lib/src/type_helpers.dart b/packages/go_router_builder/lib/src/type_helpers.dart index d375c013e20..a822ea86280 100644 --- a/packages/go_router_builder/lib/src/type_helpers.dart +++ b/packages/go_router_builder/lib/src/type_helpers.dart @@ -28,6 +28,9 @@ 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!'; +/// The name of the generated, private helper for comparing iterables. +const String iterablesEqualHelperName = r'_$iterablesEqual'; + const List<_TypeHelper> _helpers = <_TypeHelper>[ _TypeHelperBigInt(), _TypeHelperBool(), @@ -86,6 +89,22 @@ String encodeField(PropertyAccessorElement element) { ); } +/// Returns the comparison of a parameter with its default value. +/// +/// Otherwise, throws an [InvalidGenerationSourceError]. +String compareField(ParameterElement param, String value1, String value2) { + for (final _TypeHelper helper in _helpers) { + if (helper._matchesType(param.type)) { + return helper._compare(param.name, param.defaultValueCode!); + } + } + + throw InvalidGenerationSourceError( + 'The type `${param.type}` is not supported.', + element: param, + ); +} + /// Gets the name of the `const` map generated to help encode [Enum] types. String enumMapName(InterfaceType type) => '_\$${type.element.name}EnumMap'; @@ -119,6 +138,8 @@ abstract class _TypeHelper { String _encode(String fieldName, DartType type); bool _matchesType(DartType type); + + String _compare(String value1, String value2) => '$value1 != $value2'; } class _TypeHelperBigInt extends _TypeHelperWithHelper { @@ -252,9 +273,12 @@ class _TypeHelperUri extends _TypeHelperWithHelper { const TypeChecker.fromRuntime(Uri).isAssignableFromType(type); } -class _TypeHelperIterable extends _TypeHelper { +class _TypeHelperIterable extends _TypeHelperWithHelper { const _TypeHelperIterable(); + @override + String helperName(DartType paramType) => iterablesEqualHelperName; + @override String _decode( ParameterElement parameterElement, Set pathParameters) { @@ -324,6 +348,10 @@ $fieldName$nullAwareAccess.map((e) => e.toString()).toList()'''; @override bool _matchesType(DartType type) => const TypeChecker.fromRuntime(Iterable).isAssignableFromType(type); + + @override + String _compare(String value1, String value2) => + '!$iterablesEqualHelperName($value1, $value2)'; } abstract class _TypeHelperWithHelper extends _TypeHelper { diff --git a/packages/go_router_builder/pubspec.yaml b/packages/go_router_builder/pubspec.yaml index 1f4e9da1504..6c2ab45cb38 100644 --- a/packages/go_router_builder/pubspec.yaml +++ b/packages/go_router_builder/pubspec.yaml @@ -2,7 +2,7 @@ name: go_router_builder description: >- A builder that supports generated strongly-typed route helpers for package:go_router -version: 2.7.4 +version: 2.7.5 repository: https://github.com/flutter/packages/tree/main/packages/go_router_builder issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router_builder%22 diff --git a/packages/go_router_builder/test_inputs/iterable_with_default_value.dart.expect b/packages/go_router_builder/test_inputs/iterable_with_default_value.dart.expect index 0b53e45b02d..625e92c0984 100644 --- a/packages/go_router_builder/test_inputs/iterable_with_default_value.dart.expect +++ b/packages/go_router_builder/test_inputs/iterable_with_default_value.dart.expect @@ -13,7 +13,7 @@ extension $IterableDefaultValueRouteExtension on IterableDefaultValueRoute { String get location => GoRouteData.$location( '/iterable-default-value-route', queryParams: { - if (param != const [0]) + if (!_$iterablesEqual(param, const [0])) 'param': param.map((e) => e.toString()).toList(), }, ); @@ -27,3 +27,17 @@ extension $IterableDefaultValueRouteExtension on IterableDefaultValueRoute { void replace(BuildContext context) => context.replace(location); } + +bool _$iterablesEqual(Iterable? iterable1, Iterable? iterable2) { + if (identical(iterable1, iterable2)) return true; + if (iterable1 == null || iterable2 == null) return false; + final iterator1 = iterable1.iterator; + final iterator2 = iterable2.iterator; + while (true) { + final hasNext1 = iterator1.moveNext(); + final hasNext2 = iterator2.moveNext(); + if (hasNext1 != hasNext2) return false; + if (!hasNext1) return true; + if (iterator1.current != iterator2.current) return false; + } +} diff --git a/packages/go_router_builder/test_inputs/list.dart.expect b/packages/go_router_builder/test_inputs/list.dart.expect index 2fbf6eef07b..3c1e5a726fb 100644 --- a/packages/go_router_builder/test_inputs/list.dart.expect +++ b/packages/go_router_builder/test_inputs/list.dart.expect @@ -23,7 +23,7 @@ extension $ListRouteExtension on ListRoute { 'ids': ids.map((e) => e.toString()).toList(), if (nullableIds != null) 'nullable-ids': nullableIds?.map((e) => e.toString()).toList(), - if (idsWithDefaultValue != const [0]) + if (!_$iterablesEqual(idsWithDefaultValue, const [0])) 'ids-with-default-value': idsWithDefaultValue.map((e) => e.toString()).toList(), }, @@ -38,3 +38,17 @@ extension $ListRouteExtension on ListRoute { void replace(BuildContext context) => context.replace(location); } + +bool _$iterablesEqual(Iterable? iterable1, Iterable? iterable2) { + if (identical(iterable1, iterable2)) return true; + if (iterable1 == null || iterable2 == null) return false; + final iterator1 = iterable1.iterator; + final iterator2 = iterable2.iterator; + while (true) { + final hasNext1 = iterator1.moveNext(); + final hasNext2 = iterator2.moveNext(); + if (hasNext1 != hasNext2) return false; + if (!hasNext1) return true; + if (iterator1.current != iterator2.current) return false; + } +} diff --git a/packages/go_router_builder/test_inputs/set.dart.expect b/packages/go_router_builder/test_inputs/set.dart.expect index 6a8c150b54c..d33aa7e7c67 100644 --- a/packages/go_router_builder/test_inputs/set.dart.expect +++ b/packages/go_router_builder/test_inputs/set.dart.expect @@ -23,7 +23,7 @@ extension $SetRouteExtension on SetRoute { 'ids': ids.map((e) => e.toString()).toList(), if (nullableIds != null) 'nullable-ids': nullableIds?.map((e) => e.toString()).toList(), - if (idsWithDefaultValue != const {0}) + if (!_$iterablesEqual(idsWithDefaultValue, const {0})) 'ids-with-default-value': idsWithDefaultValue.map((e) => e.toString()).toList(), }, @@ -38,3 +38,17 @@ extension $SetRouteExtension on SetRoute { void replace(BuildContext context) => context.replace(location); } + +bool _$iterablesEqual(Iterable? iterable1, Iterable? iterable2) { + if (identical(iterable1, iterable2)) return true; + if (iterable1 == null || iterable2 == null) return false; + final iterator1 = iterable1.iterator; + final iterator2 = iterable2.iterator; + while (true) { + final hasNext1 = iterator1.moveNext(); + final hasNext2 = iterator2.moveNext(); + if (hasNext1 != hasNext2) return false; + if (!hasNext1) return true; + if (iterator1.current != iterator2.current) return false; + } +}