Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add conversion assists for various widgets, including Hook-based widgets #2306

Merged
merged 30 commits into from
Apr 8, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cd7f967
enable riverpod_lint
K9i-0 Mar 12, 2023
b4fea06
remove unnecessary Consumer
K9i-0 Mar 12, 2023
42f6739
move field
K9i-0 Mar 12, 2023
40f6379
add convert to widget assists
K9i-0 Mar 12, 2023
c8b2e46
implement ConvertToStatelessWidget
K9i-0 Mar 12, 2023
2024145
implement ConvertToStatefulWidget
K9i-0 Mar 12, 2023
38dfc92
fix file name
K9i-0 Mar 12, 2023
6e67631
add current test result
K9i-0 Mar 12, 2023
e08805d
Merge branch 'master' into add_new_lints
K9i-0 Mar 14, 2023
ee9bae7
Implement ConvertToStatefulHookWidget's method
K9i-0 Mar 17, 2023
3ed6c52
Implement ConvertToStatefulHookConsumerWidget's method
K9i-0 Mar 17, 2023
ec624b2
Implement ConvertToHookWidget's method
K9i-0 Mar 17, 2023
6cac1ac
Implement ConvertToHookConsumerWidget's method
K9i-0 Mar 17, 2023
feff63e
Add test
K9i-0 Mar 21, 2023
a29515c
Refactor
K9i-0 Mar 21, 2023
177eda9
remove
K9i-0 Mar 21, 2023
f23df4c
Merge branch 'master' into add_new_lints
K9i-0 Apr 7, 2023
74524df
Display hook-related assists only when there is a dependency on hooks…
K9i-0 Apr 8, 2023
5e50047
Change priorities
K9i-0 Apr 8, 2023
02b77fa
update golden files
K9i-0 Apr 8, 2023
f8f4597
change test file name
K9i-0 Apr 8, 2023
5eb0935
Provide assists for hook-related functions only when there is a depen…
K9i-0 Apr 8, 2023
a224e24
change name
K9i-0 Apr 8, 2023
d9d8a34
Merge branch 'master' of https://github.com/rrousselGit/riverpod into…
rrousselGit Apr 8, 2023
1929cba
Remove dynamic
rrousselGit Apr 8, 2023
0c12926
Update packages/riverpod_lint_flutter_test/test/assists/convert_to_wi…
rrousselGit Apr 8, 2023
c38f878
Use named parameters
K9i-0 Apr 8, 2023
94e67f0
Refactor
rrousselGit Apr 8, 2023
b2c8fb9
Merge branch 'add_new_lints' of https://github.com/k9i-0/riverpod int…
rrousselGit Apr 8, 2023
3975a36
Changelog
rrousselGit Apr 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions packages/riverpod_lint/lib/riverpod_lint.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -56,8 +57,16 @@ class _RiverpodPlugin extends PluginBase {
List<Assist> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -39,148 +29,146 @@ class ConvertToConsumerStatefulWidget extends RiverpodAssist {
CustomLintContext context,
SourceRange target,
) {
if (targetWidget.needHooksRiverpod &&
!context.pubspec.dependencies.keys.contains('hooks_riverpod')) {
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)) {
_convertStatelessToConsumerStatefulWidget(reporter, node);
if (statelessBaseType.isExactlyType(type)) {
_convertStatelessToStatefulWidget(reporter, node);
return;
}

if (_statefulBaseType.isExactlyType(type)) {
_convertStatefulToConsumerStatefulWidget(
reporter,
node,
resolver.source,
);
if (statefulBaseType.isExactlyType(type)) {
_convertStatefulToStatefulWidget(reporter, node, resolver.source);
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<ClassDeclaration>();
if (widgetClass == null) return;

final stateClass = _findStateClass(widgetClass);
if (stateClass == null) return;

final createStateMethod = widgetClass.members
final buildMethod = node
.thisOrAncestorOfType<ClassDeclaration>()
?.members
.whereType<MethodDeclaration>()
.firstWhereOrNull((element) => element.name.lexeme == 'createState');
if (createStateMethod != null) {
final returnTypeString = createStateMethod.returnType?.toSource() ?? '';
if (returnTypeString != stateClass.name.lexeme) {
// Replace State<MyWidget> with ConsumerState<MyWidget>
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<MyWidget> with ConsumerState<MyWidget>
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<CompilationUnit>()
?.declarations
.whereType<ClassDeclaration>()
.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,
) {
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<ClassDeclaration>();
if (widgetClass == null) return;

// Now update "build" to take a "ref" parameter
final buildMethod = node
.thisOrAncestorOfType<ClassDeclaration>()
?.members
.whereType<MethodDeclaration>()
.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<MethodDeclaration>()
.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}>',
);
}
});
Expand Down
Loading