diff --git a/packages/riverpod_lint/CHANGELOG.md b/packages/riverpod_lint/CHANGELOG.md index 03d1a2967..b89703d0e 100644 --- a/packages/riverpod_lint/CHANGELOG.md +++ b/packages/riverpod_lint/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased minor + +- Added assists for converting widgets to HookWidget/HookConsumerWidget (thanks to @K9i-0) + ## 1.1.8 - 2023-04-07 - Disable unsupported_provider_value when a notifier returns "this" diff --git a/packages/riverpod_lint/lib/riverpod_lint.dart b/packages/riverpod_lint/lib/riverpod_lint.dart index 0d9b39b6b..27c1cd92f 100644 --- a/packages/riverpod_lint/lib/riverpod_lint.dart +++ b/packages/riverpod_lint/lib/riverpod_lint.dart @@ -1,7 +1,8 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'src/assists/convert_to_consumer_stateful_widget.dart'; -import 'src/assists/convert_to_consumer_widget.dart'; +import 'src/assists/convert_to_stateful_base_widget.dart'; +import 'src/assists/convert_to_stateless_base_widget.dart'; +import 'src/assists/convert_to_widget_utils.dart'; import 'src/assists/stateful_to_stateless_provider.dart'; import 'src/assists/stateless_to_stateful_provider.dart'; import 'src/assists/wrap_with_consumer.dart'; @@ -56,8 +57,16 @@ class _RiverpodPlugin extends PluginBase { List getAssists() => [ WrapWithConsumer(), WrapWithProviderScope(), - ConvertToConsumerWidget(), - ConvertToConsumerStatefulWidget(), + ...StatelessBaseWidgetType.values.map( + (targetWidget) => ConvertToStatelessBaseWidget( + targetWidget: targetWidget, + ), + ), + ...StatefulBaseWidgetType.values.map( + (targetWidget) => ConvertToStatefulBaseWidget( + targetWidget: targetWidget, + ), + ), // StateProvider to SyncStatefulProvider // convert FutureProvider <> AsyncNotifierProvider diff --git a/packages/riverpod_lint/lib/src/assists/convert_to_consumer_widget.dart b/packages/riverpod_lint/lib/src/assists/convert_to_consumer_widget.dart deleted file mode 100644 index 9f0e0c15e..000000000 --- a/packages/riverpod_lint/lib/src/assists/convert_to_consumer_widget.dart +++ /dev/null @@ -1,202 +0,0 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/source/source_range.dart'; -// ignore: implementation_imports, somehow not exported by analyzer -import 'package:analyzer/src/generated/source.dart' show Source; -import 'package:collection/collection.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; - -import '../object_utils.dart'; -import '../riverpod_custom_lint.dart'; - -/// But the priority above everything else -const convertPriority = 100; - -const _statelessBaseType = TypeChecker.any([ - TypeChecker.fromName('StatelessWidget', packageName: 'flutter'), - TypeChecker.fromName('HookConsumerWidget', packageName: 'hooks_riverpod'), - TypeChecker.fromName('HookWidget', packageName: 'flutter_hooks'), -]); - -const _statefulBaseType = TypeChecker.any([ - TypeChecker.fromName('StatefulWidget', packageName: 'flutter'), - TypeChecker.fromName( - 'ConsumerStatefulWidget', - packageName: 'flutter_riverpod', - ), - TypeChecker.fromName( - 'StatefulHookConsumerWidget', - packageName: 'hooks_riverpod', - ), - TypeChecker.fromName('StatefulHookWidget', packageName: 'flutter_hooks'), -]); - -const _stateType = TypeChecker.fromName('State', packageName: 'flutter'); - -class ConvertToConsumerWidget extends RiverpodAssist { - ConvertToConsumerWidget(); - - @override - void run( - CustomLintResolver resolver, - ChangeReporter reporter, - CustomLintContext context, - SourceRange target, - ) { - context.registry.addExtendsClause((node) { - // Only offer the assist if hovering the extended type - if (!node.superclass.sourceRange.intersects(target)) return; - - final type = node.superclass.type; - if (type == null) return; - - if (_statelessBaseType.isExactlyType(type)) { - _convertStatelessToConsumerWidget(reporter, node); - return; - } - - if (_statefulBaseType.isExactlyType(type)) { - _convertStatefulToConsumerWidget(reporter, node, resolver.source); - return; - } - }); - } - - void _convertStatefulToConsumerWidget( - ChangeReporter reporter, - ExtendsClause node, - Source source, - ) { - final changeBuilder = reporter.createChangeBuilder( - message: 'Convert to ConsumerWidget', - priority: convertPriority, - ); - - changeBuilder.addDartFileEdit((builder) { - // Change the extended base class - builder.addSimpleReplacement( - node.superclass.sourceRange, - 'ConsumerWidget', - ); - - final widgetClass = node.thisOrAncestorOfType(); - if (widgetClass == null) return; - - // Remove createState method - final createStateMethod = widgetClass.members - .whereType() - .firstWhereOrNull((element) => element.name.lexeme == 'createState'); - if (createStateMethod != null) { - builder.addDeletion(createStateMethod.sourceRange); - } - - // Search for the associated State class - final stateClass = _findStateClass(widgetClass); - if (stateClass == null) return; - - // Move the build method to the widget class - - final buildMethod = stateClass.members - .whereType() - .firstWhereOrNull((element) => element.name.lexeme == 'build'); - if (buildMethod == null) return; - - final newBuildMethod = _buildMethodWithRef(buildMethod, source); - if (newBuildMethod == null) return; - builder.addSimpleInsertion( - widgetClass.rightBracket.offset, - newBuildMethod, - ); - - // Delete the state class - builder.addDeletion(stateClass.sourceRange); - }); - } - - String? _buildMethodWithRef( - MethodDeclaration buildMethod, - Source source, - ) { - final parameters = buildMethod.parameters; - if (parameters == null) return null; - - if (parameters.parameters.length == 2) { - // The build method already has a ref parameter, nothing to change - return '${source.contents.data.substring(buildMethod.offset, buildMethod.end)}\n'; - } - - final buffer = StringBuffer(); - final refParamStartOffset = parameters.parameters.firstOrNull?.end ?? - parameters.leftParenthesis.offset + 1; - - buffer - ..write( - source.contents.data.substring(buildMethod.offset, refParamStartOffset), - ) - ..write(', WidgetRef ref') - ..writeln( - source.contents.data.substring(refParamStartOffset, buildMethod.end), - ); - - return buffer.toString(); - } - - ClassDeclaration? _findStateClass(ClassDeclaration widgetClass) { - final widgetType = widgetClass.declaredElement?.thisType; - if (widgetType == null) return null; - - return widgetClass - .thisOrAncestorOfType() - ?.declarations - .whereType() - .where( - // Is the class a state class? - (e) => - e.extendsClause?.superclass.type - .let(_stateType.isAssignableFromType) ?? - false, - ) - .firstWhereOrNull((e) { - final stateWidgetType = e - .extendsClause?.superclass.typeArguments?.arguments.firstOrNull?.type; - if (stateWidgetType == null) return false; - - final checker = TypeChecker.fromStatic(widgetType); - return checker.isExactlyType(stateWidgetType); - }); - } - - void _convertStatelessToConsumerWidget( - ChangeReporter reporter, - ExtendsClause node, - ) { - final changeBuilder = reporter.createChangeBuilder( - message: 'Convert to ConsumerWidget', - priority: convertPriority, - ); - - changeBuilder.addDartFileEdit((builder) { - // Change the extended base class - builder.addSimpleReplacement( - node.superclass.sourceRange, - 'ConsumerWidget', - ); - - // Now update "build" to take a "ref" parameter - final buildMethod = node - .thisOrAncestorOfType() - ?.members - .whereType() - .firstWhereOrNull((element) => element.name.lexeme == 'build'); - - if (buildMethod == null) return; - final buildParams = buildMethod.parameters; - // If there is more than one parameter, the build method already has the "ref" - if (buildParams == null || buildParams.parameters.length != 1) return; - - builder.addSimpleInsertion( - buildParams.parameters.last.end, - ', WidgetRef ref', - ); - }); - } -} diff --git a/packages/riverpod_lint/lib/src/assists/convert_to_consumer_stateful_widget.dart b/packages/riverpod_lint/lib/src/assists/convert_to_stateful_base_widget.dart similarity index 53% rename from packages/riverpod_lint/lib/src/assists/convert_to_consumer_stateful_widget.dart rename to packages/riverpod_lint/lib/src/assists/convert_to_stateful_base_widget.dart index 07f7bd015..e4f60c98c 100644 --- a/packages/riverpod_lint/lib/src/assists/convert_to_consumer_stateful_widget.dart +++ b/packages/riverpod_lint/lib/src/assists/convert_to_stateful_base_widget.dart @@ -5,32 +5,22 @@ import 'package:analyzer/src/generated/source.dart' show Source; import 'package:collection/collection.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; -import '../object_utils.dart'; import '../riverpod_custom_lint.dart'; -import 'convert_to_consumer_widget.dart'; - -const statefulConvertPriority = convertPriority - 1; - -const _statelessBaseType = TypeChecker.any([ - TypeChecker.fromName('StatelessWidget', packageName: 'flutter'), - TypeChecker.fromName('ConsumerWidget', packageName: 'flutter_riverpod'), - TypeChecker.fromName('HookConsumerWidget', packageName: 'hooks_riverpod'), - TypeChecker.fromName('HookWidget', packageName: 'flutter_hooks'), -]); - -const _statefulBaseType = TypeChecker.any([ - TypeChecker.fromName('StatefulWidget', packageName: 'flutter'), - TypeChecker.fromName( - 'StatefulHookConsumerWidget', - packageName: 'hooks_riverpod', - ), - TypeChecker.fromName('StatefulHookWidget', packageName: 'flutter_hooks'), -]); - -const _stateType = TypeChecker.fromName('State', packageName: 'flutter'); - -class ConvertToConsumerStatefulWidget extends RiverpodAssist { - ConvertToConsumerStatefulWidget(); +import 'convert_to_widget_utils.dart'; + +class ConvertToStatefulBaseWidget extends RiverpodAssist { + ConvertToStatefulBaseWidget({ + required this.targetWidget, + }); + final StatefulBaseWidgetType targetWidget; + late final statelessBaseType = getStatelessBaseType( + exclude: targetWidget == StatefulBaseWidgetType.statefulWidget + ? StatelessBaseWidgetType.statelessWidget + : null, + ); + late final statefulBaseType = getStatefulBaseType( + exclude: targetWidget, + ); @override void run( @@ -39,6 +29,12 @@ class ConvertToConsumerStatefulWidget extends RiverpodAssist { CustomLintContext context, SourceRange target, ) { + if (targetWidget.requiredPackage != null && + !context.pubspec.dependencies.keys + .contains(targetWidget.requiredPackage)) { + return; + } + context.registry.addExtendsClause((node) { // Only offer the assist if hovering the extended type if (!node.superclass.sourceRange.intersects(target)) return; @@ -46,141 +42,145 @@ class ConvertToConsumerStatefulWidget extends RiverpodAssist { final type = node.superclass.type; if (type == null) return; - if (_statelessBaseType.isExactlyType(type)) { - _convertStatelessToConsumerStatefulWidget(reporter, node); + if (statelessBaseType.isExactlyType(type)) { + _convertStatelessToStatefulWidget(reporter, node); return; } - if (_statefulBaseType.isExactlyType(type)) { - _convertStatefulToConsumerStatefulWidget( + if (statefulBaseType.isExactlyType(type)) { + final isExactlyStatefulWidget = StatefulBaseWidgetType + .statefulWidget.typeChecker + .isExactlyType(type); + + _convertStatefulToStatefulWidget( reporter, node, resolver.source, + // This adjustment assumes that the priority of the standard "Convert to StatelessWidget" is 30. + priorityAdjustment: isExactlyStatefulWidget ? -4 : 0, ); return; } }); } - void _convertStatefulToConsumerStatefulWidget( + void _convertStatelessToStatefulWidget( ChangeReporter reporter, ExtendsClause node, - Source source, ) { final changeBuilder = reporter.createChangeBuilder( - message: 'Convert to ConsumerStatefulWidget', - priority: statefulConvertPriority, + message: 'Convert to ${targetWidget.widgetName}', + priority: targetWidget.priority, ); changeBuilder.addDartFileEdit((builder) { // Change the extended base class builder.addSimpleReplacement( node.superclass.sourceRange, - 'ConsumerStatefulWidget', + targetWidget.widgetName, ); final widgetClass = node.thisOrAncestorOfType(); if (widgetClass == null) return; - final stateClass = _findStateClass(widgetClass); - if (stateClass == null) return; - - final createStateMethod = widgetClass.members + final buildMethod = node + .thisOrAncestorOfType() + ?.members .whereType() - .firstWhereOrNull((element) => element.name.lexeme == 'createState'); - if (createStateMethod != null) { - final returnTypeString = createStateMethod.returnType?.toSource() ?? ''; - if (returnTypeString != stateClass.name.lexeme) { - // Replace State with ConsumerState - builder.addSimpleReplacement( - createStateMethod.returnType!.sourceRange, - 'ConsumerState<${widgetClass.name}>', - ); - } + .firstWhereOrNull((element) => element.name.lexeme == 'build'); + if (buildMethod == null) return; + + final createdStateClassName = '_${widgetClass.name.lexeme.public}State'; + final String baseStateName; + switch (targetWidget) { + case StatefulBaseWidgetType.consumerStatefulWidget: + case StatefulBaseWidgetType.statefulHookConsumerWidget: + baseStateName = 'ConsumerState'; + break; + case StatefulBaseWidgetType.statefulHookWidget: + case StatefulBaseWidgetType.statefulWidget: + baseStateName = 'State'; + break; } - final stateExtends = stateClass.extendsClause; - if (stateExtends != null) { - // Replace State with ConsumerState - builder.addSimpleReplacement( - stateExtends.superclass.sourceRange, - 'ConsumerState<${widgetClass.name}>', + // Split the class into two classes right before the build method + builder.addSimpleInsertion(buildMethod.offset, ''' +@override + $baseStateName<${widgetClass.name.lexeme}> createState() => $createdStateClassName(); +} + +class $createdStateClassName extends $baseStateName<${widgetClass.name}> { +'''); + + final buildParams = buildMethod.parameters; + // If the build method has a ref, remove it + if (buildParams != null && buildParams.parameters.length == 2) { + builder.addDeletion( + sourceRangeFrom( + start: buildParams.parameters.first.end, + end: buildParams.rightParenthesis.offset, + ), ); } }); } - ClassDeclaration? _findStateClass(ClassDeclaration widgetClass) { - final widgetType = widgetClass.declaredElement?.thisType; - if (widgetType == null) return null; - - return widgetClass - .thisOrAncestorOfType() - ?.declarations - .whereType() - .where( - // Is the class a state class? - (e) => - e.extendsClause?.superclass.type - .let(_stateType.isAssignableFromType) ?? - false, - ) - .firstWhereOrNull((e) { - final stateWidgetType = e - .extendsClause?.superclass.typeArguments?.arguments.firstOrNull?.type; - if (stateWidgetType == null) return false; - - final checker = TypeChecker.fromStatic(widgetType); - return checker.isExactlyType(stateWidgetType); - }); - } - - void _convertStatelessToConsumerStatefulWidget( + void _convertStatefulToStatefulWidget( ChangeReporter reporter, ExtendsClause node, - ) { + Source source, { + required int priorityAdjustment, + }) { final changeBuilder = reporter.createChangeBuilder( - message: 'Convert to ConsumerStatefulWidget', - priority: statefulConvertPriority, + message: 'Convert to ${targetWidget.widgetName}', + priority: targetWidget.priority + priorityAdjustment, ); changeBuilder.addDartFileEdit((builder) { // Change the extended base class builder.addSimpleReplacement( node.superclass.sourceRange, - 'ConsumerStatefulWidget', + targetWidget.widgetName, ); final widgetClass = node.thisOrAncestorOfType(); if (widgetClass == null) return; - // Now update "build" to take a "ref" parameter - final buildMethod = node - .thisOrAncestorOfType() - ?.members - .whereType() - .firstWhereOrNull((element) => element.name.lexeme == 'build'); - if (buildMethod == null) return; - - final createdStateClassName = '_${widgetClass.name.lexeme.public}State'; + final stateClass = findStateClass(widgetClass); + if (stateClass == null) return; - // Split the class into two classes right before the build method - builder.addSimpleInsertion(buildMethod.offset, ''' -@override - ConsumerState<${widgetClass.name.lexeme}> createState() => $createdStateClassName(); -} + final String baseStateName; + switch (targetWidget) { + case StatefulBaseWidgetType.consumerStatefulWidget: + case StatefulBaseWidgetType.statefulHookConsumerWidget: + baseStateName = 'ConsumerState'; + break; + case StatefulBaseWidgetType.statefulHookWidget: + case StatefulBaseWidgetType.statefulWidget: + baseStateName = 'State'; + break; + } -class $createdStateClassName extends ConsumerState<${widgetClass.name}> { -'''); + final createStateMethod = widgetClass.members + .whereType() + .firstWhereOrNull((element) => element.name.lexeme == 'createState'); + if (createStateMethod != null) { + final returnTypeString = createStateMethod.returnType?.toSource() ?? ''; + if (returnTypeString != stateClass.name.lexeme) { + // Replace State + builder.addSimpleReplacement( + createStateMethod.returnType!.sourceRange, + '$baseStateName<${widgetClass.name}>', + ); + } + } - final buildParams = buildMethod.parameters; - // If the build method has a ref, remove it - if (buildParams != null && buildParams.parameters.length == 2) { - builder.addDeletion( - sourceRangeFrom( - start: buildParams.parameters.first.end, - end: buildParams.rightParenthesis.offset, - ), + final stateExtends = stateClass.extendsClause; + if (stateExtends != null) { + // Replace State + builder.addSimpleReplacement( + stateExtends.superclass.sourceRange, + '$baseStateName<${widgetClass.name}>', ); } }); diff --git a/packages/riverpod_lint/lib/src/assists/convert_to_stateless_base_widget.dart b/packages/riverpod_lint/lib/src/assists/convert_to_stateless_base_widget.dart new file mode 100644 index 000000000..fc2c30174 --- /dev/null +++ b/packages/riverpod_lint/lib/src/assists/convert_to_stateless_base_widget.dart @@ -0,0 +1,235 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/source/source_range.dart'; +// ignore: implementation_imports, somehow not exported by analyzer +import 'package:analyzer/src/generated/source.dart' show Source; +import 'package:collection/collection.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +import '../riverpod_custom_lint.dart'; +import 'convert_to_widget_utils.dart'; + +class ConvertToStatelessBaseWidget extends RiverpodAssist { + ConvertToStatelessBaseWidget({ + required this.targetWidget, + }); + final StatelessBaseWidgetType targetWidget; + late final statelessBaseType = getStatelessBaseType( + exclude: targetWidget, + ); + late final statefulBaseType = getStatefulBaseType( + exclude: targetWidget == StatelessBaseWidgetType.statelessWidget + ? StatefulBaseWidgetType.statefulWidget + : null, + ); + + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + SourceRange target, + ) { + if (targetWidget.requiredPackage != null && + !context.pubspec.dependencies.keys + .contains(targetWidget.requiredPackage)) { + return; + } + + context.registry.addExtendsClause((node) { + // Only offer the assist if hovering the extended type + if (!node.superclass.sourceRange.intersects(target)) return; + + final type = node.superclass.type; + if (type == null) return; + + if (statelessBaseType.isExactlyType(type)) { + _convertStatelessToStatelessWidget( + reporter, + node, + ); + return; + } + + if (statefulBaseType.isExactlyType(type)) { + final isExactlyStatefulWidget = StatefulBaseWidgetType + .statefulWidget.typeChecker + .isExactlyType(type); + + _convertStatefulToStatelessWidget( + reporter, + node, + resolver.source, + // This adjustment assumes that the priority of the standard "Convert to StatelessWidget" is 30. + priorityAdjustment: isExactlyStatefulWidget ? -4 : 0, + ); + return; + } + }); + } + + void _convertStatelessToStatelessWidget( + ChangeReporter reporter, + ExtendsClause node, + ) { + final changeBuilder = reporter.createChangeBuilder( + message: 'Convert to ${targetWidget.widgetName}', + priority: targetWidget.priority, + ); + + changeBuilder.addDartFileEdit((builder) { + // Change the extended base class + builder.addSimpleReplacement( + node.superclass.sourceRange, + targetWidget.widgetName, + ); + + final buildMethod = node + .thisOrAncestorOfType() + ?.members + .whereType() + .firstWhereOrNull((element) => element.name.lexeme == 'build'); + + if (buildMethod == null) return; + final buildParams = buildMethod.parameters; + + if (buildParams == null) return; + + switch (targetWidget) { + case StatelessBaseWidgetType.consumerWidget: + case StatelessBaseWidgetType.hookConsumerWidget: + // If the build method has not a ref, add it + if (buildParams.parameters.length == 1) { + builder.addSimpleInsertion( + buildParams.parameters.last.end, + ', WidgetRef ref', + ); + } + break; + case StatelessBaseWidgetType.hookWidget: + case StatelessBaseWidgetType.statelessWidget: + // If the build method has a ref, remove it + if (buildParams.parameters.length == 2) { + builder.addDeletion( + sourceRangeFrom( + start: buildParams.parameters.first.end, + end: buildParams.rightParenthesis.offset, + ), + ); + } + break; + } + }); + } + + void _convertStatefulToStatelessWidget( + ChangeReporter reporter, + ExtendsClause node, + Source source, { + required int priorityAdjustment, + }) { + final changeBuilder = reporter.createChangeBuilder( + message: 'Convert to ${targetWidget.widgetName}', + priority: targetWidget.priority + priorityAdjustment, + ); + + changeBuilder.addDartFileEdit((builder) { + // Change the extended base class + builder.addSimpleReplacement( + node.superclass.sourceRange, + targetWidget.widgetName, + ); + + final widgetClass = node.thisOrAncestorOfType(); + if (widgetClass == null) return; + + // Remove createState method + final createStateMethod = widgetClass.members + .whereType() + .firstWhereOrNull((element) => element.name.lexeme == 'createState'); + if (createStateMethod != null) { + builder.addDeletion(createStateMethod.sourceRange); + } + + // Search for the associated State class + final stateClass = findStateClass(widgetClass); + if (stateClass == null) return; + + // Move the build method to the widget class + final buildMethod = stateClass.members + .whereType() + .firstWhereOrNull((element) => element.name.lexeme == 'build'); + if (buildMethod == null) return; + + final String? newBuildMethod; + switch (targetWidget) { + case StatelessBaseWidgetType.consumerWidget: + case StatelessBaseWidgetType.hookConsumerWidget: + newBuildMethod = _buildMethodWithRef(buildMethod, source); + break; + case StatelessBaseWidgetType.hookWidget: + case StatelessBaseWidgetType.statelessWidget: + newBuildMethod = _buildMethodWithoutRef(buildMethod, source); + break; + } + + if (newBuildMethod == null) return; + builder.addSimpleInsertion( + widgetClass.rightBracket.offset, + newBuildMethod, + ); + + // Delete the state class + builder.addDeletion(stateClass.sourceRange); + }); + } + + String? _buildMethodWithRef(MethodDeclaration buildMethod, Source source) { + final parameters = buildMethod.parameters; + if (parameters == null) return null; + + if (parameters.parameters.length == 2) { + // The build method already has a ref parameter, nothing to change + return '${source.contents.data.substring(buildMethod.offset, buildMethod.end)}\n'; + } + + final buffer = StringBuffer(); + final refParamStartOffset = parameters.parameters.firstOrNull?.end ?? + parameters.leftParenthesis.offset + 1; + + buffer + ..write( + source.contents.data.substring(buildMethod.offset, refParamStartOffset), + ) + ..write(', WidgetRef ref') + ..writeln( + source.contents.data.substring(refParamStartOffset, buildMethod.end), + ); + + return buffer.toString(); + } + + String? _buildMethodWithoutRef(MethodDeclaration buildMethod, Source source) { + final parameters = buildMethod.parameters; + if (parameters == null) return null; + + if (parameters.parameters.length == 1) { + // The build method already has not a ref parameter, nothing to change + return '${source.contents.data.substring(buildMethod.offset, buildMethod.end)}\n'; + } + + final buffer = StringBuffer(); + final contextEndOffset = parameters.parameters.firstOrNull?.end ?? + parameters.leftParenthesis.offset + 1; + final refParamStartOffset = parameters.parameters.last.offset; + + buffer + ..write( + source.contents.data.substring(buildMethod.offset, contextEndOffset), + ) + ..writeln( + source.contents.data.substring(refParamStartOffset, buildMethod.end), + ); + + return buffer.toString(); + } +} diff --git a/packages/riverpod_lint/lib/src/assists/convert_to_widget_utils.dart b/packages/riverpod_lint/lib/src/assists/convert_to_widget_utils.dart new file mode 100644 index 000000000..ed5c43299 --- /dev/null +++ b/packages/riverpod_lint/lib/src/assists/convert_to_widget_utils.dart @@ -0,0 +1,151 @@ +import 'package:analyzer/dart/ast/ast.dart'; +// ignore: implementation_imports, somehow not exported by analyzer +import 'package:collection/collection.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +import '../object_utils.dart'; + +enum StatelessBaseWidgetType { + hookConsumerWidget( + widgetName: 'HookConsumerWidget', + priority: 37, + typeChecker: TypeChecker.fromName( + 'HookConsumerWidget', + packageName: 'hooks_riverpod', + ), + requiredPackage: 'hooks_riverpod', + ), + hookWidget( + widgetName: 'HookWidget', + priority: 36, + typeChecker: TypeChecker.fromName( + 'HookWidget', + packageName: 'flutter_hooks', + ), + requiredPackage: 'flutter_hooks', + ), + consumerWidget( + widgetName: 'ConsumerWidget', + priority: 35, + typeChecker: TypeChecker.fromName( + 'ConsumerWidget', + packageName: 'flutter_riverpod', + ), + ), + statelessWidget( + widgetName: 'StatelessWidget', + priority: 34, + typeChecker: TypeChecker.fromName( + 'StatelessWidget', + packageName: 'flutter', + ), + ), + ; + + const StatelessBaseWidgetType({ + required this.widgetName, + required this.priority, + required this.typeChecker, + this.requiredPackage, + }); + final String widgetName; + final int priority; + final TypeChecker typeChecker; + final String? requiredPackage; +} + +enum StatefulBaseWidgetType { + statefulHookConsumerWidget( + widgetName: 'StatefulHookConsumerWidget', + priority: 33, + typeChecker: TypeChecker.fromName( + 'StatefulHookConsumerWidget', + packageName: 'hooks_riverpod', + ), + requiredPackage: 'hooks_riverpod', + ), + statefulHookWidget( + widgetName: 'StatefulHookWidget', + priority: 32, + typeChecker: TypeChecker.fromName( + 'StatefulHookWidget', + packageName: 'flutter_hooks', + ), + requiredPackage: 'flutter_hooks', + ), + consumerStatefulWidget( + widgetName: 'ConsumerStatefulWidget', + priority: 31, + typeChecker: TypeChecker.fromName( + 'ConsumerStatefulWidget', + packageName: 'flutter_riverpod', + ), + ), + statefulWidget( + widgetName: 'StatefulWidget', + priority: 30, + typeChecker: TypeChecker.fromName( + 'StatefulWidget', + packageName: 'flutter', + ), + ), + ; + + const StatefulBaseWidgetType({ + required this.widgetName, + required this.priority, + required this.typeChecker, + this.requiredPackage, + }); + final String widgetName; + final int priority; + final TypeChecker typeChecker; + final String? requiredPackage; +} + +TypeChecker getStatelessBaseType({ + required StatelessBaseWidgetType? exclude, +}) { + return TypeChecker.any( + StatelessBaseWidgetType.values + .where((e) => e != exclude) + .map((e) => e.typeChecker), + ); +} + +TypeChecker getStatefulBaseType({ + required StatefulBaseWidgetType? exclude, +}) { + return TypeChecker.any( + StatefulBaseWidgetType.values + .where((e) => e != exclude) + .map((e) => e.typeChecker), + ); +} + +const _stateType = TypeChecker.fromName('State', packageName: 'flutter'); + +ClassDeclaration? findStateClass(ClassDeclaration widgetClass) { + final widgetType = widgetClass.declaredElement?.thisType; + if (widgetType == null) return null; + + return widgetClass + .thisOrAncestorOfType() + ?.declarations + .whereType() + .where( + // Is the class a state class? + (e) => + e.extendsClause?.superclass.type + .let(_stateType.isAssignableFromType) ?? + false, + ) + .firstWhereOrNull((e) { + final stateWidgetType = + e.extendsClause?.superclass.typeArguments?.arguments.firstOrNull?.type; + if (stateWidgetType == null) return false; + + final checker = TypeChecker.fromStatic(widgetType); + return checker.isExactlyType(stateWidgetType); + }); +} diff --git a/packages/riverpod_lint/lib/src/assists/stateful_to_stateless_provider.dart b/packages/riverpod_lint/lib/src/assists/stateful_to_stateless_provider.dart index be59cbb3d..4edace94e 100644 --- a/packages/riverpod_lint/lib/src/assists/stateful_to_stateless_provider.dart +++ b/packages/riverpod_lint/lib/src/assists/stateful_to_stateless_provider.dart @@ -2,7 +2,9 @@ import 'package:analyzer/source/source_range.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; import '../riverpod_custom_lint.dart'; -import 'convert_to_consumer_widget.dart'; + +/// But the priority above everything else +const convertPriority = 100; class StatefulToStatelessProvider extends RiverpodAssist { StatefulToStatelessProvider(); diff --git a/packages/riverpod_lint/lib/src/assists/stateless_to_stateful_provider.dart b/packages/riverpod_lint/lib/src/assists/stateless_to_stateful_provider.dart index d0dd2dbe4..e6b168ba2 100644 --- a/packages/riverpod_lint/lib/src/assists/stateless_to_stateful_provider.dart +++ b/packages/riverpod_lint/lib/src/assists/stateless_to_stateful_provider.dart @@ -2,7 +2,7 @@ import 'package:analyzer/source/source_range.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; import '../riverpod_custom_lint.dart'; -import 'convert_to_consumer_widget.dart'; +import 'stateful_to_stateless_provider.dart'; class StatelessToStatefulProvider extends RiverpodAssist { StatelessToStatefulProvider(); diff --git a/packages/riverpod_lint_flutter_test/test/assists/convert_to_consumer_stateful_widget.json b/packages/riverpod_lint_flutter_test/test/assists/convert_to_consumer_stateful_widget.json index b3960181f..e019d34fb 100644 --- a/packages/riverpod_lint_flutter_test/test/assists/convert_to_consumer_stateful_widget.json +++ b/packages/riverpod_lint_flutter_test/test/assists/convert_to_consumer_stateful_widget.json @@ -1 +1 @@ -[{"priority":99,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":221,"length":0,"replacement":"@override\n ConsumerState createState() => _StatelessState();\n}\n\nclass _StatelessState extends ConsumerState {\n"},{"offset":168,"length":15,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":99,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":404,"length":0,"replacement":"@override\n ConsumerState createState() => _StatelessWithCommaState();\n}\n\nclass _StatelessWithCommaState extends ConsumerState {\n"},{"offset":342,"length":15,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":99,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":563,"length":0,"replacement":"@override\n ConsumerState createState() => _HookState();\n}\n\nclass _HookState extends ConsumerState {\n"},{"offset":520,"length":10,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":99,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":787,"length":23,"replacement":""},{"offset":737,"length":0,"replacement":"@override\n ConsumerState createState() => _HookConsumerState();\n}\n\nclass _HookConsumerState extends ConsumerState {\n"},{"offset":678,"length":18,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":99,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":1022,"length":15,"replacement":"ConsumerState"},{"offset":939,"length":15,"replacement":"ConsumerState"},{"offset":876,"length":14,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":99,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":1389,"length":26,"replacement":"ConsumerState"},{"offset":1203,"length":14,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":99,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":1708,"length":19,"replacement":"ConsumerState"},{"offset":1615,"length":19,"replacement":"ConsumerState"},{"offset":1544,"length":18,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":99,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":2424,"length":35,"replacement":"ConsumerState"},{"offset":2291,"length":35,"replacement":"ConsumerState"},{"offset":2204,"length":26,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":99,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":2676,"length":23,"replacement":""},{"offset":2626,"length":0,"replacement":"@override\n ConsumerState createState() => _ConsumerState();\n}\n\nclass _ConsumerState extends ConsumerState {\n"},{"offset":2575,"length":14,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}}] \ No newline at end of file +[{"priority":31,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":221,"length":0,"replacement":"@override\n ConsumerState createState() => _StatelessState();\n}\n\nclass _StatelessState extends ConsumerState {\n"},{"offset":168,"length":15,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":31,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":404,"length":0,"replacement":"@override\n ConsumerState createState() => _StatelessWithCommaState();\n}\n\nclass _StatelessWithCommaState extends ConsumerState {\n"},{"offset":342,"length":15,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":31,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":563,"length":0,"replacement":"@override\n ConsumerState createState() => _HookState();\n}\n\nclass _HookState extends ConsumerState {\n"},{"offset":520,"length":10,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":31,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":787,"length":23,"replacement":""},{"offset":737,"length":0,"replacement":"@override\n ConsumerState createState() => _HookConsumerState();\n}\n\nclass _HookConsumerState extends ConsumerState {\n"},{"offset":678,"length":18,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":27,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":1022,"length":15,"replacement":"ConsumerState"},{"offset":939,"length":15,"replacement":"ConsumerState"},{"offset":876,"length":14,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":27,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":1389,"length":26,"replacement":"ConsumerState"},{"offset":1203,"length":14,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":31,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":1708,"length":19,"replacement":"ConsumerState"},{"offset":1615,"length":19,"replacement":"ConsumerState"},{"offset":1544,"length":18,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":31,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":2424,"length":35,"replacement":"ConsumerState"},{"offset":2291,"length":35,"replacement":"ConsumerState"},{"offset":2204,"length":26,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":31,"change":{"message":"Convert to ConsumerStatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":2676,"length":23,"replacement":""},{"offset":2626,"length":0,"replacement":"@override\n ConsumerState createState() => _ConsumerState();\n}\n\nclass _ConsumerState extends ConsumerState {\n"},{"offset":2575,"length":14,"replacement":"ConsumerStatefulWidget"}]}],"linkedEditGroups":[]}}] \ No newline at end of file diff --git a/packages/riverpod_lint_flutter_test/test/assists/convert_to_consumer_widget.json b/packages/riverpod_lint_flutter_test/test/assists/convert_to_consumer_widget.json index 851b4e43a..8e975d16f 100644 --- a/packages/riverpod_lint_flutter_test/test/assists/convert_to_consumer_widget.json +++ b/packages/riverpod_lint_flutter_test/test/assists/convert_to_consumer_widget.json @@ -1 +1 @@ -[{"priority":100,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":266,"length":0,"replacement":", WidgetRef ref"},{"offset":168,"length":15,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":100,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":454,"length":0,"replacement":", WidgetRef ref"},{"offset":342,"length":15,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":100,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":608,"length":0,"replacement":", WidgetRef ref"},{"offset":520,"length":10,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":100,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":678,"length":18,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":100,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":993,"length":174,"replacement":""},{"offset":990,"length":0,"replacement":"/// Hello world\n @override\n Widget build(BuildContext context, WidgetRef ref) {\n // Some comments\n return const Placeholder();\n }\n"},{"offset":927,"length":62,"replacement":""},{"offset":876,"length":14,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":100,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":1350,"length":165,"replacement":""},{"offset":1347,"length":0,"replacement":"@override\n Widget build(\n BuildContext context, WidgetRef ref,\n ) {\n return const Placeholder();\n }\n"},{"offset":1265,"length":81,"replacement":""},{"offset":1203,"length":14,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":100,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":1676,"length":142,"replacement":""},{"offset":1673,"length":0,"replacement":"@override\n Widget build(BuildContext context, WidgetRef ref) {\n return const Placeholder();\n }\n"},{"offset":1603,"length":69,"replacement":""},{"offset":1544,"length":18,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":100,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":2008,"length":159,"replacement":""},{"offset":2005,"length":0,"replacement":"@override\n Widget build(BuildContext context, WidgetRef ref) {\n return const Placeholder();\n }\n"},{"offset":1918,"length":86,"replacement":""},{"offset":1851,"length":22,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":100,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":2383,"length":167,"replacement":""},{"offset":2380,"length":0,"replacement":"@override\n Widget build(BuildContext context, WidgetRef ref) {\n return const Placeholder();\n }\n"},{"offset":2279,"length":100,"replacement":""},{"offset":2204,"length":26,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}}] \ No newline at end of file +[{"priority":35,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":266,"length":0,"replacement":", WidgetRef ref"},{"offset":168,"length":15,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":35,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":454,"length":0,"replacement":", WidgetRef ref"},{"offset":342,"length":15,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":35,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":608,"length":0,"replacement":", WidgetRef ref"},{"offset":520,"length":10,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":35,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":678,"length":18,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":31,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":993,"length":174,"replacement":""},{"offset":990,"length":0,"replacement":"/// Hello world\n @override\n Widget build(BuildContext context, WidgetRef ref) {\n // Some comments\n return const Placeholder();\n }\n"},{"offset":927,"length":62,"replacement":""},{"offset":876,"length":14,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":31,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":1350,"length":165,"replacement":""},{"offset":1347,"length":0,"replacement":"@override\n Widget build(\n BuildContext context, WidgetRef ref,\n ) {\n return const Placeholder();\n }\n"},{"offset":1265,"length":81,"replacement":""},{"offset":1203,"length":14,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":35,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":1676,"length":142,"replacement":""},{"offset":1673,"length":0,"replacement":"@override\n Widget build(BuildContext context, WidgetRef ref) {\n return const Placeholder();\n }\n"},{"offset":1603,"length":69,"replacement":""},{"offset":1544,"length":18,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":35,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":2008,"length":159,"replacement":""},{"offset":2005,"length":0,"replacement":"@override\n Widget build(BuildContext context, WidgetRef ref) {\n return const Placeholder();\n }\n"},{"offset":1918,"length":86,"replacement":""},{"offset":1851,"length":22,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":35,"change":{"message":"Convert to ConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":2383,"length":167,"replacement":""},{"offset":2380,"length":0,"replacement":"@override\n Widget build(BuildContext context, WidgetRef ref) {\n return const Placeholder();\n }\n"},{"offset":2279,"length":100,"replacement":""},{"offset":2204,"length":26,"replacement":"ConsumerWidget"}]}],"linkedEditGroups":[]}}] \ No newline at end of file diff --git a/packages/riverpod_lint_flutter_test/test/assists/convert_to_consumer_widget_test.dart b/packages/riverpod_lint_flutter_test/test/assists/convert_to_consumer_widget_test.dart deleted file mode 100644 index 96610e22c..000000000 --- a/packages/riverpod_lint_flutter_test/test/assists/convert_to_consumer_widget_test.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'dart:io'; - -import 'package:test/test.dart'; -import 'package:riverpod_lint/src/assists/convert_to_consumer_widget.dart'; -import 'package:riverpod_lint/src/assists/convert_to_consumer_stateful_widget.dart'; -import 'package:analyzer/source/source_range.dart'; -import 'package:analyzer/dart/analysis/results.dart'; -import 'package:analyzer/dart/analysis/utilities.dart'; - -import '../golden.dart'; - -void main() { - testGolden( - 'Convert widgets to consumerwidgets', - 'assists/convert_to_consumer_widget.json', - () async { - final assist = ConvertToConsumerWidget(); - final file = File( - 'test/assists/convert_to_consumer_widget.dart', - ).absolute; - - final result = await resolveFile2(path: file.path); - result as ResolvedUnitResult; - - var changes = [ - // Stateless - ...await assist.testRun(result, const SourceRange(163, 0)), - ...await assist.testRun(result, const SourceRange(174, 0)), - ...await assist.testRun(result, const SourceRange(185, 0)), - - // StatelessWithComma - ...await assist.testRun(result, const SourceRange(350, 0)), - - // Hook - ...await assist.testRun(result, const SourceRange(524, 0)), - - // HookConsumer - ...await assist.testRun(result, const SourceRange(690, 0)), - - // Stateful - ...await assist.testRun(result, const SourceRange(884, 0)), - - // ExplicitCreateState - ...await assist.testRun(result, const SourceRange(1208, 0)), - - // HookStateful - ...await assist.testRun(result, const SourceRange(1553, 0)), - - // ConsumerStateful - ...await assist.testRun(result, const SourceRange(1863, 0)), - - // HookConsumerStateful - ...await assist.testRun(result, const SourceRange(2214, 0)), - - // ConsumerWidget - ...await assist.testRun(result, const SourceRange(2582, 0)), - ]; - - expect(changes, hasLength(9)); - - return changes; - }, - ); - - testGolden( - 'Convert widgets to stateful consumers', - 'assists/convert_to_consumer_stateful_widget.json', - () async { - final assist = ConvertToConsumerStatefulWidget(); - final file = File( - 'test/assists/convert_to_consumer_widget.dart', - ).absolute; - - final result = await resolveFile2(path: file.path); - result as ResolvedUnitResult; - - final changes = [ - // Stateless - ...await assist.testRun(result, const SourceRange(163, 0)), - ...await assist.testRun(result, const SourceRange(174, 0)), - ...await assist.testRun(result, const SourceRange(185, 0)), - - // StatelessWithComma - ...await assist.testRun(result, const SourceRange(350, 0)), - - // Hook - ...await assist.testRun(result, const SourceRange(524, 0)), - - // HookConsumer - ...await assist.testRun(result, const SourceRange(690, 0)), - - // Stateful - ...await assist.testRun(result, const SourceRange(884, 0)), - - // ExplicitCreateState - ...await assist.testRun(result, const SourceRange(1208, 0)), - - // HookStateful - ...await assist.testRun(result, const SourceRange(1553, 0)), - - // ConsumerStateful - ...await assist.testRun(result, const SourceRange(1863, 0)), - - // HookConsumerStateful - ...await assist.testRun(result, const SourceRange(2214, 0)), - - // ConsumerWidget - ...await assist.testRun(result, const SourceRange(2582, 0)), - ]; - - expect(changes, hasLength(9)); - - return changes; - }, - ); -} diff --git a/packages/riverpod_lint_flutter_test/test/assists/convert_to_hook_consumer_widget.json b/packages/riverpod_lint_flutter_test/test/assists/convert_to_hook_consumer_widget.json new file mode 100644 index 000000000..449c635fc --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/assists/convert_to_hook_consumer_widget.json @@ -0,0 +1 @@ +[{"priority":37,"change":{"message":"Convert to HookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":266,"length":0,"replacement":", WidgetRef ref"},{"offset":168,"length":15,"replacement":"HookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":37,"change":{"message":"Convert to HookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":454,"length":0,"replacement":", WidgetRef ref"},{"offset":342,"length":15,"replacement":"HookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":37,"change":{"message":"Convert to HookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":608,"length":0,"replacement":", WidgetRef ref"},{"offset":520,"length":10,"replacement":"HookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":33,"change":{"message":"Convert to HookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":993,"length":174,"replacement":""},{"offset":990,"length":0,"replacement":"/// Hello world\n @override\n Widget build(BuildContext context, WidgetRef ref) {\n // Some comments\n return const Placeholder();\n }\n"},{"offset":927,"length":62,"replacement":""},{"offset":876,"length":14,"replacement":"HookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":33,"change":{"message":"Convert to HookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":1350,"length":165,"replacement":""},{"offset":1347,"length":0,"replacement":"@override\n Widget build(\n BuildContext context, WidgetRef ref,\n ) {\n return const Placeholder();\n }\n"},{"offset":1265,"length":81,"replacement":""},{"offset":1203,"length":14,"replacement":"HookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":37,"change":{"message":"Convert to HookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":1676,"length":142,"replacement":""},{"offset":1673,"length":0,"replacement":"@override\n Widget build(BuildContext context, WidgetRef ref) {\n return const Placeholder();\n }\n"},{"offset":1603,"length":69,"replacement":""},{"offset":1544,"length":18,"replacement":"HookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":37,"change":{"message":"Convert to HookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":2008,"length":159,"replacement":""},{"offset":2005,"length":0,"replacement":"@override\n Widget build(BuildContext context, WidgetRef ref) {\n return const Placeholder();\n }\n"},{"offset":1918,"length":86,"replacement":""},{"offset":1851,"length":22,"replacement":"HookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":37,"change":{"message":"Convert to HookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":2383,"length":167,"replacement":""},{"offset":2380,"length":0,"replacement":"@override\n Widget build(BuildContext context, WidgetRef ref) {\n return const Placeholder();\n }\n"},{"offset":2279,"length":100,"replacement":""},{"offset":2204,"length":26,"replacement":"HookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":37,"change":{"message":"Convert to HookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":2575,"length":14,"replacement":"HookConsumerWidget"}]}],"linkedEditGroups":[]}}] \ No newline at end of file diff --git a/packages/riverpod_lint_flutter_test/test/assists/convert_to_hook_widget.json b/packages/riverpod_lint_flutter_test/test/assists/convert_to_hook_widget.json new file mode 100644 index 000000000..2698c3ded --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/assists/convert_to_hook_widget.json @@ -0,0 +1 @@ +[{"priority":36,"change":{"message":"Convert to HookWidget","edits":[{"fileStamp":0,"edits":[{"offset":168,"length":15,"replacement":"HookWidget"}]}],"linkedEditGroups":[]}},{"priority":36,"change":{"message":"Convert to HookWidget","edits":[{"fileStamp":0,"edits":[{"offset":342,"length":15,"replacement":"HookWidget"}]}],"linkedEditGroups":[]}},{"priority":36,"change":{"message":"Convert to HookWidget","edits":[{"fileStamp":0,"edits":[{"offset":787,"length":23,"replacement":""},{"offset":678,"length":18,"replacement":"HookWidget"}]}],"linkedEditGroups":[]}},{"priority":32,"change":{"message":"Convert to HookWidget","edits":[{"fileStamp":0,"edits":[{"offset":993,"length":174,"replacement":""},{"offset":990,"length":0,"replacement":"/// Hello world\n @override\n Widget build(BuildContext context) {\n // Some comments\n return const Placeholder();\n }\n"},{"offset":927,"length":62,"replacement":""},{"offset":876,"length":14,"replacement":"HookWidget"}]}],"linkedEditGroups":[]}},{"priority":32,"change":{"message":"Convert to HookWidget","edits":[{"fileStamp":0,"edits":[{"offset":1350,"length":165,"replacement":""},{"offset":1347,"length":0,"replacement":"@override\n Widget build(\n BuildContext context,\n ) {\n return const Placeholder();\n }\n"},{"offset":1265,"length":81,"replacement":""},{"offset":1203,"length":14,"replacement":"HookWidget"}]}],"linkedEditGroups":[]}},{"priority":36,"change":{"message":"Convert to HookWidget","edits":[{"fileStamp":0,"edits":[{"offset":1676,"length":142,"replacement":""},{"offset":1673,"length":0,"replacement":"@override\n Widget build(BuildContext context) {\n return const Placeholder();\n }\n"},{"offset":1603,"length":69,"replacement":""},{"offset":1544,"length":18,"replacement":"HookWidget"}]}],"linkedEditGroups":[]}},{"priority":36,"change":{"message":"Convert to HookWidget","edits":[{"fileStamp":0,"edits":[{"offset":2008,"length":159,"replacement":""},{"offset":2005,"length":0,"replacement":"@override\n Widget build(BuildContext context) {\n return const Placeholder();\n }\n"},{"offset":1918,"length":86,"replacement":""},{"offset":1851,"length":22,"replacement":"HookWidget"}]}],"linkedEditGroups":[]}},{"priority":36,"change":{"message":"Convert to HookWidget","edits":[{"fileStamp":0,"edits":[{"offset":2383,"length":167,"replacement":""},{"offset":2380,"length":0,"replacement":"@override\n Widget build(BuildContext context) {\n return const Placeholder();\n }\n"},{"offset":2279,"length":100,"replacement":""},{"offset":2204,"length":26,"replacement":"HookWidget"}]}],"linkedEditGroups":[]}},{"priority":36,"change":{"message":"Convert to HookWidget","edits":[{"fileStamp":0,"edits":[{"offset":2676,"length":23,"replacement":""},{"offset":2575,"length":14,"replacement":"HookWidget"}]}],"linkedEditGroups":[]}}] \ No newline at end of file diff --git a/packages/riverpod_lint_flutter_test/test/assists/convert_to_stateful_hook_consumer_widget.json b/packages/riverpod_lint_flutter_test/test/assists/convert_to_stateful_hook_consumer_widget.json new file mode 100644 index 000000000..de88392fa --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/assists/convert_to_stateful_hook_consumer_widget.json @@ -0,0 +1 @@ +[{"priority":33,"change":{"message":"Convert to StatefulHookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":221,"length":0,"replacement":"@override\n ConsumerState createState() => _StatelessState();\n}\n\nclass _StatelessState extends ConsumerState {\n"},{"offset":168,"length":15,"replacement":"StatefulHookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":33,"change":{"message":"Convert to StatefulHookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":404,"length":0,"replacement":"@override\n ConsumerState createState() => _StatelessWithCommaState();\n}\n\nclass _StatelessWithCommaState extends ConsumerState {\n"},{"offset":342,"length":15,"replacement":"StatefulHookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":33,"change":{"message":"Convert to StatefulHookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":563,"length":0,"replacement":"@override\n ConsumerState createState() => _HookState();\n}\n\nclass _HookState extends ConsumerState {\n"},{"offset":520,"length":10,"replacement":"StatefulHookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":33,"change":{"message":"Convert to StatefulHookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":787,"length":23,"replacement":""},{"offset":737,"length":0,"replacement":"@override\n ConsumerState createState() => _HookConsumerState();\n}\n\nclass _HookConsumerState extends ConsumerState {\n"},{"offset":678,"length":18,"replacement":"StatefulHookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":29,"change":{"message":"Convert to StatefulHookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":1022,"length":15,"replacement":"ConsumerState"},{"offset":939,"length":15,"replacement":"ConsumerState"},{"offset":876,"length":14,"replacement":"StatefulHookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":29,"change":{"message":"Convert to StatefulHookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":1389,"length":26,"replacement":"ConsumerState"},{"offset":1203,"length":14,"replacement":"StatefulHookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":33,"change":{"message":"Convert to StatefulHookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":1708,"length":19,"replacement":"ConsumerState"},{"offset":1615,"length":19,"replacement":"ConsumerState"},{"offset":1544,"length":18,"replacement":"StatefulHookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":33,"change":{"message":"Convert to StatefulHookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":2045,"length":31,"replacement":"ConsumerState"},{"offset":1930,"length":31,"replacement":"ConsumerState"},{"offset":1851,"length":22,"replacement":"StatefulHookConsumerWidget"}]}],"linkedEditGroups":[]}},{"priority":33,"change":{"message":"Convert to StatefulHookConsumerWidget","edits":[{"fileStamp":0,"edits":[{"offset":2676,"length":23,"replacement":""},{"offset":2626,"length":0,"replacement":"@override\n ConsumerState createState() => _ConsumerState();\n}\n\nclass _ConsumerState extends ConsumerState {\n"},{"offset":2575,"length":14,"replacement":"StatefulHookConsumerWidget"}]}],"linkedEditGroups":[]}}] \ No newline at end of file diff --git a/packages/riverpod_lint_flutter_test/test/assists/convert_to_stateful_hook_widget.json b/packages/riverpod_lint_flutter_test/test/assists/convert_to_stateful_hook_widget.json new file mode 100644 index 000000000..8190dcdbc --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/assists/convert_to_stateful_hook_widget.json @@ -0,0 +1 @@ +[{"priority":32,"change":{"message":"Convert to StatefulHookWidget","edits":[{"fileStamp":0,"edits":[{"offset":221,"length":0,"replacement":"@override\n State createState() => _StatelessState();\n}\n\nclass _StatelessState extends State {\n"},{"offset":168,"length":15,"replacement":"StatefulHookWidget"}]}],"linkedEditGroups":[]}},{"priority":32,"change":{"message":"Convert to StatefulHookWidget","edits":[{"fileStamp":0,"edits":[{"offset":404,"length":0,"replacement":"@override\n State createState() => _StatelessWithCommaState();\n}\n\nclass _StatelessWithCommaState extends State {\n"},{"offset":342,"length":15,"replacement":"StatefulHookWidget"}]}],"linkedEditGroups":[]}},{"priority":32,"change":{"message":"Convert to StatefulHookWidget","edits":[{"fileStamp":0,"edits":[{"offset":563,"length":0,"replacement":"@override\n State createState() => _HookState();\n}\n\nclass _HookState extends State {\n"},{"offset":520,"length":10,"replacement":"StatefulHookWidget"}]}],"linkedEditGroups":[]}},{"priority":32,"change":{"message":"Convert to StatefulHookWidget","edits":[{"fileStamp":0,"edits":[{"offset":787,"length":23,"replacement":""},{"offset":737,"length":0,"replacement":"@override\n State createState() => _HookConsumerState();\n}\n\nclass _HookConsumerState extends State {\n"},{"offset":678,"length":18,"replacement":"StatefulHookWidget"}]}],"linkedEditGroups":[]}},{"priority":28,"change":{"message":"Convert to StatefulHookWidget","edits":[{"fileStamp":0,"edits":[{"offset":1022,"length":15,"replacement":"State"},{"offset":939,"length":15,"replacement":"State"},{"offset":876,"length":14,"replacement":"StatefulHookWidget"}]}],"linkedEditGroups":[]}},{"priority":28,"change":{"message":"Convert to StatefulHookWidget","edits":[{"fileStamp":0,"edits":[{"offset":1389,"length":26,"replacement":"State"},{"offset":1203,"length":14,"replacement":"StatefulHookWidget"}]}],"linkedEditGroups":[]}},{"priority":32,"change":{"message":"Convert to StatefulHookWidget","edits":[{"fileStamp":0,"edits":[{"offset":2045,"length":31,"replacement":"State"},{"offset":1930,"length":31,"replacement":"State"},{"offset":1851,"length":22,"replacement":"StatefulHookWidget"}]}],"linkedEditGroups":[]}},{"priority":32,"change":{"message":"Convert to StatefulHookWidget","edits":[{"fileStamp":0,"edits":[{"offset":2424,"length":35,"replacement":"State"},{"offset":2291,"length":35,"replacement":"State"},{"offset":2204,"length":26,"replacement":"StatefulHookWidget"}]}],"linkedEditGroups":[]}},{"priority":32,"change":{"message":"Convert to StatefulHookWidget","edits":[{"fileStamp":0,"edits":[{"offset":2676,"length":23,"replacement":""},{"offset":2626,"length":0,"replacement":"@override\n State createState() => _ConsumerState();\n}\n\nclass _ConsumerState extends State {\n"},{"offset":2575,"length":14,"replacement":"StatefulHookWidget"}]}],"linkedEditGroups":[]}}] \ No newline at end of file diff --git a/packages/riverpod_lint_flutter_test/test/assists/convert_to_stateful_widget.json b/packages/riverpod_lint_flutter_test/test/assists/convert_to_stateful_widget.json new file mode 100644 index 000000000..1b18dc0fd --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/assists/convert_to_stateful_widget.json @@ -0,0 +1 @@ +[{"priority":30,"change":{"message":"Convert to StatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":563,"length":0,"replacement":"@override\n State createState() => _HookState();\n}\n\nclass _HookState extends State {\n"},{"offset":520,"length":10,"replacement":"StatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":30,"change":{"message":"Convert to StatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":787,"length":23,"replacement":""},{"offset":737,"length":0,"replacement":"@override\n State createState() => _HookConsumerState();\n}\n\nclass _HookConsumerState extends State {\n"},{"offset":678,"length":18,"replacement":"StatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":30,"change":{"message":"Convert to StatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":1708,"length":19,"replacement":"State"},{"offset":1615,"length":19,"replacement":"State"},{"offset":1544,"length":18,"replacement":"StatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":30,"change":{"message":"Convert to StatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":2045,"length":31,"replacement":"State"},{"offset":1930,"length":31,"replacement":"State"},{"offset":1851,"length":22,"replacement":"StatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":30,"change":{"message":"Convert to StatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":2424,"length":35,"replacement":"State"},{"offset":2291,"length":35,"replacement":"State"},{"offset":2204,"length":26,"replacement":"StatefulWidget"}]}],"linkedEditGroups":[]}},{"priority":30,"change":{"message":"Convert to StatefulWidget","edits":[{"fileStamp":0,"edits":[{"offset":2676,"length":23,"replacement":""},{"offset":2626,"length":0,"replacement":"@override\n State createState() => _ConsumerState();\n}\n\nclass _ConsumerState extends State {\n"},{"offset":2575,"length":14,"replacement":"StatefulWidget"}]}],"linkedEditGroups":[]}}] \ No newline at end of file diff --git a/packages/riverpod_lint_flutter_test/test/assists/convert_to_stateless_widget.json b/packages/riverpod_lint_flutter_test/test/assists/convert_to_stateless_widget.json new file mode 100644 index 000000000..71d0987a6 --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/assists/convert_to_stateless_widget.json @@ -0,0 +1 @@ +[{"priority":34,"change":{"message":"Convert to StatelessWidget","edits":[{"fileStamp":0,"edits":[{"offset":520,"length":10,"replacement":"StatelessWidget"}]}],"linkedEditGroups":[]}},{"priority":34,"change":{"message":"Convert to StatelessWidget","edits":[{"fileStamp":0,"edits":[{"offset":787,"length":23,"replacement":""},{"offset":678,"length":18,"replacement":"StatelessWidget"}]}],"linkedEditGroups":[]}},{"priority":34,"change":{"message":"Convert to StatelessWidget","edits":[{"fileStamp":0,"edits":[{"offset":1676,"length":142,"replacement":""},{"offset":1673,"length":0,"replacement":"@override\n Widget build(BuildContext context) {\n return const Placeholder();\n }\n"},{"offset":1603,"length":69,"replacement":""},{"offset":1544,"length":18,"replacement":"StatelessWidget"}]}],"linkedEditGroups":[]}},{"priority":34,"change":{"message":"Convert to StatelessWidget","edits":[{"fileStamp":0,"edits":[{"offset":2008,"length":159,"replacement":""},{"offset":2005,"length":0,"replacement":"@override\n Widget build(BuildContext context) {\n return const Placeholder();\n }\n"},{"offset":1918,"length":86,"replacement":""},{"offset":1851,"length":22,"replacement":"StatelessWidget"}]}],"linkedEditGroups":[]}},{"priority":34,"change":{"message":"Convert to StatelessWidget","edits":[{"fileStamp":0,"edits":[{"offset":2383,"length":167,"replacement":""},{"offset":2380,"length":0,"replacement":"@override\n Widget build(BuildContext context) {\n return const Placeholder();\n }\n"},{"offset":2279,"length":100,"replacement":""},{"offset":2204,"length":26,"replacement":"StatelessWidget"}]}],"linkedEditGroups":[]}},{"priority":34,"change":{"message":"Convert to StatelessWidget","edits":[{"fileStamp":0,"edits":[{"offset":2676,"length":23,"replacement":""},{"offset":2575,"length":14,"replacement":"StatelessWidget"}]}],"linkedEditGroups":[]}}] \ No newline at end of file diff --git a/packages/riverpod_lint_flutter_test/test/assists/convert_to_consumer_widget.dart b/packages/riverpod_lint_flutter_test/test/assists/convert_to_widget.dart similarity index 100% rename from packages/riverpod_lint_flutter_test/test/assists/convert_to_consumer_widget.dart rename to packages/riverpod_lint_flutter_test/test/assists/convert_to_widget.dart diff --git a/packages/riverpod_lint_flutter_test/test/assists/convert_to_widget_test.dart b/packages/riverpod_lint_flutter_test/test/assists/convert_to_widget_test.dart new file mode 100644 index 000000000..acf84f117 --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/assists/convert_to_widget_test.dart @@ -0,0 +1,215 @@ +import 'dart:io'; + +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:riverpod_lint/src/assists/convert_to_stateful_base_widget.dart'; +import 'package:riverpod_lint/src/assists/convert_to_stateless_base_widget.dart'; +import 'package:riverpod_lint/src/assists/convert_to_widget_utils.dart'; +import 'package:riverpod_lint/src/riverpod_custom_lint.dart'; +import 'package:test/test.dart'; + +import '../golden.dart'; + +void main() { + final pubspecWithDependencies = Pubspec( + 'test_project', + dependencies: { + 'hooks_riverpod': HostedDependency(), + 'flutter_hooks': HostedDependency(), + }, + ); + + StatelessBaseWidgetType.values.forEach( + (targetWidget) { + _runGoldenTest( + ConvertToStatelessBaseWidget( + targetWidget: targetWidget, + ), + 'Convert widgets to ${targetWidget.name}s with hooks_riverpod and flutter_hooks dependency', + 'assists/convert_to_${targetWidget.name.toSnakeCase()}.json', + pubspecWithDependencies, + targetWidget == StatelessBaseWidgetType.statelessWidget ? 6 : 9, + ); + }, + ); + + StatefulBaseWidgetType.values.forEach( + (targetWidget) { + _runGoldenTest( + ConvertToStatefulBaseWidget( + targetWidget: targetWidget, + ), + 'Convert widgets to ${targetWidget.name}s with hooks_riverpod and flutter_hooks dependency', + 'assists/convert_to_${targetWidget.name.toSnakeCase()}.json', + pubspecWithDependencies, + targetWidget == StatefulBaseWidgetType.statefulWidget ? 6 : 9, + ); + }, + ); + + final pubspecWithoutDependencies = Pubspec( + 'test_project', + ); + + StatelessBaseWidgetType.values.forEach( + (targetWidget) { + final int expectedChangeCount; + switch (targetWidget) { + case StatelessBaseWidgetType.consumerWidget: + expectedChangeCount = 9; + break; + case StatelessBaseWidgetType.hookWidget: + case StatelessBaseWidgetType.hookConsumerWidget: + expectedChangeCount = 0; + break; + case StatelessBaseWidgetType.statelessWidget: + expectedChangeCount = 6; + break; + } + final String goldenFilePath; + switch (targetWidget) { + case StatelessBaseWidgetType.hookWidget: + case StatelessBaseWidgetType.hookConsumerWidget: + goldenFilePath = 'assists/empty.json'; + break; + case StatelessBaseWidgetType.consumerWidget: + case StatelessBaseWidgetType.statelessWidget: + goldenFilePath = + 'assists/convert_to_${targetWidget.name.toSnakeCase()}.json'; + break; + } + + _runGoldenTest( + ConvertToStatelessBaseWidget( + targetWidget: targetWidget, + ), + 'Convert widgets to ${targetWidget.name}s without hooks_riverpod and flutter_hooks dependency', + goldenFilePath, + pubspecWithoutDependencies, + expectedChangeCount, + ); + }, + ); + + StatefulBaseWidgetType.values.forEach( + (targetWidget) { + final int expectedChangeCount; + switch (targetWidget) { + case StatefulBaseWidgetType.consumerStatefulWidget: + expectedChangeCount = 9; + break; + case StatefulBaseWidgetType.statefulHookWidget: + case StatefulBaseWidgetType.statefulHookConsumerWidget: + expectedChangeCount = 0; + break; + case StatefulBaseWidgetType.statefulWidget: + expectedChangeCount = 6; + break; + } + final String goldenFilePath; + switch (targetWidget) { + case StatefulBaseWidgetType.statefulHookWidget: + case StatefulBaseWidgetType.statefulHookConsumerWidget: + goldenFilePath = 'assists/empty.json'; + break; + case StatefulBaseWidgetType.consumerStatefulWidget: + case StatefulBaseWidgetType.statefulWidget: + goldenFilePath = + 'assists/convert_to_${targetWidget.name.toSnakeCase()}.json'; + break; + } + + _runGoldenTest( + ConvertToStatefulBaseWidget( + targetWidget: targetWidget, + ), + 'Convert widgets to ${targetWidget.name}s without hooks_riverpod and flutter_hooks dependency', + goldenFilePath, + pubspecWithoutDependencies, + expectedChangeCount, + ); + }, + ); +} + +extension _StringX on String { + String toSnakeCase() { + return replaceAllMapped( + RegExp(r'([A-Z])'), + (match) => '_${match.group(1)!.toLowerCase()}', + ); + } +} + +void _runGoldenTest( + RiverpodAssist assist, + String description, + String goldenFilePath, + Pubspec pubspec, + int expectedChangeCount, +) { + testGolden( + description, + goldenFilePath, + () async { + final file = File( + 'test/assists/convert_to_widget.dart', + ).absolute; + + final result = await resolveFile2(path: file.path); + result as ResolvedUnitResult; + + final changes = [ + // Stateless + ...await assist.testRun(result, const SourceRange(163, 0), + pubspec: pubspec), + ...await assist.testRun(result, const SourceRange(174, 0), + pubspec: pubspec), + ...await assist.testRun(result, const SourceRange(185, 0), + pubspec: pubspec), + + // StatelessWithComma + ...await assist.testRun(result, const SourceRange(350, 0), + pubspec: pubspec), + + // Hook + ...await assist.testRun(result, const SourceRange(524, 0), + pubspec: pubspec), + + // HookConsumer + ...await assist.testRun(result, const SourceRange(690, 0), + pubspec: pubspec), + + // Stateful + ...await assist.testRun(result, const SourceRange(884, 0), + pubspec: pubspec), + + // ExplicitCreateState + ...await assist.testRun(result, const SourceRange(1208, 0), + pubspec: pubspec), + + // HookStateful + ...await assist.testRun(result, const SourceRange(1553, 0), + pubspec: pubspec), + + // ConsumerStateful + ...await assist.testRun(result, const SourceRange(1863, 0), + pubspec: pubspec), + + // HookConsumerStateful + ...await assist.testRun(result, const SourceRange(2214, 0), + pubspec: pubspec), + + // ConsumerWidget + ...await assist.testRun(result, const SourceRange(2582, 0), + pubspec: pubspec), + ]; + + expect(changes, hasLength(expectedChangeCount)); + + return changes; + }, + ); +} diff --git a/packages/riverpod_lint_flutter_test/test/assists/empty.json b/packages/riverpod_lint_flutter_test/test/assists/empty.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/assists/empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file