diff --git a/packages/leancode_lint/README.md b/packages/leancode_lint/README.md index d3441558..dd8f4490 100644 --- a/packages/leancode_lint/README.md +++ b/packages/leancode_lint/README.md @@ -469,6 +469,85 @@ class MyWidget extends StatelessWidget { None +### `missing_cleanup` + +**DO** cleanup of resources that require `dispose()`, `close()` or `cancel()` in StatefulWidget `State` classes. + +Resources such as controllers, stream controllers or focus nodes must be cleanup in the `dispose()` method to prevent memory leaks. + +**BAD:**` + +```dart +class MyWidgetState extends State { + late final TextEditingController controller = TextEditingController(); + + @override + Widget build(BuildContext context) { + return TextField(controller: controller); + } +} +``` + +**BAD:** + +```dart +class MyWidgetState extends State { + @override + Widget build(BuildContext context) { + return TextField(controller: TextEditingController()); + } +} +``` + +**GOOD:** + +```dart +class MyWidgetState extends State { + late final TextEditingController controller; + + @override + void initState() { + super.initState(); + controller = TextEditingController(); + } + + @override + Widget build(BuildContext context) { + return TextField(controller: controller); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } +} +``` + +#### Configuration + +```yaml +custom_lint: + rules: + - missing_cleanup: + ignored_types: + - ignore: AnimationController + from_package: flutter + cleanup_methods: + close: true + dispose: true + cancel: true + +``` + +- `ignored_types` - an optional YamlList - skips dispose checks for specified types. This allows disabling the lint rule for classes where dispose method checks are not needed. + - `ignore` - A required String - name of the instance to ignore + - `from_package` - A required String - name of the source package +- `cleanup_methods` - an optional YamlMap - controls which disposal methods the lint rule should recognize and check for. By default, the rule looks for `dispose`, `close`, and `cancel` methods. You can selectively enable or disable checking for each of these methods. + - `dispose` - A boolean (default: true) - whether to check for `dispose()` method calls + - `close` - A boolean (default: true) - whether to check for `close()` method calls + - `cancel` - A boolean (default: true) - whether to check for `cancel()` method calls + ## Assists Assists are IDE refactorings not related to a particular issue. They can be triggered by placing your cursor over a relevant piece of code and opening the code actions dialog. For instance, in VSCode this is done with ctrl+. or +.. diff --git a/packages/leancode_lint/lib/leancode_lint.dart b/packages/leancode_lint/lib/leancode_lint.dart index fc004f25..d99886e4 100644 --- a/packages/leancode_lint/lib/leancode_lint.dart +++ b/packages/leancode_lint/lib/leancode_lint.dart @@ -8,6 +8,7 @@ import 'package:leancode_lint/lints/avoid_single_child_in_multi_child_widget.dar import 'package:leancode_lint/lints/catch_parameter_names.dart'; import 'package:leancode_lint/lints/constructor_parameters_and_fields_should_have_the_same_order.dart'; import 'package:leancode_lint/lints/hook_widget_does_not_use_hooks.dart'; +import 'package:leancode_lint/lints/missing_cleanup.dart'; import 'package:leancode_lint/lints/prefer_center_over_align.dart'; import 'package:leancode_lint/lints/prefix_widgets_returning_slivers.dart'; import 'package:leancode_lint/lints/start_comments_with_space.dart'; @@ -34,6 +35,7 @@ class _Linter extends PluginBase { const UsePadding(), const UseDedicatedMediaQueryMethods(), const PreferCenterOverAlign(), + MissingCleanup.fromConfigs(configs), ]; @override diff --git a/packages/leancode_lint/lib/lints/missing_cleanup.dart b/packages/leancode_lint/lib/lints/missing_cleanup.dart new file mode 100644 index 00000000..18597b26 --- /dev/null +++ b/packages/leancode_lint/lib/lints/missing_cleanup.dart @@ -0,0 +1,374 @@ +// Replace this once analyzer is updated +// ignore_for_file: deprecated_member_use + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element2.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:leancode_lint/utils.dart'; +import 'package:yaml/yaml.dart'; + +class _IgnoredTypes { + const _IgnoredTypes({required this.name, required this.packageName}); + + final String name; + final String packageName; +} + +class MissingCleanupConfig { + const MissingCleanupConfig({ + this.ignoredTypesCheckers = const [], + this.cleanupMethods = const {}, + }); + + factory MissingCleanupConfig.fromConfig(Map json) { + final ignoredTypes = + (json['ignored_types'] as YamlList?)?.nodes + .map( + (e) => _IgnoredTypes( + name: (e as YamlMap)['ignore'] as String, + packageName: e['from_package'] as String, + ), + ) + .toSet() ?? + const {}; + + final cleanupMethodsMap = (json['cleanup_methods'] as YamlMap?)?.nodes.map( + (key, value) => MapEntry( + (key as YamlScalar).value as String, + (value as YamlScalar).value as bool, + ), + ); + + final cleanupMethods = { + if (cleanupMethodsMap?['dispose'] ?? true) 'dispose', + if (cleanupMethodsMap?['close'] ?? true) 'close', + if (cleanupMethodsMap?['cancel'] ?? true) 'cancel', + }; + + return MissingCleanupConfig( + ignoredTypesCheckers: [ + for (final _IgnoredTypes(:name, :packageName) in ignoredTypes) + if (packageName.startsWith('dart:')) + TypeChecker.fromUrl('$packageName#$name') + else + TypeChecker.fromName(name, packageName: packageName), + ], + cleanupMethods: cleanupMethods, + ); + } + + final List ignoredTypesCheckers; + final Set cleanupMethods; +} + +/// Checks for proper disposal of resources in StatefulWidget classes. +/// Warns when disposable resources are not properly disposed in the dispose() method. +class MissingCleanup extends DartLintRule { + const MissingCleanup({required this.config}) + : super( + code: const LintCode( + name: ruleName, + problemMessage: + 'Resource should be disposed in the dispose() method.', + correctionMessage: 'Add disposal of this resource.', + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + factory MissingCleanup.fromConfigs(CustomLintConfigs configs) { + final config = MissingCleanupConfig.fromConfig( + configs.rules[ruleName]?.json.cast() ?? {}, + ); + + return MissingCleanup(config: config); + } + + final MissingCleanupConfig config; + + static const ruleName = 'missing_cleanup'; + + @override + List getFixes() => [_AddDisposeMethod()]; + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addFieldDeclaration((node) { + final type = _getFieldDeclarationType(node); + + if (type == null || _isIgnoredInstance(type)) { + return; + } + + final classNode = _getContainingClass(node); + if (classNode == null) { + return; + } + + if (_isWidgetClass(classNode) && + !_isFieldUsedByConstructor(classNode, node)) { + reporter.atNode(node, code); + return; + } + + final disposableMethodName = _getDisposableMethodName(type); + if (!_isStateOfWidget(classNode) || disposableMethodName == null) { + return; + } + + final disposeExpressions = _DisposeExpressionsGatherer.gatherForTarget( + node: classNode, + targetName: node.fields.variables.first.name.lexeme, + ); + + if (disposeExpressions.isEmpty) { + reporter.atNode( + node, + code, + data: _AvoidMissingDisposeAnalysisData( + instanceName: node.fields.variables.first.name.lexeme, + classNode: classNode, + disposeMethodName: disposableMethodName, + ), + ); + } + }); + context.registry.addInstanceCreationExpression((node) { + final type = switch (node.staticType) { + final InterfaceType type => type, + _ => null, + }; + + if (type == null || _isIgnoredInstance(type)) { + return; + } + + final classNode = _getContainingClass(node); + + if (classNode == null || + !(_isWidgetClass(classNode) || _isStateOfWidget(classNode)) || + _getDisposableMethodName(type) == null) { + return; + } + + if (_isInReturnWidget(node)) { + reporter.atNode(node, code); + } + }); + } + + bool _isIgnoredInstance(InterfaceType type) => config.ignoredTypesCheckers + .any((checker) => checker.isExactly(type.element)); + + InterfaceType? _getFieldDeclarationType(FieldDeclaration field) { + if (field.fields.type?.type case final InterfaceType type) { + return type; + } + return switch (field.fields.variables.first.initializer?.staticType) { + final InterfaceType type => type, + _ => null, + }; + } + + bool _isFieldUsedByConstructor( + ClassDeclaration classNode, + FieldDeclaration node, + ) { + final constructorDeclaration = _getConstructorDeclaration(classNode); + if (constructorDeclaration == null) { + return false; + } + + return _hasConstructorParameterOrInitializer( + constructorDeclaration, + node.fields.variables.first.name.lexeme, + ); + } + + bool _hasConstructorParameterOrInitializer( + ConstructorDeclaration constructorDeclaration, + String parameterName, + ) { + if (constructorDeclaration.parameters.parameters.any( + (parameter) => parameter.name?.lexeme == parameterName, + )) { + return true; + } + + return constructorDeclaration.initializers + .whereType() + .any((initializer) => initializer.fieldName.name == parameterName); + } + + ConstructorDeclaration? _getConstructorDeclaration( + ClassDeclaration classNode, + ) => classNode.members.whereType().firstOrNull; + + ClassDeclaration? _getContainingClass(AstNode node) { + var classNode = node.parent; + while (classNode != null && classNode is! ClassDeclaration) { + classNode = classNode.parent; + } + return switch (classNode) { + final ClassDeclaration classNode => classNode, + _ => null, + }; + } + + String? _getDisposableMethodName(InterfaceType type) { + return type.methods2 + .firstWhereOrNull( + (method) => config.cleanupMethods.contains(method.name3), + ) + ?.name3 ?? + type.element3.inheritedMembers.entries + .firstWhereOrNull( + (entry) => + config.cleanupMethods.contains(entry.key.name) && + entry.value.baseElement is MethodElement2, + ) + ?.key + .name; + } + + bool _isWidgetType(InterfaceType type) { + const widgetTypeChecker = TypeChecker.fromName( + 'Widget', + packageName: 'flutter', + ); + + return widgetTypeChecker.isExactlyType(type) || + widgetTypeChecker.isSuperTypeOf(type); + } + + bool _isInReturnWidget(InstanceCreationExpression node) { + AstNode? currentNode = node; + var returnsWidget = false; + var isInReturn = false; + + bool isCurrentNodeReturn() => + currentNode is ReturnStatement || currentNode is ExpressionFunctionBody; + + while (currentNode != null && !(returnsWidget && isInReturn)) { + if (!isInReturn && isCurrentNodeReturn()) { + isInReturn = true; + } + if (currentNode case MethodDeclaration( + returnType: NamedType(:final InterfaceType type), + ) when _isWidgetType(type)) { + returnsWidget = true; + } + + currentNode = currentNode.parent; + } + return returnsWidget && isInReturn; + } + + bool _isStateOfWidget(ClassDeclaration classNode) => + switch (classNode.declaredElement) { + final element? => const TypeChecker.fromName( + 'State', + packageName: 'flutter', + ).isAssignableFrom(element), + _ => false, + }; + + bool _isWidgetClass(ClassDeclaration classNode) => + switch (classNode.declaredElement) { + final element? => const TypeChecker.fromName( + 'Widget', + packageName: 'flutter', + ).isAssignableFrom(element), + _ => false, + }; +} + +class _DisposeExpressionsGatherer extends GeneralizingAstVisitor { + _DisposeExpressionsGatherer({required this.targetName}); + + final String targetName; + + final List _disposeExpressions = []; + + static List gatherForTarget({ + required AstNode node, + required String targetName, + }) { + final visitor = _DisposeExpressionsGatherer(targetName: targetName); + node.accept(visitor); + return visitor._disposeExpressions; + } + + @override + void visitExpressionStatement(ExpressionStatement node) { + if (node.expression + case MethodInvocation( + methodName: SimpleIdentifier( + name: 'dispose' || 'close' || 'cancel', + ), + target: SimpleIdentifier(:final name), + ) && + final invocation when name == targetName) { + _disposeExpressions.add(invocation); + } + } +} + +class _AddDisposeMethod extends DartFix { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + if (analysisError.data case _AvoidMissingDisposeAnalysisData( + :final classNode, + :final instanceName, + :final disposeMethodName, + )) { + final disposeMethodNode = _getStateDisposeMethod(classNode); + if (disposeMethodNode?.body case BlockFunctionBody( + block: Block(statements: [final statement, ...]), + )) { + reporter + .createChangeBuilder( + message: + 'Add $instanceName.$disposeMethodName() to the state dispose method', + priority: 80, + ) + .addDartFileEdit((builder) { + builder.addSimpleInsertion( + statement.offset, + '$instanceName.$disposeMethodName();\n ', + ); + }); + } + } + } + + MethodDeclaration? _getStateDisposeMethod(ClassDeclaration classNode) => + classNode.members.whereType().firstWhereOrNull( + (member) => member.name.lexeme == 'dispose', + ); +} + +class _AvoidMissingDisposeAnalysisData { + const _AvoidMissingDisposeAnalysisData({ + required this.instanceName, + required this.classNode, + required this.disposeMethodName, + }); + + final String instanceName; + final ClassDeclaration classNode; + final String disposeMethodName; +} diff --git a/packages/leancode_lint/pubspec.yaml b/packages/leancode_lint/pubspec.yaml index 77aead11..83f4e4a3 100644 --- a/packages/leancode_lint/pubspec.yaml +++ b/packages/leancode_lint/pubspec.yaml @@ -16,3 +16,4 @@ dependencies: analyzer_plugin: ^0.13.0 collection: ^1.19.1 custom_lint_builder: ^0.7.5 + yaml: ^3.1.3 diff --git a/packages/leancode_lint/test/lints_test_app/analysis_options.yaml b/packages/leancode_lint/test/lints_test_app/analysis_options.yaml index ff0317ee..8e3e4d06 100644 --- a/packages/leancode_lint/test/lints_test_app/analysis_options.yaml +++ b/packages/leancode_lint/test/lints_test_app/analysis_options.yaml @@ -18,6 +18,14 @@ custom_lint: application_prefix: Lncd # TODO: remove explicit enable once enabled by default - constructor_parameters_and_fields_should_have_the_same_order: true + - missing_cleanup: + ignored_types: + - ignore: AnimationController + from_package: flutter + cleanup_methods: + close: true + dispose: true + cancel: true analyzer: plugins: diff --git a/packages/leancode_lint/test/lints_test_app/lib/avoid_conditional_hooks_test.dart b/packages/leancode_lint/test/lints_test_app/lib/avoid_conditional_hooks_test.dart index fbd51170..78e3b1f8 100644 --- a/packages/leancode_lint/test/lints_test_app/lib/avoid_conditional_hooks_test.dart +++ b/packages/leancode_lint/test/lints_test_app/lib/avoid_conditional_hooks_test.dart @@ -1,3 +1,5 @@ +// Not related to the test +// ignore_for_file: missing_cleanup import 'dart:math'; import 'package:flutter/material.dart'; diff --git a/packages/leancode_lint/test/lints_test_app/lib/missing_cleanup/missing_cleanup_dispose_bloc_test.dart b/packages/leancode_lint/test/lints_test_app/lib/missing_cleanup/missing_cleanup_dispose_bloc_test.dart new file mode 100644 index 00000000..a8c53631 --- /dev/null +++ b/packages/leancode_lint/test/lints_test_app/lib/missing_cleanup/missing_cleanup_dispose_bloc_test.dart @@ -0,0 +1,42 @@ +// Test Bloc classes for avoid_missing_dispose lint +// ignore_for_file: unused_field, use_design_system_item + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CounterBloc extends Bloc { + CounterBloc() : super(0) { + on((event, emit) => emit(event + state)); + } +} + +class BlocStatefulWidget extends StatefulWidget { + const BlocStatefulWidget({super.key}); + + @override + State createState() => _BlocStatefulWidgetState(); +} + +class _BlocStatefulWidgetState extends State { + late CounterBloc _counterBloc; + // expect_lint: missing_cleanup + final _notDisposedBloc = CounterBloc(); + // expect_lint: missing_cleanup + late final CounterBloc _notDisposedBlocLate; + + @override + void initState() { + super.initState(); + _counterBloc = CounterBloc(); + _notDisposedBlocLate = CounterBloc(); + } + + @override + void dispose() { + _counterBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => const SizedBox(); +} diff --git a/packages/leancode_lint/test/lints_test_app/lib/missing_cleanup/missing_cleanup_test.dart b/packages/leancode_lint/test/lints_test_app/lib/missing_cleanup/missing_cleanup_test.dart new file mode 100644 index 00000000..ca371863 --- /dev/null +++ b/packages/leancode_lint/test/lints_test_app/lib/missing_cleanup/missing_cleanup_test.dart @@ -0,0 +1,129 @@ +// ignored for lint test purpose +// ignore_for_file: unused_field, prefer_final_fields, unused_element, use_design_system_item, use_design_system_item_LftText + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +class MissingDisposeStatefulWidget extends StatefulWidget { + MissingDisposeStatefulWidget({ + super.key, + required this.scrollController, + required FocusNode focusNode, + }) : _focusNode = focusNode, + _streamController = StreamController(); + // expect_lint: missing_cleanup + final controller = TextEditingController(); + final ScrollController scrollController; + final FocusNode _focusNode; + final StreamController _streamController; + + @override + State createState() => + _MissingDisposeStatefulWidgetState(); +} + +class _MissingDisposeStatefulWidgetState + extends State + with TickerProviderStateMixin { + late TextEditingController _textControllerTest; + late ScrollController _scrollController; + late FocusNode _focusNode; + late ValueNotifier _valueNotifier; + late final _focusNode2 = FocusNode(); + final _pageController = PageController(); + final _streamController = StreamController(); + // expect_lint: missing_cleanup + final _streamController2 = StreamController(); + late AnimationController _animationController; + final _timer = Timer(Duration.zero, () {}); + final _timer2 = Timer(Duration.zero, () {}); + + // expect_lint: missing_cleanup + var _notDisposedController = ScrollController(); + // expect_lint: missing_cleanup + late final _notDisposedController2 = FocusNode(); + // expect_lint: missing_cleanup + final _notDisposedController3 = ValueNotifier(0); + // expect_lint: missing_cleanup + late final ScrollController _notDisposedController4; + // expect_lint: missing_cleanup + late final FocusNode _notDisposedController5; + // expect_lint: missing_cleanup + late final ValueNotifier _notDisposedController6; + // expect_lint: missing_cleanup + late final Timer _notDisposedTimer; + + late final AnimationController _ignoredInstance; + + @override + void dispose() { + _timer.cancel(); + _streamController.close(); + _textControllerTest.dispose(); + _scrollController.dispose(); + _valueNotifier.dispose(); + _focusNode.dispose(); + _focusNode2.dispose(); + _pageController.dispose(); + super.dispose(); + } + + void _disposeAnimationController() { + _animationController.dispose(); + } + + void _disposeTimer() { + _timer2.cancel(); + } + + @override + Widget build(BuildContext context) { + final animationController2 = useAnimationController( + duration: const Duration(seconds: 1), + ); + return Column( + children: [ + // expect_lint: missing_cleanup + TextField(controller: TextEditingController()), + _buildTextField(), + AnimatedBuilder( + animation: animationController2, + builder: (context, child) { + return Container( + color: Colors.red, + height: animationController2.value, + ); + }, + ), + ], + ); + } + + TextField _buildTextField() { + // expect_lint: missing_cleanup + return TextField(controller: TextEditingController()); + } +} + +class StatelessMissingDisposeWidget extends StatelessWidget { + StatelessMissingDisposeWidget({ + super.key, + required this.scrollController, + required FocusNode focusNode, + }) : _focusNode = focusNode; + + // expect_lint: missing_cleanup + final controller = TextEditingController(); + final ScrollController scrollController; + final FocusNode _focusNode; + + @override + Widget build(BuildContext context) { + return const SizedBox(); + } +} + +class RegularClass { + final controller = TextEditingController(); +} diff --git a/packages/leancode_lint/test/lints_test_app/pubspec.lock b/packages/leancode_lint/test/lints_test_app/pubspec.lock index 20bd5b57..126e2e34 100644 --- a/packages/leancode_lint/test/lints_test_app/pubspec.lock +++ b/packages/leancode_lint/test/lints_test_app/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f url: "https://pub.dev" source: hosted - version: "80.0.0" + version: "85.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.6.0" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 + sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.13.4" args: dependency: transitive description: name: args - sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" bloc: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" ci: dependency: transitive description: @@ -117,18 +117,18 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1" + sha256: "9656925637516c5cf0f5da018b33df94025af2088fe09c8ae2ca54c53f2d9a84" url: "https://pub.dev" source: hosted - version: "0.7.5" + version: "0.7.6" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228" + sha256: "6cdc8e87e51baaaba9c43e283ed8d28e59a0c4732279df62f66f7b5984655414" url: "https://pub.dev" source: hosted - version: "0.7.5" + version: "0.7.6" custom_lint_core: dependency: transitive description: @@ -141,18 +141,18 @@ packages: dependency: transitive description: name: custom_lint_visitor - sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" + sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2" url: "https://pub.dev" source: hosted - version: "1.0.0+7.3.0" + version: "1.0.0+7.7.0" dart_style: dependency: transitive description: name: dart_style - sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" + sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" file: dependency: transitive description: @@ -202,18 +202,18 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" glob: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" hooks_riverpod: dependency: "direct main" description: @@ -226,10 +226,10 @@ packages: dependency: transitive description: name: hotreloader - sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.0" json_annotation: dependency: transitive description: @@ -244,7 +244,7 @@ packages: path: "../.." relative: true source: path - version: "18.0.0" + version: "17.0.0" logging: dependency: transitive description: @@ -289,10 +289,10 @@ packages: dependency: transitive description: name: package_config - sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" path: dependency: transitive description: @@ -305,18 +305,18 @@ packages: dependency: transitive description: name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.1.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.2.0" pubspec_parse: dependency: transitive description: @@ -374,10 +374,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" state_notifier: dependency: transitive description: @@ -390,42 +390,42 @@ packages: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "0bd04f5bb74fcd6ff0606a888a30e917af9bd52820b178eaa464beb11dca84b6" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.7" typed_data: dependency: transitive description: @@ -446,34 +446,34 @@ packages: dependency: transitive description: name: vector_math - sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.4" vm_service: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.2" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" yaml: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.21.0-13.0.pre.4"