diff --git a/forui/example/pubspec.lock b/forui/example/pubspec.lock index 498cffe37..b35981699 100644 --- a/forui/example/pubspec.lock +++ b/forui/example/pubspec.lock @@ -236,10 +236,10 @@ packages: dependency: transitive description: name: flutter_svg - sha256: "936d9c1c010d3e234d1672574636f3352b4941ca3decaddd3cafaeb9ad49c471" + sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123" url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.16" flutter_test: dependency: "direct dev" description: flutter @@ -501,18 +501,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "8c4967f8b7cb46dc914e178daa29813d83ae502e0529d7b0478330616a691ef7" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.14" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -714,10 +714,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: ab9ff38fc771e9ee1139320adbe3d18a60327370c218c60752068ebee4b49ab1 + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.15" + version: "1.1.16" vector_math: dependency: transitive description: @@ -786,10 +786,10 @@ packages: dependency: transitive description: name: win32 - sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" + sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" url: "https://pub.dev" source: hosted - version: "5.8.0" + version: "5.9.0" xdg_directories: dependency: transitive description: diff --git a/forui/lib/src/widgets/switch.dart b/forui/lib/src/widgets/switch.dart index 233b5f067..c1ecb6539 100644 --- a/forui/lib/src/widgets/switch.dart +++ b/forui/lib/src/widgets/switch.dart @@ -309,7 +309,7 @@ final class FSwitchStateStyle with Diagnosticable implements FFormFieldStyle { }); @override - FFormFieldStyle copyWith({ + FSwitchStateStyle copyWith({ Color? checkedColor, Color? uncheckedColor, Color? thumbColor, diff --git a/forui_internal_lints/lib/forui_internal_lints.dart b/forui_internal_lints/lib/forui_internal_lints.dart index fcc5b9e25..b12a2cc53 100644 --- a/forui_internal_lints/lib/forui_internal_lints.dart +++ b/forui_internal_lints/lib/forui_internal_lints.dart @@ -1,8 +1,9 @@ import 'package:custom_lint_builder/custom_lint_builder.dart'; import 'package:forui_internal_lints/src/always_call_super_dispose_last.dart'; +import 'package:forui_internal_lints/src/always_provide_flag_property_parameter.dart'; import 'package:forui_internal_lints/src/avoid_record_diagnostics_properties.dart'; -import 'package:forui_internal_lints/src/diagnosticable_styles.dart'; +import 'package:forui_internal_lints/src/style_api.dart'; import 'package:forui_internal_lints/src/prefer_specific_diagnostics_properties.dart'; import 'package:forui_internal_lints/src/prefix_public_types.dart'; @@ -13,8 +14,9 @@ class _ForuiLinter extends PluginBase { @override List getLintRules(CustomLintConfigs configs) => const [ AlwaysCallSuperDisposeLast(), + AlwaysProvideFlagPropertyArgument(), AvoidRecordDiagnosticsProperties(), - DiagnosticableStylesRule(), + StyleApiRule(), PreferSpecificDiagnosticsProperties(), PrefixPublicTypesRule(), ]; diff --git a/forui_internal_lints/lib/src/always_call_super_dispose_last.dart b/forui_internal_lints/lib/src/always_call_super_dispose_last.dart index 16fc5c3f3..67c1bd57f 100644 --- a/forui_internal_lints/lib/src/always_call_super_dispose_last.dart +++ b/forui_internal_lints/lib/src/always_call_super_dispose_last.dart @@ -38,7 +38,8 @@ class AlwaysCallSuperDisposeLast extends DartLintRule { if (node.body case BlockFunctionBody(block: Block(:final statements)) when statements.isNotEmpty) { for (final statement in statements.reversed.skip(1)) { - if (statement case ExpressionStatement(:final MethodInvocation expression) when expression.toSource() == 'super.dispose()') { + if (statement case ExpressionStatement(:final MethodInvocation expression) + when expression.toSource() == 'super.dispose()') { reporter.atNode(expression, _code); } } @@ -59,13 +60,14 @@ class _Visitor extends SimpleElementVisitor { bool? visitMixinElement(MixinElement type) => _visitInterface(type); bool _visitInterface(InterfaceElement interface) { - if (self != interface && - interface.methods.any((method) => - !method.isStatic && - method.hasMustCallSuper && - method.returnType is VoidType && - method.name == 'dispose' && - method.parameters.isEmpty)) { + bool signature(MethodElement method) => + !method.isStatic && + method.hasMustCallSuper && + method.returnType is VoidType && + method.name == 'dispose' && + method.parameters.isEmpty; + + if (self != interface && interface.methods.any(signature)) { return true; } diff --git a/forui_internal_lints/lib/src/always_provide_flag_property_parameter.dart b/forui_internal_lints/lib/src/always_provide_flag_property_parameter.dart new file mode 100644 index 000000000..bfc6156c7 --- /dev/null +++ b/forui_internal_lints/lib/src/always_provide_flag_property_parameter.dart @@ -0,0 +1,37 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +const _code = LintCode( + name: 'always_provide_flag_property_parameter', + problemMessage: 'Provide `ifTrue` and/or `ifFalse` parameter', +); + +const _flagProperty = TypeChecker.fromName('FlagProperty', packageName: 'flutter'); + +/// A lint rule that ensures flag property provides `ifTrue` and/or `ifFalse` parameter. +class AlwaysProvideFlagPropertyArgument extends DartLintRule { + /// Creates a new [AlwaysProvideFlagPropertyArgument]. + const AlwaysProvideFlagPropertyArgument() : super(code: _code); + + @override + void run(CustomLintResolver resolver, ErrorReporter reporter, CustomLintContext context) { + context.registry.addInstanceCreationExpression((node) { + if (node.staticType case final type when type == null || !_flagProperty.isExactlyType(type)) { + return; + } + + if (node.argumentList.length < 2) { + return; + } + + if (node.argumentList.arguments + .whereType() + .any((expression) => expression.element?.name == 'ifTrue' || expression.element?.name == 'ifFalse')) { + return; + } + + reporter.atNode(node, _code); + }); + } +} diff --git a/forui_internal_lints/lib/src/diagnosticable_styles.dart b/forui_internal_lints/lib/src/diagnosticable_styles.dart deleted file mode 100644 index 5543e232f..000000000 --- a/forui_internal_lints/lib/src/diagnosticable_styles.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; - -const _code = LintCode( - name: 'diagnosticable_styles', - problemMessage: 'Style(s) should be assignable to Diagnosticable.', -); - -const _suffixes = {'Style', 'Styles'}; - -const _checker = TypeChecker.fromName('Diagnosticable', packageName: 'flutter'); - -/// A lint rule that checks if a class ending with 'Style' or 'Styles' is assignable to Diagnosticable. -class DiagnosticableStylesRule extends DartLintRule { - /// Creates a new [DiagnosticableStylesRule]. - const DiagnosticableStylesRule() : super(code: _code); - - @override - void run(CustomLintResolver resolver, ErrorReporter reporter, CustomLintContext context) { - context.registry.addClassDeclaration((node) { - final declared = node.declaredElement; - - if (declared?.name case final name? when _suffixes.every((s) => !name.endsWith(s))) { - return; - } - - if (declared case final declared? when declared.isConstructable && !_checker.isAssignableFrom(declared)) { - reporter.atElement( - declared, - LintCode( - name: 'diagnosticable_styles', - problemMessage: 'Style(s), ${declared.name}, should be assignable to Diagnosticable.', - ), - ); - } - }); - } -} diff --git a/forui_internal_lints/lib/src/style_api.dart b/forui_internal_lints/lib/src/style_api.dart new file mode 100644 index 000000000..4dd39133f --- /dev/null +++ b/forui_internal_lints/lib/src/style_api.dart @@ -0,0 +1,81 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +const _code = LintCode( + name: 'style_api', + problemMessage: 'Style(s) should conform to the Diagnosticable interface.', +); + +const _suffixes = {'Style', 'Styles'}; + +const _checker = TypeChecker.fromName('Diagnosticable', packageName: 'flutter'); + +/// A lint rule that checks if a class ending with 'Style' or 'Styles' conforms to the required interface. +class StyleApiRule extends DartLintRule { + /// Creates a new [StyleApiRule]. + const StyleApiRule() : super(code: _code); + + @override + void run(CustomLintResolver resolver, ErrorReporter reporter, CustomLintContext context) { + context.registry.addClassDeclaration((node) { + final declared = node.declaredElement; + if (declared == null) { + return; + } + + if (declared.name case final name when _suffixes.every((s) => !name.endsWith(s))) { + return; + } + + if (declared.isConstructable && !_checker.isAssignableFrom(declared)) { + reporter.atElement( + declared, + LintCode( + name: 'style_api', + problemMessage: '${declared.name}, should be assignable to Diagnosticable.', + ), + ); + } + + if (!declared.isConstructable) { + return; + } + + final copyWith = declared.getMethod('copyWith'); + if (copyWith == null || + copyWith.isStatic || + copyWith.returnType != declared.thisType || + copyWith.parameters.isEmpty) { + reporter.atElement( + declared, + LintCode( + name: 'style_api', + problemMessage: '${declared.name} does not provide a valid copyWith(...) method.', + ), + ); + } + + final equality = declared.getMethod('=='); + if (equality == null) { + reporter.atElement( + declared, + LintCode( + name: 'style_api', + problemMessage: '${declared.name} does not provide a valid == operator.', + ), + ); + } + + final hashCode = declared.getGetter('hashCode'); + if (hashCode == null) { + reporter.atElement( + declared, + LintCode( + name: 'style_api', + problemMessage: '${declared.name} does not provide a valid hashCode getter.', + ), + ); + } + }); + } +} diff --git a/forui_internal_lints/testbed/lib/src/always_provide_flag_property_parameter.dart b/forui_internal_lints/testbed/lib/src/always_provide_flag_property_parameter.dart new file mode 100644 index 000000000..827c011dc --- /dev/null +++ b/forui_internal_lints/testbed/lib/src/always_provide_flag_property_parameter.dart @@ -0,0 +1,12 @@ +import 'package:flutter/foundation.dart'; + +final goodBoth = FlagProperty('good', value: true, ifTrue: 'good', ifFalse: 'bad'); + +final goodTrue = FlagProperty('good', value: true, ifTrue: 'good'); + +final goodFalse = FlagProperty('good', value: false, ifFalse: 'bad'); + +// expect_lint: always_provide_flag_property_parameter +final bad = FlagProperty('bad', value: false); + + diff --git a/forui_internal_lints/testbed/lib/src/diagnosticable_styles.dart b/forui_internal_lints/testbed/lib/src/diagnosticable_styles.dart deleted file mode 100644 index 0a321ab0e..000000000 --- a/forui_internal_lints/testbed/lib/src/diagnosticable_styles.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:flutter/foundation.dart'; - -class FGood {} - -class FGoodStyle with Diagnosticable {} - -class FGoodStyles with Diagnosticable {} - -// expect_lint: diagnosticable_styles -class FBadStyle {} - -// expect_lint: diagnosticable_styles -class FBadStyles {} \ No newline at end of file diff --git a/forui_internal_lints/testbed/lib/src/style_api.dart b/forui_internal_lints/testbed/lib/src/style_api.dart new file mode 100644 index 000000000..a720866bd --- /dev/null +++ b/forui_internal_lints/testbed/lib/src/style_api.dart @@ -0,0 +1,96 @@ +import 'package:flutter/foundation.dart'; + +class FGood { +} + +class FGoodStyle with Diagnosticable { + FGoodStyle copyWith({required bool flag}) => this; + + @override + bool operator ==(Object other) => false; + + @override + int get hashCode => 0; +} + +class FGoodStyles with Diagnosticable { + FGoodStyles copyWith({required bool flag}) => this; + + @override + bool operator ==(Object other) => false; + + @override + int get hashCode => 0; +} + +// expect_lint: style_api +class FBadStyle { + FBadStyle copyWith({required bool flag}) => this; + + @override + bool operator ==(Object other) => false; + + @override + int get hashCode => 0; +} + +// expect_lint: style_api +class FBadStyles { + FBadStyles copyWith({required bool flag}) => this; + + @override + bool operator ==(Object other) => false; + + @override + int get hashCode => 0; +} + +// expect_lint: style_api +class FBadCopyWithStyle with Diagnosticable { + @override + bool operator ==(Object other) => false; + + @override + int get hashCode => 0; +} + +// expect_lint: style_api +class FBadCopyWithStyles with Diagnosticable { + @override + bool operator ==(Object other) => false; + + @override + int get hashCode => 0; +} + +// expect_lint: style_api +class FBadEqualityStyle with Diagnosticable { + FBadEqualityStyle copyWith({required bool flag}) => this; + + @override + int get hashCode => 0; +} + +// expect_lint: style_api +class FBadEqualityStyles with Diagnosticable { + FBadEqualityStyles copyWith({required bool flag}) => this; + + @override + int get hashCode => 0; +} + +// expect_lint: style_api +class FBadHashCodeStyle with Diagnosticable { + FBadHashCodeStyle copyWith({required bool flag}) => this; + + @override + bool operator ==(Object other) => false; +} + +// expect_lint: style_api +class FBadHashCodeStyles with Diagnosticable { + FBadHashCodeStyles copyWith({required bool flag}) => this; + + @override + bool operator ==(Object other) => false; +} \ No newline at end of file