diff --git a/.vscode/settings.json b/.vscode/settings.json index 9c316d5..e217b26 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,7 @@ "ints", "pubspec", "redeclares", + "RGBO", "subosito", "Supertypes", "tearoffs", diff --git a/packages/altive_lints/lib/altive_lints.dart b/packages/altive_lints/lib/altive_lints.dart index 33cbeb9..addc0b5 100644 --- a/packages/altive_lints/lib/altive_lints.dart +++ b/packages/altive_lints/lib/altive_lints.dart @@ -1,16 +1,20 @@ 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'; +import 'src/lints/avoid_single_child.dart'; PluginBase createPlugin() => _AltivePlugin(); class _AltivePlugin extends PluginBase { @override List getLintRules(CustomLintConfigs configs) => [ + const AvoidConsecutiveSliverToBoxAdapter(), + const AvoidHardcodedColor(), const AvoidHardcodedJapanese(), const AvoidShrinkWrapInListView(), - const AvoidConsecutiveSliverToBoxAdapter(), + const AvoidSingleChild(), ]; } diff --git a/packages/altive_lints/lib/src/lints/avoid_hardcoded_color.dart b/packages/altive_lints/lib/src/lints/avoid_hardcoded_color.dart new file mode 100644 index 0000000..2119151 --- /dev/null +++ b/packages/altive_lints/lib/src/lints/avoid_hardcoded_color.dart @@ -0,0 +1,78 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +/// An `avoid_hardcoded_color` rule that discourages the use of +/// hardcoded colors directly in the code, promoting the use of `ColorScheme`, +/// `ThemeExtension`, or other Theme-based systems for defining colors. +/// +/// This practice ensures that colors are consistent and +/// adaptable to different themes and accessibility settings. +/// +/// ### Example +/// +/// #### BAD: +/// +/// ```dart +/// Container( +/// color: Color(0xFF00FF00), // LINT +/// ); +/// ``` +/// +/// #### GOOD: +/// +/// ```dart +/// Container( +/// color: Theme.of(context).colorScheme.primary, +/// ); +/// ``` +class AvoidHardcodedColor extends DartLintRule { + const AvoidHardcodedColor() : super(code: _lintCode); + + static const _lintCode = LintCode( + name: 'avoid_hardcoded_color', + problemMessage: 'Avoid using hardcoded color. Use ColorScheme, ' + 'ThemeExtension, or other Theme-based color definitions instead. ' + 'Consider using Theme.of(context).colorScheme or a custom ' + 'ThemeExtension for color definitions.', + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addInstanceCreationExpression((node) { + final typeName = + node.staticType?.getDisplayString(withNullability: false); + + if (typeName == 'Color') { + reporter.reportErrorForNode(code, node); + } + }); + + context.registry.addPrefixedIdentifier((node) { + final prefix = node.prefix.name; + if (prefix == 'Colors') { + final element = node.staticElement; + if (element is PropertyAccessorElement) { + final returnType = element.returnType; + if (_isColorType(returnType)) { + reporter.reportErrorForNode(code, node); + } + } + } + }); + } + + bool _isColorType(DartType? type) { + return type != null && + (type.isDartCoreInt || + type.getDisplayString(withNullability: false) == 'Color' || + type.getDisplayString(withNullability: false) == 'MaterialColor' || + type.getDisplayString(withNullability: false) == + 'MaterialAccentColor'); + } +} diff --git a/packages/altive_lints/lib/src/lints/avoid_single_child.dart b/packages/altive_lints/lib/src/lints/avoid_single_child.dart new file mode 100644 index 0000000..649f555 --- /dev/null +++ b/packages/altive_lints/lib/src/lints/avoid_single_child.dart @@ -0,0 +1,77 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:collection/collection.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +/// An `avoid_single_child` rule that warns against using layout +/// widgets intended for multiple children with only one child. +/// +/// This includes widgets like `Column`,`Row`, `Flex`, `Wrap`, +/// `ListView`, and `SliverList`. +/// +/// Using these widgets with a single child can lead to +/// unnecessary overhead and less efficient layouts. +/// Instead, consider using a widget more suited for single +/// children or adding more children to the widget. +/// +/// ### Example +/// +/// #### BAD: +/// +/// ```dart +/// Column( +/// children: [YourWidget()], // LINT +/// ); +/// ``` +/// +/// #### GOOD: +/// +/// ```dart +/// Center(child: YourWidget()); +/// // or +/// Column( +/// children: [YourWidget1(), YourWidget2()], +/// ); +/// ``` +class AvoidSingleChild extends DartLintRule { + const AvoidSingleChild() : super(code: _code); + + static const _code = LintCode( + name: 'avoid_single_child', + problemMessage: + 'Avoid using a single child in widgets that expect multiple children. ' + 'Consider using a single child widget or adding more children.', + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addInstanceCreationExpression((node) { + final className = + node.staticType?.getDisplayString(withNullability: false); + if ([ + 'Column', + 'Row', + 'Flex', + 'Wrap', + 'ListView', + 'SliverList', + ].contains(className)) { + final childrenArg = node.argumentList.arguments.firstWhereOrNull( + (arg) => arg is NamedExpression && arg.name.label.name == 'children', + ); + + final childrenList = childrenArg is NamedExpression + ? childrenArg.expression as ListLiteral + : null; + + if (childrenList != null && childrenList.elements.length == 1) { + reporter.reportErrorForNode(_code, node); + } + } + }); + } +} diff --git a/packages/altive_lints/lint_test/lints/avoid_hardcoded_color.dart b/packages/altive_lints/lint_test/lints/avoid_hardcoded_color.dart new file mode 100644 index 0000000..53b1a00 --- /dev/null +++ b/packages/altive_lints/lint_test/lints/avoid_hardcoded_color.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // expect_lint: avoid_hardcoded_color + const ColoredBox(color: Color(0xFF00FF00)), + // expect_lint: avoid_hardcoded_color + const ColoredBox(color: Color.fromRGBO(0, 255, 0, 1)), + // expect_lint: avoid_hardcoded_color + const ColoredBox(color: Colors.green), + ColoredBox(color: Theme.of(context).colorScheme.primary), + ], + ); + } +} diff --git a/packages/altive_lints/lint_test/lints/avoid_shrink_wrap_in_list_view.dart b/packages/altive_lints/lint_test/lints/avoid_shrink_wrap_in_list_view.dart index 492f296..72e1dba 100644 --- a/packages/altive_lints/lint_test/lints/avoid_shrink_wrap_in_list_view.dart +++ b/packages/altive_lints/lint_test/lints/avoid_shrink_wrap_in_list_view.dart @@ -1,3 +1,4 @@ +// ignore_for_file: avoid_single_child import 'package:flutter/material.dart'; class MyWidget extends StatelessWidget { diff --git a/packages/altive_lints/lint_test/lints/avoid_single_child.dart b/packages/altive_lints/lint_test/lints/avoid_single_child.dart new file mode 100644 index 0000000..dcaa7d5 --- /dev/null +++ b/packages/altive_lints/lint_test/lints/avoid_single_child.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +class MyWidget extends StatelessWidget { + const MyWidget({super.key}); + + @override + Widget build(BuildContext context) { + return ListView( + children: [ + // expect_lint: avoid_single_child + const Column( + children: [ + Text('Hello'), + ], + ), + // expect_lint: avoid_single_child + const Row( + children: [ + Text('World'), + ], + ), + // expect_lint: avoid_single_child + const Flex( + direction: Axis.horizontal, + children: [ + Text('Hello'), + ], + ), + // expect_lint: avoid_single_child + const Wrap( + children: [ + Text('World'), + ], + ), + // expect_lint: avoid_single_child + ListView( + children: const [ + Text('Hello'), + ], + ), + // expect_lint: avoid_single_child + SliverList.list( + children: const [ + Text('World'), + ], + ), + ], + ); + } +}