diff --git a/packages/altive_lints/lib/altive_lints.dart b/packages/altive_lints/lib/altive_lints.dart index a74465c..addc0b5 100644 --- a/packages/altive_lints/lib/altive_lints.dart +++ b/packages/altive_lints/lib/altive_lints.dart @@ -1,5 +1,6 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'src/lints/avoid_consecutive_sliver_to_box_adapter.dart'; import 'src/lints/avoid_hardcoded_color.dart'; import 'src/lints/avoid_hardcoded_japanese.dart'; import 'src/lints/avoid_shrink_wrap_in_list_view.dart'; @@ -10,6 +11,7 @@ PluginBase createPlugin() => _AltivePlugin(); class _AltivePlugin extends PluginBase { @override List getLintRules(CustomLintConfigs configs) => [ + const AvoidConsecutiveSliverToBoxAdapter(), const AvoidHardcodedColor(), const AvoidHardcodedJapanese(), const AvoidShrinkWrapInListView(), diff --git a/packages/altive_lints/lib/src/lints/avoid_consecutive_sliver_to_box_adapter.dart b/packages/altive_lints/lib/src/lints/avoid_consecutive_sliver_to_box_adapter.dart new file mode 100644 index 0000000..9ebe208 --- /dev/null +++ b/packages/altive_lints/lib/src/lints/avoid_consecutive_sliver_to_box_adapter.dart @@ -0,0 +1,108 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +/// A `avoid_consecutive_sliver_to_box_adapter` rule that +/// identifies and discourages the use of consecutive +/// `SliverToBoxAdapter` widgets within a list. +/// +/// Consecutive usage of `SliverToBoxAdapter` can lead to +/// inefficient nesting and performance issues in scrollable areas. +/// +/// It suggests using `SliverList.list` or similar consolidated +/// sliver widgets to optimize rendering performance and reduce +/// the complexity of the widget tree. +/// +/// ### Example +/// +/// #### BAD: +/// +/// ```dart +/// CustomScrollView( +/// slivers: [ +/// SliverToBoxAdapter(child: Text('Item 1')), // Consecutive usage +/// SliverToBoxAdapter(child: Text('Item 2')), // LINT +/// ], +/// ); +/// ``` +/// +/// #### GOOD: +/// +/// ```dart +/// CustomScrollView( +/// slivers: [ +/// SliverList.list( +/// children: [ +/// Text('Item 1') +/// Text('Item 2') +/// ], +/// ), +/// ], +/// ); +/// ``` +class AvoidConsecutiveSliverToBoxAdapter extends DartLintRule { + const AvoidConsecutiveSliverToBoxAdapter() : super(code: _code); + + static const _code = LintCode( + name: 'avoid_consecutive_sliver_to_box_adapter', + problemMessage: 'Avoid using consecutive `SliverToBoxAdapter`. ' + 'Consider using `SliverList.list` instead.', + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addListLiteral((node) { + final iterator = node.elements.iterator; + if (!iterator.moveNext()) { + // if there are no elements, there is nothing to check. + return; + } + + var current = iterator.current; + while (iterator.moveNext()) { + final next = iterator.current; + if (_useSliverToBoxAdapter(current) && _useSliverToBoxAdapter(next)) { + reporter.reportErrorForNode(_code, node); + return; + } + current = next; + } + }); + } + + bool _useSliverToBoxAdapter(CollectionElement element) { + if (element is! Expression) { + return false; + } + return _isSliverToBoxAdapter(element) || _hasSliverToBoxAdapter(element); + } + + bool _isSliverToBoxAdapter(Expression expression) { + final typeName = + expression.staticType?.getDisplayString(withNullability: false); + return typeName == 'SliverToBoxAdapter'; + } + + bool _hasSliverToBoxAdapter(Expression element) { + if (element is! InstanceCreationExpression) { + return false; + } + final constructor = element; + final arguments = constructor.argumentList.arguments; + for (final argument in arguments) { + if (argument is NamedExpression && argument.name.label.name == 'sliver') { + final sliverExpression = argument.expression; + final sliverTypeName = sliverExpression.staticType + ?.getDisplayString(withNullability: false); + if (sliverTypeName == 'SliverToBoxAdapter') { + return true; + } + } + } + return false; + } +} diff --git a/packages/altive_lints/lint_test/lints/avoid_consecutive_sliver_to_box_adapter.dart b/packages/altive_lints/lint_test/lints/avoid_consecutive_sliver_to_box_adapter.dart new file mode 100644 index 0000000..b21fff3 --- /dev/null +++ b/packages/altive_lints/lint_test/lints/avoid_consecutive_sliver_to_box_adapter.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +class ConsecutiveSliverToBoxAdapters extends StatelessWidget { + const ConsecutiveSliverToBoxAdapters({super.key}); + + @override + Widget build(BuildContext context) { + return const CustomScrollView( + // expect_lint: avoid_consecutive_sliver_to_box_adapter + slivers: [ + SliverToBoxAdapter( + child: Text('Hello'), + ), + SliverToBoxAdapter( + child: Text('World'), + ), + ], + ); + } +} + +class ConsecutiveSliverToBoxAdaptersWithSliverPadding extends StatelessWidget { + const ConsecutiveSliverToBoxAdaptersWithSliverPadding({super.key}); + + @override + Widget build(BuildContext context) { + return const CustomScrollView( + // expect_lint: avoid_consecutive_sliver_to_box_adapter + slivers: [ + SliverPadding( + padding: EdgeInsets.zero, + sliver: SliverToBoxAdapter( + child: Text('Hello'), + ), + ), + SliverPadding( + padding: EdgeInsets.zero, + sliver: SliverToBoxAdapter( + child: Text('World'), + ), + ), + ], + ); + } +} + +class NonConsecutiveSliverToBoxAdapters extends StatelessWidget { + const NonConsecutiveSliverToBoxAdapters({super.key}); + + @override + Widget build(BuildContext context) { + return CustomScrollView( + slivers: [ + const SliverToBoxAdapter( + child: Text('Hello'), + ), + SliverList.builder( + itemCount: 10, + itemBuilder: (context, index) { + return const SliverToBoxAdapter( + child: Text('item'), + ); + }, + ), + const SliverToBoxAdapter( + child: Text('World'), + ), + ], + ); + } +}