From 9b70b6e71e2b37315b264f50b7819d001c65deae Mon Sep 17 00:00:00 2001 From: Eslam Shaaban <159810907+Islam-Shaaban-Ibrahim@users.noreply.github.com> Date: Wed, 15 Apr 2026 12:53:48 +0200 Subject: [PATCH 01/49] Migrate/avoid global state (#227) * Add analysis_server_plugin dependency * Migrate avoid_global_state rule and tests * applied code review suggestions * applied suggestions from code review * edited main documentation and changed avoid_global_state visitor naming --- lib/main.dart | 25 ++++++ lib/solid_lints.dart | 77 ------------------- .../avoid_global_state_rule.dart | 74 ++++++++---------- .../visitors/avoid_global_state_visitor.dart | 51 ++++++++++++ lint_test/.gitignore | 45 +++++++++++ lint_test/.metadata | 30 ++++++++ lint_test/README.md | 3 + lint_test/avoid_global_state_test.dart | 30 -------- lint_test/pubspec.yaml | 4 +- pubspec.yaml | 6 +- test/avoid_global_state_rule_test.dart | 76 ++++++++++++++++++ 11 files changed, 266 insertions(+), 155 deletions(-) create mode 100644 lib/main.dart delete mode 100644 lib/solid_lints.dart create mode 100644 lib/src/lints/avoid_global_state/visitors/avoid_global_state_visitor.dart create mode 100644 lint_test/.gitignore create mode 100644 lint_test/.metadata create mode 100644 lint_test/README.md delete mode 100644 lint_test/avoid_global_state_test.dart create mode 100644 test/avoid_global_state_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 00000000..707269e3 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,25 @@ +import 'package:analysis_server_plugin/plugin.dart'; +import 'package:analysis_server_plugin/registry.dart'; +import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; + +/// The entry point for the Solid Lints analyser server plugin. +/// +/// This plugin integrates custom lint rules into the Dart analysis server, +/// allowing them to run during static analysis. +final plugin = SolidLintsPlugin(); + +/// An analysis server plugin that provides Solid lint rules. +/// +/// This plugin registers custom lint rules and enables them to be executed +/// by the Dart analyzer during code analysis. +class SolidLintsPlugin extends Plugin { + @override + String get name => 'solid_lints'; + + @override + void register(PluginRegistry registry) { + registry.registerLintRule( + AvoidGlobalStateRule(), + ); + } +} diff --git a/lib/solid_lints.dart b/lib/solid_lints.dart deleted file mode 100644 index 4fc66f58..00000000 --- a/lib/solid_lints.dart +++ /dev/null @@ -1,77 +0,0 @@ -library solid_metrics; - -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; -import 'package:solid_lints/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart'; -import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; -import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart'; -import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; -import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; -import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart'; -import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart'; -import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; -import 'package:solid_lints/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart'; -import 'package:solid_lints/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart'; -import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart'; -import 'package:solid_lints/src/lints/avoid_using_api/avoid_using_api_rule.dart'; -import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; -import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; -import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart'; -import 'package:solid_lints/src/lints/member_ordering/member_ordering_rule.dart'; -import 'package:solid_lints/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart'; -import 'package:solid_lints/src/lints/newline_before_return/newline_before_return_rule.dart'; -import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; -import 'package:solid_lints/src/lints/no_equal_then_else/no_equal_then_else_rule.dart'; -import 'package:solid_lints/src/lints/no_magic_number/no_magic_number_rule.dart'; -import 'package:solid_lints/src/lints/number_of_parameters/number_of_parameters_rule.dart'; -import 'package:solid_lints/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart'; -import 'package:solid_lints/src/lints/prefer_early_return/prefer_early_return_rule.dart'; -import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; -import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; -import 'package:solid_lints/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart'; -import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; - -/// Creates a plugin for our custom linter -PluginBase createPlugin() => _SolidLints(); - -/// Initialize custom solid lints -class _SolidLints extends PluginBase { - @override - List getLintRules(CustomLintConfigs configs) { - final List supportedRules = [ - CyclomaticComplexityRule.createRule(configs), - NumberOfParametersRule.createRule(configs), - FunctionLinesOfCodeRule.createRule(configs), - AvoidNonNullAssertionRule.createRule(configs), - AvoidLateKeywordRule.createRule(configs), - AvoidGlobalStateRule.createRule(configs), - AvoidReturningWidgetsRule.createRule(configs), - DoubleLiteralFormatRule.createRule(configs), - AvoidUnnecessaryTypeAssertions.createRule(configs), - AvoidUnnecessarySetStateRule.createRule(configs), - AvoidUnnecessaryTypeCastsRule.createRule(configs), - AvoidUnrelatedTypeAssertionsRule.createRule(configs), - AvoidUnusedParametersRule.createRule(configs), - AvoidUsingApiRule.createRule(configs), - NewlineBeforeReturnRule.createRule(configs), - NoEmptyBlockRule.createRule(configs), - NoEqualThenElseRule.createRule(configs), - MemberOrderingRule.createRule(configs), - NoMagicNumberRule.createRule(configs), - PreferConditionalExpressionsRule.createRule(configs), - PreferFirstRule.createRule(configs), - PreferLastRule.createRule(configs), - PreferMatchFileNameRule.createRule(configs), - ProperSuperCallsRule.createRule(configs), - AvoidDebugPrintInReleaseRule.createRule(configs), - PreferEarlyReturnRule.createRule(configs), - AvoidFinalWithGetterRule.createRule(configs), - NamedParametersOrderingRule.createRule(configs), - AvoidUnnecessaryReturnVariableRule.createRule(configs), - ]; - - // Return only enabled rules - return supportedRules.where((r) => r.enabled).toList(); - } -} diff --git a/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart b/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart index f8211320..91a38cc4 100644 --- a/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart +++ b/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart @@ -1,8 +1,8 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_global_state/visitors/avoid_global_state_visitor.dart'; /// Avoid top-level and static mutable variables. /// @@ -23,7 +23,6 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// } /// ``` /// -/// /// #### GOOD: /// /// ```dart @@ -35,49 +34,36 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// static final int globalFinal = 1; /// } /// ``` -class AvoidGlobalStateRule extends SolidLintRule { - /// This lint rule represents - /// the error whether we use global state. - static const lintName = 'avoid_global_state'; +class AvoidGlobalStateRule extends AnalysisRule { + /// Lint name used for suppression and reporting. + static const String lintName = 'avoid_global_state'; - AvoidGlobalStateRule._(super.config); + /// Lint code used for suppression and reporting. + static const LintCode code = LintCode( + lintName, + 'Avoid variables that can be globally mutated.', + correctionMessage: + 'Prefer using final/const or a state management solution.', + ); - /// Creates a new instance of [AvoidGlobalStateRule] - /// based on the lint configuration. - factory AvoidGlobalStateRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (_) => 'Avoid variables that can be globally mutated.', - ); + /// Creates an instance of [AvoidGlobalStateRule]. + AvoidGlobalStateRule() + : super( + name: lintName, + description: 'Avoid top-level or static mutable variables ', + ); - return AvoidGlobalStateRule._(rule); - } + @override + LintCode get diagnosticCode => code; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addTopLevelVariableDeclaration( - (node) => node.variables.variables - .where((variable) => variable.isPublicMutable) - .forEach((node) => reporter.atNode(node, code)), - ); - context.registry.addFieldDeclaration((node) { - if (!node.isStatic) return; - node.fields.variables - .where((variable) => variable.isPublicMutable) - .forEach((node) => reporter.atNode(node, code)); - }); - } -} - -extension on VariableDeclaration { - bool get isMutable => !isFinal && !isConst; + final visitor = AvoidGlobalStateVisitor(this); - bool get isPrivate => declaredFragment?.element.isPrivate ?? false; - - bool get isPublicMutable => isMutable && !isPrivate; + registry.addTopLevelVariableDeclaration(this, visitor); + registry.addFieldDeclaration(this, visitor); + } } diff --git a/lib/src/lints/avoid_global_state/visitors/avoid_global_state_visitor.dart b/lib/src/lints/avoid_global_state/visitors/avoid_global_state_visitor.dart new file mode 100644 index 00000000..1f5a1876 --- /dev/null +++ b/lib/src/lints/avoid_global_state/visitors/avoid_global_state_visitor.dart @@ -0,0 +1,51 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; + +/// Visitor for [AvoidGlobalStateRule]. +class AvoidGlobalStateVisitor extends SimpleAstVisitor { + /// The rule this visitor is associated with. + final AvoidGlobalStateRule rule; + + /// Creates an instance of [AvoidGlobalStateVisitor]. + AvoidGlobalStateVisitor(this.rule); + + @override + void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { + for (final variable in node.variables.variables) { + if (_isPublicMutable(variable)) { + rule.reportAtNode(variable); + } + } + } + + @override + void visitFieldDeclaration(FieldDeclaration node) { + if (!node.isStatic) return; + + for (final variable in node.fields.variables) { + if (_isPublicMutable(variable)) { + rule.reportAtNode(variable); + } + } + } + + /// Returns true if the variable is mutable and not private. + bool _isPublicMutable(VariableDeclaration variable) { + return _isMutable(variable) && !_isPrivate(variable); + } + + /// A variable is mutable if it is not final or const. + bool _isMutable(VariableDeclaration variable) { + final parent = variable.parent; + return parent is VariableDeclarationList && + !parent.isFinal && + !parent.isConst; + } + + /// A variable is private if its element is private. + bool _isPrivate(VariableDeclaration variable) { + final element = variable.declaredFragment?.element; + return element?.isPrivate ?? false; + } +} diff --git a/lint_test/.gitignore b/lint_test/.gitignore new file mode 100644 index 00000000..3820a95c --- /dev/null +++ b/lint_test/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/lint_test/.metadata b/lint_test/.metadata new file mode 100644 index 00000000..0bb2266c --- /dev/null +++ b/lint_test/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "05db9689081f091050f01aed79f04dce0c750154" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + - platform: web + create_revision: 05db9689081f091050f01aed79f04dce0c750154 + base_revision: 05db9689081f091050f01aed79f04dce0c750154 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/lint_test/README.md b/lint_test/README.md new file mode 100644 index 00000000..9002c39f --- /dev/null +++ b/lint_test/README.md @@ -0,0 +1,3 @@ +# solid_lints_test + +A new Flutter project. diff --git a/lint_test/avoid_global_state_test.dart b/lint_test/avoid_global_state_test.dart deleted file mode 100644 index 37028d42..00000000 --- a/lint_test/avoid_global_state_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// ignore_for_file: type_annotate_public_apis, prefer_match_file_name, unused_local_variable - -/// Check global mutable variable fail -/// `avoid_global_state` - -// expect_lint: avoid_global_state -var globalMutable = 0; - -final globalFinal = 1; - -const globalConst = 1; - -class Test { - static final int globalFinal = 1; - - // expect_lint: avoid_global_state - static int globalMutable = 0; - - final int memberFinal = 1; - - int memberMutable = 0; - - void m() { - int localMutable = 0; - - final localFinal = 1; - - const localConst = 2; - } -} diff --git a/lint_test/pubspec.yaml b/lint_test/pubspec.yaml index cb37cd0b..10e64475 100644 --- a/lint_test/pubspec.yaml +++ b/lint_test/pubspec.yaml @@ -1,9 +1,9 @@ name: solid_lints_test -description: A starting point for Dart libraries or applications. +description: Test project for solid_lints rules publish_to: none environment: - sdk: '>=3.0.0 <4.0.0' + sdk: ">=3.5.0 <4.0.0" dependencies: flutter: diff --git a/pubspec.yaml b/pubspec.yaml index 80a1415e..a7c16536 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,9 +11,9 @@ environment: sdk: ">=3.5.0 <4.0.0" dependencies: - analyzer: ^8.4.0 + analyzer: ^10.0.1 collection: ^1.19.0 - custom_lint_builder: ^0.8.1 + analysis_server_plugin: ^0.3.3 glob: ^2.1.3 path: ^1.9.1 yaml: ^3.1.3 @@ -24,3 +24,5 @@ dependencies: dev_dependencies: args: ^2.6.0 + analyzer_testing: ^0.1.9 + test_reflective_loader: ^0.3.0 diff --git a/test/avoid_global_state_rule_test.dart b/test/avoid_global_state_rule_test.dart new file mode 100644 index 00000000..29616804 --- /dev/null +++ b/test/avoid_global_state_rule_test.dart @@ -0,0 +1,76 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidGlobalStateRuleTest); + }); +} + +@reflectiveTest +class AvoidGlobalStateRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = AvoidGlobalStateRule(); + super.setUp(); + } + + void test_reports_mutable_top_level_variable() async { + await assertDiagnostics( + r''' +var globalMutable = 0; +''', + [lint(4, 17)], + ); + } + + void test_reports_mutable_static_field() async { + await assertDiagnostics( + r''' +class Test { + static int staticMutable = 0; +} +''', + [lint(26, 17)], + ); + } + + void test_does_not_report_global_immutable_variables() async { + await assertNoDiagnostics(r''' +final globalFinal = 1; +const globalConst = 1; +'''); + } + + void test_does_not_report_global_private_variables() async { + await assertNoDiagnostics(r''' +var _privateTopLevel = 0; +'''); + } + + void test_does_not_report_class_level_immutable_variables() async { + await assertNoDiagnostics(r''' +class Test { + static final int staticFinal = 1; + static const int staticConst = 2; +} +'''); + } + + void test_does_not_report_class_level_private_variables() async { + await assertNoDiagnostics(r''' +class Test { + static int _staticPrivate = 0; +} +'''); + } + + void test_does_not_report_local_variables() async { + await assertNoDiagnostics(r''' +void m() { + int localMutable = 0; +} +'''); + } +} From 87f0bd59ce126599faa6e937462c0bfaf6816a48 Mon Sep 17 00:00:00 2001 From: Eslam Shaaban <159810907+Islam-Shaaban-Ibrahim@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:12:37 +0200 Subject: [PATCH 02/49] migrated avoid non null assertion rule and tests (#230) * migrated avoid non null assertion rule and tests * applied code review suggestions * applied code review suggestions regarding making lintCode private --- lib/main.dart | 4 + .../avoid_global_state_rule.dart | 4 +- .../avoid_non_null_assertion_rule.dart | 78 +++++++------------ .../avoid_non_null_assertion_visitor.dart | 41 ++++++++++ lint_test/avoid_non_null_assertion_test.dart | 25 ------ test/avoid_non_null_assertion_rule_test.dart | 59 ++++++++++++++ 6 files changed, 132 insertions(+), 79 deletions(-) create mode 100644 lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart delete mode 100644 lint_test/avoid_non_null_assertion_test.dart create mode 100644 test/avoid_non_null_assertion_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index 707269e3..4b1be247 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:analysis_server_plugin/plugin.dart'; import 'package:analysis_server_plugin/registry.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; +import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; /// The entry point for the Solid Lints analyser server plugin. /// @@ -21,5 +22,8 @@ class SolidLintsPlugin extends Plugin { registry.registerLintRule( AvoidGlobalStateRule(), ); + registry.registerLintRule( + AvoidNonNullAssertionRule(), + ); } } diff --git a/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart b/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart index 91a38cc4..6ee78241 100644 --- a/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart +++ b/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart @@ -39,7 +39,7 @@ class AvoidGlobalStateRule extends AnalysisRule { static const String lintName = 'avoid_global_state'; /// Lint code used for suppression and reporting. - static const LintCode code = LintCode( + static const LintCode _code = LintCode( lintName, 'Avoid variables that can be globally mutated.', correctionMessage: @@ -54,7 +54,7 @@ class AvoidGlobalStateRule extends AnalysisRule { ); @override - LintCode get diagnosticCode => code; + LintCode get diagnosticCode => _code; @override void registerNodeProcessors( diff --git a/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart b/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart index 2b27901d..ba98b75a 100644 --- a/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart +++ b/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart @@ -1,10 +1,8 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/ast/token.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart'; /// Rule which warns about usages of bang operator ("!") /// as it may result in unexpected runtime exceptions. @@ -37,56 +35,32 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// final map = {'key': 'value'}; /// map['key']!; /// ``` -class AvoidNonNullAssertionRule extends SolidLintRule { - /// This lint rule represents - /// the error whether we use bang operator. - static const lintName = 'avoid_non_null_assertion'; +class AvoidNonNullAssertionRule extends AnalysisRule { + /// Name of the lint + static const String lintName = 'avoid_non_null_assertion'; - AvoidNonNullAssertionRule._(super.config); + /// Lint code used for suppression and reporting + static const LintCode _code = LintCode( + lintName, + 'Avoid using the bang operator. It may result in runtime exceptions.', + ); - /// Creates a new instance of [AvoidNonNullAssertionRule] - /// based on the lint configuration. - factory AvoidNonNullAssertionRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (_) => 'Avoid using the bang operator. ' - 'It may result in runtime exceptions.', - ); + /// creates an instance of [AvoidNonNullAssertionRule] + AvoidNonNullAssertionRule() + : super( + name: lintName, + description: + 'Warns about usages of bang operator (!) except valid Map access.', + ); - return AvoidNonNullAssertionRule._(rule); - } + @override + LintCode get diagnosticCode => _code; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addPostfixExpression((node) { - if (node.operator.type != TokenType.BANG) { - return; - } - - // DCM's and Flutter's documentation treats "bang" as a valid way of - // accessing a Map. For compatibility it's excluded from this rule. - // See more: - // * https://dcm.dev/docs/rules/common/avoid-non-null-assertion - // * https://dart.dev/null-safety/understanding-null-safety#the-map-index-operator-is-nullable - final operand = node.operand; - if (operand is IndexExpression) { - final type = operand.target?.staticType; - final isInterface = type is InterfaceType; - final isMap = isInterface && - (type.isDartCoreMap || - type.allSupertypes.any((v) => v.isDartCoreMap)); - - if (isMap) { - return; - } - } - - reporter.atNode(node, code); - }); + registry.addPostfixExpression(this, AvoidNonNullAssertionVisitor(this)); } } diff --git a/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart b/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart new file mode 100644 index 00000000..1dd32ae2 --- /dev/null +++ b/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart @@ -0,0 +1,41 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; + +/// visitor for [AvoidNonNullAssertionRule] +class AvoidNonNullAssertionVisitor extends SimpleAstVisitor { + /// Rule associated with this visitor + final AvoidNonNullAssertionRule rule; + + /// Creates an instance of [AvoidNonNullAssertionVisitor] + AvoidNonNullAssertionVisitor(this.rule); + + @override + void visitPostfixExpression(PostfixExpression node) { + if (node.operator.type != TokenType.BANG) { + return; + } + + final operand = node.operand; + + if (operand is IndexExpression) { + final type = operand.target?.staticType; + + if (_isMap(type)) { + return; + } + } + + rule.reportAtNode(node); + } + + bool _isMap(DartType? type) { + if (type is! InterfaceType) { + return false; + } + + return type.isDartCoreMap || type.allSupertypes.any((v) => v.isDartCoreMap); + } +} diff --git a/lint_test/avoid_non_null_assertion_test.dart b/lint_test/avoid_non_null_assertion_test.dart deleted file mode 100644 index 2a33a7df..00000000 --- a/lint_test/avoid_non_null_assertion_test.dart +++ /dev/null @@ -1,25 +0,0 @@ -// ignore_for_file: avoid_global_state, prefer_match_file_name -// ignore_for_file: member_ordering - -/// Check "bang" operator fail -/// -/// `avoid_non_null_assertion` -class AvoidNonNullAssertion { - AvoidNonNullAssertion? object; - int? number; - - void test() { - // expect_lint: avoid_non_null_assertion - number!; - - // expect_lint: avoid_non_null_assertion - object!.number!; - - // expect_lint: avoid_non_null_assertion - object!.test(); - - // No lint on maps - final map = {'key': 'value'}; - map['key']!; - } -} diff --git a/test/avoid_non_null_assertion_rule_test.dart b/test/avoid_non_null_assertion_rule_test.dart new file mode 100644 index 00000000..8d054cda --- /dev/null +++ b/test/avoid_non_null_assertion_rule_test.dart @@ -0,0 +1,59 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidNonNullAssertionRuleTest); + }); +} + +@reflectiveTest +class AvoidNonNullAssertionRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = AvoidNonNullAssertionRule(); + super.setUp(); + } + + void test_reports_non_null_assertion_on_nullable_value() async { + await assertDiagnostics( + r''' +void m(int? number) { + final value = number!; +} +''', + [lint(38, 7)], + ); + } + + void test_reports_non_null_assertion_on_method_call() async { + await assertDiagnostics( + r''' +void m(Object? object) { + object!.toString(); +} +''', + [lint(27, 7)], + ); + } + + void test_does_not_report_map_access() async { + await assertNoDiagnostics(r''' +void m() { + final map = {'key': 'value'}; + map['key']!; +} +'''); + } + + void test_does_not_report_safe_null_check() async { + await assertNoDiagnostics(r''' +void m(int? number) { + if (number != null) { + final value = number; + } +} +'''); + } +} From 1119f5cd1994e5f6f42bf87cdcb7ced73dba26f1 Mon Sep 17 00:00:00 2001 From: Eslam Shaaban <159810907+Islam-Shaaban-Ibrahim@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:19:47 +0200 Subject: [PATCH 03/49] Migrate/avoid debug print in release mode (#228) * Add analysis_server_plugin dependency * Migrate avoid_global_state rule and tests * applied code review suggestions * applied code review suggestions * applied suggestions from code review * migrated avoid debug print in release and tests * edited main documentation and changed avoid_global_state visitor naming * fixed main.dart conflict * applied suggestions from review * refactored code according to suggestions from review * applied code review suggestions * applied latest changes according to code review * remove unnecessary comments in AvoidDebugPrintInReleaseVisitor --- lib/main.dart | 1 + .../avoid_debug_print_in_release_rule.dart | 196 +++--------------- .../avoid_debug_print_in_release_visitor.dart | 86 ++++++++ .../avoid_debug_print_in_release_test.dart | 58 ------ ...void_debug_print_in_release_rule_test.dart | 186 +++++++++++++++++ 5 files changed, 298 insertions(+), 229 deletions(-) create mode 100644 lib/src/lints/avoid_debug_print_in_release/visitors/avoid_debug_print_in_release_visitor.dart delete mode 100644 lint_test/avoid_debug_print_in_release_test/avoid_debug_print_in_release_test.dart create mode 100644 test/avoid_debug_print_in_release_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index 4b1be247..78ee2a4c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:analysis_server_plugin/plugin.dart'; import 'package:analysis_server_plugin/registry.dart'; +import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; diff --git a/lib/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart b/lib/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart index e9b52edb..a5d3d44b 100644 --- a/lib/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart +++ b/lib/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart @@ -1,10 +1,8 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/ast/syntactic_entity.dart'; -import 'package:analyzer/dart/ast/token.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_debug_print_in_release/visitors/avoid_debug_print_in_release_visitor.dart'; /// An `avoid_debug_print_in_release` rule which forbids calling or referencing /// debugPrint function from flutter/foundation in release mode. @@ -31,176 +29,32 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// ``` /// /// -class AvoidDebugPrintInReleaseRule extends SolidLintRule { - /// This lint rule represents - /// the error when debugPrint is called - static const lintName = 'avoid_debug_print_in_release'; - static const String _kReleaseModePath = - 'package:flutter/src/foundation/constants.dart'; - static const String _kReleaseModeName = 'kReleaseMode'; - static const _debugPrintPath = 'package:flutter/src/foundation/print.dart'; - static const _debugPrintName = 'debugPrint'; +class AvoidDebugPrintInReleaseRule extends AnalysisRule { + /// The name of the lint + static const String lintName = 'avoid_debug_print_in_release'; - AvoidDebugPrintInReleaseRule._(super.config); + /// Lint code used for suppression and reporting. + static const LintCode _code = LintCode( + lintName, + 'Avoid debugPrint in release mode.', + correctionMessage: 'Wrap in a kReleaseMode check or use a logging package.', + ); - /// Creates a new instance of [AvoidDebugPrintInReleaseRule] - /// based on the lint configuration. - factory AvoidDebugPrintInReleaseRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (_) => """ -Avoid using 'debugPrint' in release mode. Wrap -your `debugPrint` call in a `!kReleaseMode` check.""", - ); + /// Creates an instance of [AvoidDebugPrintInReleaseRule]. + AvoidDebugPrintInReleaseRule() + : super(name: lintName, description: 'Avoid debugPrint in release mode.'); - return AvoidDebugPrintInReleaseRule._(rule); - } + @override + LintCode get diagnosticCode => _code; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addFunctionExpressionInvocation( - (node) { - final func = node.function; - if (func is! Identifier) { - return; - } - - _checkIdentifier( - identifier: func, - node: node, - reporter: reporter, - ); - }, - ); - - // addFunctionReference does not get triggered. - // addVariableDeclaration and addAssignmentExpression - // are used as a workaround for simple cases - - context.registry.addVariableDeclaration((node) { - _handleVariableAssignmentDeclaration( - node: node, - reporter: reporter, - ); - }); - - context.registry.addAssignmentExpression((node) { - _handleVariableAssignmentDeclaration( - node: node, - reporter: reporter, - ); - }); - } - - /// Checks whether the function identifier satisfies conditions - void _checkIdentifier({ - required Identifier identifier, - required AstNode node, - required DiagnosticReporter reporter, - }) { - if (!_isDebugPrintNode(identifier)) { - return; - } - - final debugCheck = node.thisOrAncestorMatching( - (node) { - if (node is IfStatement) { - return _isNotReleaseCheck(node.expression); - } - - return false; - }, - ); - - if (debugCheck != null) { - return; - } - - reporter.atNode(node, code); - } - - /// Returns null if doesn't have right operand - SyntacticEntity? _getRightOperand(List entities) { - /// Example var t = 15; 15 is in 3d position - if (entities.length < 3) { - return null; - } - return entities[2]; - } - - /// Handles variable assignment and declaration - void _handleVariableAssignmentDeclaration({ - required AstNode node, - required DiagnosticReporter reporter, - }) { - final rightOperand = _getRightOperand(node.childEntities.toList()); - - if (rightOperand == null || rightOperand is! Identifier) { - return; - } - - _checkIdentifier( - identifier: rightOperand, - node: node, - reporter: reporter, - ); - } - - bool _isDebugPrintNode(Identifier node) { - final String name; - final String sourcePath; - switch (node) { - case PrefixedIdentifier(): - final prefix = node.prefix.name; - name = node.name.replaceAll('$prefix.', ''); - sourcePath = node.element?.library?.uri.toString() ?? ''; - case SimpleIdentifier(): - name = node.name; - sourcePath = node.element?.library?.uri.toString() ?? ''; - - default: - return false; - } - - return name == _debugPrintName && sourcePath == _debugPrintPath; - } - - bool _isNotReleaseCheck(Expression node) { - if (node.childEntities.toList() - case [ - final Token token, - final Identifier identifier, - ]) { - return token.type == TokenType.BANG && - _isReleaseModeIdentifier(identifier); - } - - return false; - } - - bool _isReleaseModeIdentifier(Identifier node) { - final String name; - final String sourcePath; - - switch (node) { - case PrefixedIdentifier(): - final prefix = node.prefix.name; - - name = node.name.replaceAll('$prefix.', ''); - sourcePath = node.element?.library?.uri.toString() ?? ''; - case SimpleIdentifier(): - name = node.name; - sourcePath = node.element?.library?.uri.toString() ?? ''; - default: - return false; - } - - return name == _kReleaseModeName && sourcePath == _kReleaseModePath; + final visitor = AvoidDebugPrintInReleaseVisitor(this); + registry.addMethodInvocation(this, visitor); + registry.addSimpleIdentifier(this, visitor); } } diff --git a/lib/src/lints/avoid_debug_print_in_release/visitors/avoid_debug_print_in_release_visitor.dart b/lib/src/lints/avoid_debug_print_in_release/visitors/avoid_debug_print_in_release_visitor.dart new file mode 100644 index 00000000..8b794a1b --- /dev/null +++ b/lib/src/lints/avoid_debug_print_in_release/visitors/avoid_debug_print_in_release_visitor.dart @@ -0,0 +1,86 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; + +/// Visitor for [AvoidDebugPrintInReleaseRule]. +class AvoidDebugPrintInReleaseVisitor extends SimpleAstVisitor { + /// The rule associated with this visitor. + final AvoidDebugPrintInReleaseRule rule; + static const _foundationUri = 'package:flutter/foundation.dart'; + static const _debugPrint = 'debugPrint'; + static const _kReleaseMode = 'kReleaseMode'; + static const _kDebugMode = 'kDebugMode'; + static const _callMethod = 'call'; + + /// Creates an instance of [AvoidDebugPrintInReleaseVisitor]. + AvoidDebugPrintInReleaseVisitor(this.rule); + + @override + void visitMethodInvocation(MethodInvocation node) { + final target = node.target; + final methodName = node.methodName; + + if (methodName.name == _callMethod && target is SimpleIdentifier) { + _check(node, target); + return; + } + + _check(node, methodName); + } + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + if (node.parent is! MethodInvocation) { + _check(node, node); + } + } + + void _check(AstNode node, SimpleIdentifier identifier) { + final element = identifier.element; + if (element == null) return; + + if (element.name == _debugPrint) { + final sourceUri = element.library?.uri.toString() ?? ''; + + final isFlutterFoundation = sourceUri.contains( + _foundationUri, + ); + + if (isFlutterFoundation) { + if (!_isWrappedInReleaseCheck(node)) { + rule.reportAtNode(identifier); + } + } + } + } + + bool _isWrappedInReleaseCheck(AstNode node) { + AstNode? parent = node.parent; + + while (parent != null) { + if (parent is IfStatement) { + final condition = parent.expression; + + if (_isNotReleaseModeCheck(condition) || _isDebugModeCheck(condition)) { + return true; + } + } + + parent = parent.parent; + } + + return false; + } + + bool _isNotReleaseModeCheck(Expression condition) { + return condition is PrefixExpression && + condition.operator.type == TokenType.BANG && + condition.operand is SimpleIdentifier && + (condition.operand as SimpleIdentifier).name == _kReleaseMode; + } + + bool _isDebugModeCheck(Expression condition) { + return condition is SimpleIdentifier && condition.name == _kDebugMode; + } +} diff --git a/lint_test/avoid_debug_print_in_release_test/avoid_debug_print_in_release_test.dart b/lint_test/avoid_debug_print_in_release_test/avoid_debug_print_in_release_test.dart deleted file mode 100644 index 9d6fa72a..00000000 --- a/lint_test/avoid_debug_print_in_release_test/avoid_debug_print_in_release_test.dart +++ /dev/null @@ -1,58 +0,0 @@ -// ignore_for_file: unused_local_variable - -import 'package:flutter/foundation.dart'; - -/// Test the avoid_debug_print_in_release -void avoidDebugPrintTest() { - // expect_lint: avoid_debug_print_in_release - debugPrint(''); - - // expect_lint: avoid_debug_print_in_release - final test = debugPrint; - - var test2; - - // expect_lint: avoid_debug_print_in_release - test2 = debugPrint; - - test.call('test'); - - // expect_lint: avoid_debug_print_in_release - final test3 = debugPrint(''); - - someOtherFunction(); - - if (!kReleaseMode) { - debugPrint(''); - - final test = debugPrint; - - var test2; - - test2 = debugPrint; - - test.call('test'); - - final test3 = debugPrint(''); - - someOtherFunction(); - - if (true) { - debugPrint(''); - - final test = debugPrint; - - var test2; - - test2 = debugPrint; - - test.call('test'); - - final test3 = debugPrint(''); - } - } -} - -void someOtherFunction() { - print('iii'); -} diff --git a/test/avoid_debug_print_in_release_rule_test.dart b/test/avoid_debug_print_in_release_rule_test.dart new file mode 100644 index 00000000..74c6554b --- /dev/null +++ b/test/avoid_debug_print_in_release_rule_test.dart @@ -0,0 +1,186 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidDebugPrintInReleaseRuleTest); + }); +} + +@reflectiveTest +class AvoidDebugPrintInReleaseRuleTest extends AnalysisRuleTest { + @override + void setUp() { + final flutter = newPackage('flutter'); + + flutter.addFile('lib/foundation.dart', r''' +const bool kReleaseMode = false; +const bool kDebugMode = true; +void debugPrint(String? message) {} +'''); + + flutter.addFile('lib/material.dart', r''' +export 'package:flutter/foundation.dart'; +'''); + + flutter.addFile('lib/cupertino.dart', r''' +export 'package:flutter/foundation.dart'; +'''); + + rule = AvoidDebugPrintInReleaseRule(); + + super.setUp(); + } + + @override + String get analysisRule => AvoidDebugPrintInReleaseRule.lintName; + + void test_reports_debug_print_with_package_import() async { + await assertDiagnostics( + r''' +import 'package:flutter/foundation.dart'; + +void test() { + debugPrint('This should be flagged'); +} +''', + [lint(59, 10)], + ); + } + + void test_reports_aliased_debug_print_from_package() async { + await assertDiagnostics( + r''' +import 'package:flutter/foundation.dart' as f; + +void test() { + f.debugPrint('This should be flagged'); +} +''', + [lint(66, 10)], + ); + } + + void test_reports_debug_print_as_callback() async { + await assertDiagnostics( + r''' +import 'package:flutter/foundation.dart'; + +void test() { + ['a'].forEach(debugPrint); +} +''', + [lint(73, 10)], + ); + } + + void test_reports_inside_kReleaseMode_guard() async { + await assertDiagnostics( + r''' +import 'package:flutter/foundation.dart'; + +void test() { + if (kReleaseMode) { + debugPrint('This should be flagged'); + } +} +''', + [lint(83, 10)], + ); + } + + void test_reports_inside_not_kDebugMode_guard() async { + await assertDiagnostics( + r''' +import 'package:flutter/foundation.dart'; + +void test() { + if (!kDebugMode) { + debugPrint('Should still be flagged'); + } +} +''', + [lint(82, 10)], + ); + } + + void test_does_not_report_inside_not_kReleaseMode() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/foundation.dart'; + +void test() { + if (!kReleaseMode) { + debugPrint('Safe'); + } +} +''', + ); + } + + void test_does_not_report_inside_kDebugMode() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/foundation.dart'; + +void test() { + if (kDebugMode) { + debugPrint('Safe'); + } +} +''', + ); + } + + void test_no_report_when_debugPrint_is_not_from_foundation() async { + await assertNoDiagnostics( + r''' +void debugPrint(String message) {} + +void test() { + debugPrint('Not a flutter call'); +} +''', + ); + } + + void test_reports_when_imported_via_material() async { + await assertDiagnostics( + r''' +import 'package:flutter/material.dart'; + +void test() { + debugPrint('Flagged via material'); +} +''', + [lint(57, 10)], + ); + } + + void test_reports_when_imported_via_cupertino() async { + await assertDiagnostics( + r''' +import 'package:flutter/cupertino.dart'; + +void test() { + debugPrint('Flagged via cupertino'); +} +''', + [lint(58, 10)], + ); + } + + void test_reports_debug_print_call_method() async { + await assertDiagnostics( + r''' +import 'package:flutter/foundation.dart'; + +void test() { + debugPrint.call('This should be flagged'); +} +''', + [lint(59, 10)], + ); + } +} From 04a01124ff00800b9237da2dc1c36f89fad916b6 Mon Sep 17 00:00:00 2001 From: Eslam Shaaban <159810907+Islam-Shaaban-Ibrahim@users.noreply.github.com> Date: Tue, 21 Apr 2026 08:36:54 +0200 Subject: [PATCH 04/49] Migrate/proper super calls (#229) * Migrate avoid_global_state rule and tests * applied code review suggestions * migrated avoid debug print in release and tests * applied suggestions from code review * edited main documentation and changed avoid_global_state visitor naming * migrated proper super calls rule and tests * registered ProperSuperCallsRule in the Plugin * applied code review suggestions * Fix tests structure * Remove unnecessary file --------- Co-authored-by: vova-beloded-solid --- lib/main.dart | 7 + .../proper_super_calls_rule.dart | 156 ++++------------ .../visitors/proper_super_calls_visitor.dart | 112 ++++++++++++ lint_test/proper_super_calls_test.dart | 82 --------- pubspec.yaml | 1 + ...void_debug_print_in_release_rule_test.dart | 0 .../avoid_global_state_rule_test.dart | 0 .../avoid_non_null_assertion_rule_test.dart | 0 .../proper_super_calls_rule_test.dart | 170 ++++++++++++++++++ 9 files changed, 325 insertions(+), 203 deletions(-) create mode 100644 lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart delete mode 100644 lint_test/proper_super_calls_test.dart rename test/{ => lints/avoid_debug_print_in_release}/avoid_debug_print_in_release_rule_test.dart (100%) rename test/{ => lints/avoid_global_state}/avoid_global_state_rule_test.dart (100%) rename test/{ => lints/avoid_non_null_assertion}/avoid_non_null_assertion_rule_test.dart (100%) create mode 100644 test/lints/proper_super_calls/proper_super_calls_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index 78ee2a4c..5657b7a1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:analysis_server_plugin/registry.dart'; import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; +import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; /// The entry point for the Solid Lints analyser server plugin. /// @@ -26,5 +27,11 @@ class SolidLintsPlugin extends Plugin { registry.registerLintRule( AvoidNonNullAssertionRule(), ); + registry.registerLintRule( + AvoidDebugPrintInReleaseRule(), + ); + registry.registerLintRule( + ProperSuperCallsRule(), + ); } } diff --git a/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart b/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart index 34df0dbc..fb82e8e8 100644 --- a/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart +++ b/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart @@ -1,8 +1,8 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart'; /// Ensures that `super` calls are made in the correct order for the following /// StatefulWidget methods: @@ -43,130 +43,44 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// super.dispose(); // OK /// } /// ``` -class ProperSuperCallsRule extends SolidLintRule { - /// This lint rule represents - /// the error whether the initState and dispose methods - /// are called in the incorrect order - static const lintName = 'proper_super_calls'; - static const _initState = 'initState'; - static const _dispose = 'dispose'; - static const _override = 'override'; - - /// This lint rule represents - /// the error whether super.initState() should be called first - static const _superInitStateCode = LintCode( - name: lintName, - problemMessage: "super.initState() should be first", +class ProperSuperCallsRule extends AnalysisRule { + /// Lint rule name. + static const String lintName = 'proper_super_calls'; + + /// Error code for invalid `super.initState()` placement. + static const LintCode superInitStateCode = LintCode( + lintName, + 'super.initState() should be first', ); - /// This lint rule represents - /// the error whether super.dispose() should be called last - static const _superDisposeCode = LintCode( - name: lintName, - problemMessage: "super.dispose() should be last", + /// Error code for invalid `super.dispose()` placement. + static const LintCode superDisposeCode = LintCode( + lintName, + 'super.dispose() should be last', ); - ProperSuperCallsRule._(super.config); - - /// Creates a new instance of [ProperSuperCallsRule] - /// based on the lint configuration. - factory ProperSuperCallsRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - name: lintName, - configs: configs, - problemMessage: (_) => 'Proper super calls issue', - ); + /// Creates an instance of [ProperSuperCallsRule]. + ProperSuperCallsRule() + : super( + name: lintName, + description: + 'Ensures proper ordering of Flutter lifecycle super calls.', + ); - return ProperSuperCallsRule._(rule); - } + @override + LintCode get diagnosticCode => superInitStateCode; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addMethodDeclaration( - (node) { - final methodName = node.name.toString(); - final body = node.body; - - if (methodName case _initState || _dispose - when body is BlockFunctionBody) { - final statements = body.block.statements; - - _checkSuperCalls( - node, - methodName, - statements, - reporter, - ); - } - }, + registry.addMethodDeclaration( + this, + ProperSuperCallsVisitor( + rule: this, + context: context, + ), ); } - - /// This method report an error whether `super.initState()` - /// or `super.dispose()` are called incorrect - void _checkSuperCalls( - MethodDeclaration node, - String methodName, - List statements, - DiagnosticReporter reporter, - ) { - final hasOverrideAnnotation = - node.metadata.any((annotation) => annotation.name.name == _override); - - if (!hasOverrideAnnotation) return; - if (methodName == _initState && !_isSuperInitStateCalledFirst(statements)) { - reporter.atNode( - node, - _superInitStateCode, - ); - } - if (methodName == _dispose && !_isSuperDisposeCalledLast(statements)) { - reporter.atNode( - node, - _superDisposeCode, - ); - } - } - - /// Returns `true` if `super.initState()` is called before other code in the - /// `initState` method, `false` otherwise. - bool _isSuperInitStateCalledFirst(List statements) { - if (statements.isEmpty) return false; - final firstStatement = statements.first; - - if (firstStatement is ExpressionStatement) { - final expression = firstStatement.expression; - - final isSuperInitStateCalledFirst = expression is MethodInvocation && - expression.target is SuperExpression && - expression.methodName.toString() == _initState; - - return isSuperInitStateCalledFirst; - } - - return false; - } - - /// Returns `true` if `super.dispose()` is called at the end of the `dispose` - /// method, `false` otherwise. - bool _isSuperDisposeCalledLast(List statements) { - if (statements.isEmpty) return false; - final lastStatement = statements.last; - - if (lastStatement is ExpressionStatement) { - final expression = lastStatement.expression; - - final lastStatementIsSuperDispose = expression is MethodInvocation && - expression.target is SuperExpression && - expression.methodName.toString() == _dispose; - - return lastStatementIsSuperDispose; - } - - return false; - } } diff --git a/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart b/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart new file mode 100644 index 00000000..6cbe4759 --- /dev/null +++ b/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart @@ -0,0 +1,112 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; + +/// Visitor for [ProperSuperCallsRule]. +class ProperSuperCallsVisitor extends SimpleAstVisitor { + /// The rule associated with this visitor. + final ProperSuperCallsRule rule; + + /// The context associated with this visitor. + final RuleContext context; + + static const _initState = 'initState'; + static const _dispose = 'dispose'; + static const _flutterStateClass = 'State'; + + /// Creates a new instance of [ProperSuperCallsVisitor]. + ProperSuperCallsVisitor({ + required this.rule, + required this.context, + }); + + @override + void visitMethodDeclaration(MethodDeclaration node) { + final methodName = node.name.lexeme; + final body = node.body; + + if ((methodName != _initState && methodName != _dispose) || + body is! BlockFunctionBody) { + return; + } + + if (!_overridesFlutterStateMethod(node)) { + return; + } + + final statements = body.block.statements; + final reporter = context.currentUnit?.diagnosticReporter; + + if (reporter == null) return; + + if (methodName == _initState && + !_isSuperCallFirst(statements, _initState)) { + reporter.atToken( + node.name, + ProperSuperCallsRule.superInitStateCode, + ); + } + + if (methodName == _dispose && !_isSuperCallLast(statements, _dispose)) { + reporter.atToken( + node.name, + ProperSuperCallsRule.superDisposeCode, + ); + } + } + + bool _overridesFlutterStateMethod(MethodDeclaration node) { + final classElement = node.declaredFragment?.element.enclosingElement; + + if (classElement is! ClassElement) { + return false; + } + + final methodName = node.name.lexeme; + + final supertype = classElement.supertype; + + if (supertype == null) { + return false; + } + + final isStateSubclass = _isStateSubclass(supertype); + + if (!isStateSubclass) { + return false; + } + + return methodName == _initState || methodName == _dispose; + } + + bool _isStateSubclass(InterfaceType supertype) { + final isStateSubclass = supertype.element.name == _flutterStateClass || + supertype.allSupertypes.any( + (t) => t.element.name == _flutterStateClass, + ); + return isStateSubclass; + } + + bool _isSuperCallFirst(List statements, String name) { + return statements.isNotEmpty && _isTargetSuperCall(statements.first, name); + } + + bool _isSuperCallLast(List statements, String name) { + return statements.isNotEmpty && _isTargetSuperCall(statements.last, name); + } + + bool _isTargetSuperCall(Statement statement, String name) { + if (statement is! ExpressionStatement) { + return false; + } + + final expression = statement.expression; + + return expression is MethodInvocation && + expression.target is SuperExpression && + expression.methodName.name == name; + } +} diff --git a/lint_test/proper_super_calls_test.dart b/lint_test/proper_super_calls_test.dart deleted file mode 100644 index fc6e4773..00000000 --- a/lint_test/proper_super_calls_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -// ignore_for_file: prefer_match_file_name, no_empty_block - -class Widget {} - -abstract class State { - build(); - dispose() {} - initState() {} -} - -abstract class StatefulWidget extends Widget { - State createState(); - - StatefulWidget(); -} - -/// Check "check super" keyword fail -/// -/// `proper_super_calls` -class ProperSuperCallsTest1 extends StatefulWidget { - @override - State createState() => _ProperSuperCallsTest1State(); - - ProperSuperCallsTest1(); -} - -class _ProperSuperCallsTest1State extends State { - @override - Widget build() { - return Widget(); - } - - // expect_lint: proper_super_calls - @override - void initState() { - print(''); - super.initState(); - } - - // expect_lint: proper_super_calls - @override - void dispose() { - super.dispose(); - print(''); - } -} - -class ProperSuperCallsTest2 extends StatefulWidget { - @override - State createState() => _ProperSuperCallsTest2State(); - - ProperSuperCallsTest2(); -} - -class _ProperSuperCallsTest2State extends State { - @override - Widget build() { - return Widget(); - } - - @override - void initState() { - super.initState(); - print(''); - } - - @override - void dispose() { - print(''); - super.dispose(); - } -} - -class MyClass { - dispose() {} - initState() {} -} - -abstract interface class Disposable { - /// Abstract methods should be omitted by `proper_super_calls` - void dispose(); -} diff --git a/pubspec.yaml b/pubspec.yaml index a7c16536..f85b1309 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,3 +26,4 @@ dev_dependencies: args: ^2.6.0 analyzer_testing: ^0.1.9 test_reflective_loader: ^0.3.0 + diff --git a/test/avoid_debug_print_in_release_rule_test.dart b/test/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart similarity index 100% rename from test/avoid_debug_print_in_release_rule_test.dart rename to test/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart diff --git a/test/avoid_global_state_rule_test.dart b/test/lints/avoid_global_state/avoid_global_state_rule_test.dart similarity index 100% rename from test/avoid_global_state_rule_test.dart rename to test/lints/avoid_global_state/avoid_global_state_rule_test.dart diff --git a/test/avoid_non_null_assertion_rule_test.dart b/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart similarity index 100% rename from test/avoid_non_null_assertion_rule_test.dart rename to test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart diff --git a/test/lints/proper_super_calls/proper_super_calls_rule_test.dart b/test/lints/proper_super_calls/proper_super_calls_rule_test.dart new file mode 100644 index 00000000..8969e73c --- /dev/null +++ b/test/lints/proper_super_calls/proper_super_calls_rule_test.dart @@ -0,0 +1,170 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(ProperSuperCallsRuleTest); + }); +} + +@reflectiveTest +class ProperSuperCallsRuleTest extends AnalysisRuleTest { + @override + void setUp() { + final flutter = newPackage('flutter'); + flutter.addFile( + 'lib/src/widgets/framework.dart', + r''' +abstract class StatefulWidget {} + +abstract class State { + void initState() {} + void dispose() {} +} +''', + ); + + rule = ProperSuperCallsRule(); + super.setUp(); + } + + @override + String get analysisRule => ProperSuperCallsRule.lintName; + + void test_initState_reports_when_super_not_first() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class MyWidgetState extends State { + @override + void initState() { + print('Bad'); + super.initState(); + } +} +''', + [lint(125, 9)], + ); + } + + void test_dispose_reports_when_super_not_last() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class MyWidgetState extends State { + @override + void dispose() { + super.dispose(); + print('Bad'); + } +} +''', + [lint(125, 7)], + ); + } + + void test_reports_even_without_explicit_override_annotation() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class MyWidgetState extends State { + void initState() { + print('Bad'); + super.initState(); + } +} +''', + [lint(113, 9)], + ); + } + + void test_reports_empty_body_missing_super() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class MyWidgetState extends State { + @override + void initState() {} +} +''', + [lint(125, 9)], + ); + } + + void test_reports_when_wrong_super_method_is_called() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class MyWidgetState extends State { + @override + void initState() { + super.dispose(); + print('Bad'); + } +} +''', + [lint(125, 9)], + ); + } + + void test_reports_in_deep_inheritance() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +abstract class BaseState extends State {} + +class MyWidgetState extends BaseState { + @override + void dispose() { + super.dispose(); + print('Bad'); + } +} +''', + [lint(172, 7)], + ); + } + + void test_no_report_for_correct_usage() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class MyWidgetState extends State { + @override + void initState() { + super.initState(); + print('Good'); + } + + @override + void dispose() { + print('Good'); + super.dispose(); + } +} +''', + ); + } + + void test_no_report_for_other_methods() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class MyWidgetState extends State { + void build() { + super.initState(); + } +} +''', + ); + } +} From 7a8bcfe1798b19c2fdaeed11590b01c9483f5788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tru=C8=99c=C4=83=20Daria=20Maria?= <115713732+Dariaa14@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:11:00 +0300 Subject: [PATCH 05/49] Migrated prefer_early_return and its tests (#233) * Updated prefer_early_return rule and its visitor to use AnalysisRule . * Added tests and fixed visitor for prefer early return * Added the suggested changes --- .../prefer_early_return_rule.dart | 55 +- .../visitors/prefer_early_return_visitor.dart | 12 +- .../prefer_early_return_rule_test.dart | 478 ++++++++++++++++++ 3 files changed, 511 insertions(+), 34 deletions(-) create mode 100644 test/lints/prefer_early_return/prefer_early_return_rule_test.dart diff --git a/lib/src/lints/prefer_early_return/prefer_early_return_rule.dart b/lib/src/lints/prefer_early_return/prefer_early_return_rule.dart index e4b54b18..4f420de0 100644 --- a/lib/src/lints/prefer_early_return/prefer_early_return_rule.dart +++ b/lib/src/lints/prefer_early_return/prefer_early_return_rule.dart @@ -1,8 +1,8 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/prefer_early_return/visitors/prefer_early_return_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// A rule which highlights `if` statements that span the entire body, /// and suggests replacing them with a reversed boolean check @@ -31,38 +31,35 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// c; /// } /// ``` -class PreferEarlyReturnRule extends SolidLintRule { - /// This lint rule represents the error if - /// 'if' statements should be reversed +class PreferEarlyReturnRule extends AnalysisRule { + /// The name of the lint static const String lintName = 'prefer_early_return'; - PreferEarlyReturnRule._(super.config); + /// The message shown when the lint is triggered + static const String lintMessage = 'Use reverse if to reduce nesting'; + + /// Lint code + static const LintCode _code = LintCode( + lintName, + lintMessage, + ); /// Creates a new instance of [PreferEarlyReturnRule] - /// based on the lint configuration. - factory PreferEarlyReturnRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (_) => "Use reverse if to reduce nesting", - ); + PreferEarlyReturnRule() + : super( + name: lintName, + description: lintMessage, + ); - return PreferEarlyReturnRule._(rule); - } + @override + LintCode get diagnosticCode => _code; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addBlockFunctionBody((node) { - final visitor = PreferEarlyReturnVisitor(); - node.accept(visitor); - - for (final element in visitor.nodes) { - reporter.atNode(element, code); - } - }); + final visitor = PreferEarlyReturnVisitor(this); + registry.addBlockFunctionBody(this, visitor); } } diff --git a/lib/src/lints/prefer_early_return/visitors/prefer_early_return_visitor.dart b/lib/src/lints/prefer_early_return/visitors/prefer_early_return_visitor.dart index 127de6e8..a8342ef5 100644 --- a/lib/src/lints/prefer_early_return/visitors/prefer_early_return_visitor.dart +++ b/lib/src/lints/prefer_early_return/visitors/prefer_early_return_visitor.dart @@ -1,14 +1,16 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/prefer_early_return/prefer_early_return_rule.dart'; import 'package:solid_lints/src/lints/prefer_early_return/visitors/return_statement_visitor.dart'; import 'package:solid_lints/src/lints/prefer_early_return/visitors/throw_expression_visitor.dart'; -/// The AST visitor that will collect all unnecessary if statements +/// Visitor for [PreferEarlyReturnRule]. class PreferEarlyReturnVisitor extends RecursiveAstVisitor { - final _nodes = []; + /// The rule associated with this visitor. + final PreferEarlyReturnRule rule; - /// All unnecessary if statements and conditional expressions. - Iterable get nodes => _nodes; + /// Creates an instance of [PreferEarlyReturnVisitor]. + PreferEarlyReturnVisitor(this.rule); @override void visitBlockFunctionBody(BlockFunctionBody node) { @@ -36,7 +38,7 @@ class PreferEarlyReturnVisitor extends RecursiveAstVisitor { if (_hasReturnStatement(node)) return; if (_hasThrowExpression(node)) return; - _nodes.add(node); + rule.reportAtNode(node); } // returns a list of if statements at the start of the function diff --git a/test/lints/prefer_early_return/prefer_early_return_rule_test.dart b/test/lints/prefer_early_return/prefer_early_return_rule_test.dart new file mode 100644 index 00000000..e39dae80 --- /dev/null +++ b/test/lints/prefer_early_return/prefer_early_return_rule_test.dart @@ -0,0 +1,478 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/prefer_early_return/prefer_early_return_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferEarlyReturnRuleTest); + }); +} + +@reflectiveTest +class PreferEarlyReturnRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = PreferEarlyReturnRule(); + super.setUp(); + } + + @override + String get analysisRule => PreferEarlyReturnRule.lintName; + + void test_reports_if_as_only_statement_in_function() async { + await assertDiagnostics( + r''' +void test(bool a) { + if (a) { + print('hello'); + } +} +''', + [lint(22, 32)], + ); + } + + void test_reports_if_with_return() async { + await assertDiagnostics( + r''' +void test(bool a) { + if (a) { + print('hello'); + } + + return; +} +''', + [lint(22, 32)], + ); + } + + void test_does_not_report_if_with_return_value() async { + await assertNoDiagnostics( + r''' +int test(bool a) { + if (a) { + print('hello'); + } + + return 1; +} +''', + ); + } + + void test_reports_nested_if_as_only_statement() async { + await assertDiagnostics( + r''' +void test(bool a, bool b) { + if (a) { + if (b) { + print('nested'); + } + } +} +''', + [ + lint(30, 54), + ], + ); + } + + void test_does_not_report_nested_if_with_return_value() async { + await assertNoDiagnostics( + r''' +int test(bool a, bool b) { + if (a) { + if (b) { + print('nested'); + } + } + + return 1; +} +''', + ); + } + + void test_reports_nested_3_if_as_only_statement() async { + await assertDiagnostics( + r''' +void test(bool a, bool b, bool c) { + if (a) { + if (b) { + if (c){ + print('nested'); + } + } + } +} +''', + [ + lint(38, 78), + ], + ); + } + + void test_reports_nested_3_with_return() async { + await assertDiagnostics( + r''' +void test(bool a, bool b, bool c) { + if (a) { + if (b) { + if (c){ + print('nested'); + } + } + } + return; +} +''', + [ + lint(38, 78), + ], + ); + } + + void test_does_not_report_if_else() async { + await assertNoDiagnostics( + r''' +void test(bool a) { + if (a) { + print('hello'); + } else { + print('hello'); + } +} +''', + ); + } + + void test_does_not_report_if_else_return() async { + await assertNoDiagnostics( + r''' +void test(bool a) { + if (a) { + print('hello'); + } else { + return; + } +} +''', + ); + } + + void test_does_not_report_nested_if_else() async { + await assertNoDiagnostics( + r''' +void test(bool a, bool b) { + if (a) { + if(b){ + print('hello'); + } + } else { + print('hello'); + } +} +''', + ); + } + + void test_reports_inner_if_else() async { + await assertDiagnostics( + r''' +void test(bool a, bool b) { + if (a) { + if(b){ + print('hello'); + } + else { + print('hello'); + } + } +} +''', + [ + lint(30, 90), + ], + ); + } + + void test_does_not_report_nested_3_with_else_1() async { + await assertNoDiagnostics( + r''' +void test(bool a, bool b, bool c) { + if (a) { + if (b) { + if (c){ + print('nested'); + } + } + } else{ + print('hello'); + } +} +''', + ); + } + + void test_reports_nested_3_with_else_2() async { + await assertDiagnostics( + r''' +void test(bool a, bool b, bool c) { + if (a) { + if (b) { + if (c) { + print('nested'); + } + } else { + print('nested'); + } + } +} +''', + [ + lint(38, 115), + ], + ); + } + + void test_reports_nested_3_with_else_3() async { + await assertDiagnostics( + r''' +void test(bool a, bool b, bool c) { + if (a) { + if (b) { + if (c) { + print('nested'); + } + else { + print('nested'); + } + } + } +} +''', + [ + lint(38, 126), + ], + ); + } + + void test_reports_2_sequencial_if() async { + await assertDiagnostics( + r''' +void test(bool a, bool b) { + if (a) return; + if (b) { + print('gello'); + } +} +''', + [ + lint(47, 32), + ], + ); + } + + void test_does_not_report_2_sequencial_if_with_return() async { + await assertNoDiagnostics( + r''' +void test(bool a, bool b) { + if (a) return; + if (b) return; + + return; +} +''', + ); + } + + void test_reports_2_sequencial_if_with_return_2() async { + await assertDiagnostics( + r''' +void test(bool a, bool b) { + if (a) return; + if (b) { + print('hello'); + } + + return; +} +''', + [ + lint(47, 32), + ], + ); + } + + void test_does_not_report_2_sequencial_with_following_statement() async { + await assertNoDiagnostics( + r''' +void test(bool a, bool b) { + if (a) return; + if (b) { + print('hello'); + } + + print('after'); +} +''', + ); + } + + void test_reports_2_sequencial_if_with_something() async { + await assertDiagnostics( + r''' +void test(bool a, bool b) { + if (a) { + print('hello'); + } + if (b) { + print('hello'); + } +} +''', + [ + lint(65, 32), + ], + ); + } + + void test_reports_3_sequencial_if_with_return() async { + await assertDiagnostics( + r''' +void test(bool a, bool b, bool c) { + if (a) return; + if (b) return; + if (c) { + print('hello'); + } + + return; +} +''', + [ + lint(72, 32), + ], + ); + } + + void test_reports_3_sequencial_if_with_something_2() async { + await assertDiagnostics( + r''' +void test(bool a, bool b, bool c) { + if (a) return; + if (b) { + print('hello'); + } + if (c) { + print('hello'); + } +} +''', + [ + lint(90, 32), + ], + ); + } + + void test_does_not_report_if_throw_with_return() async { + await assertNoDiagnostics( + r''' +void test(bool a) { + if (a) { + throw ''; + } + + return; +} +''', + ); + } + + void test_does_not_report_if_else_throw() async { + await assertNoDiagnostics( + r''' +void test(bool a) { + if (a) { + print('hello'); + } else { + throw ''; + } +} +''', + ); + } + + void test_reports_2_sequencial_if_throw() async { + await assertDiagnostics( + r''' +void test(bool a, bool b) { + if (a) throw ''; + if (b) { + print('hello'); + } +} +''', + [ + lint(49, 32), + ], + ); + } + + void test_reports_2_sequencial_if_throw_with_return() async { + await assertDiagnostics( + r''' +void test(bool a, bool b) { + if (a) throw ''; + if (b) { + print('hello'); + } + + return; +} +''', + [ + lint(49, 32), + ], + ); + } + + void test_reports_3_sequencial_if_throw_with_return() async { + await assertDiagnostics( + r''' +void test(bool a, bool b, bool c) { + if (a) throw ''; + if (b) throw ''; + if (c) { + print('hello'); + } + + return; +} +''', + [ + lint(76, 32), + ], + ); + } + + void test_reports_3_sequencial_if_throw_with_something() async { + await assertDiagnostics( + r''' +void test(bool a, bool b, bool c) { + if (a) throw ''; + if (b) { + print('hello'); + } + if (c) { + print('hello'); + } +} +''', + [ + lint(92, 32), + ], + ); + } +} From 20062025434c4b48dc30d89afedb70e85f5d170f Mon Sep 17 00:00:00 2001 From: daria-trusca-solid Date: Thu, 4 Jun 2026 16:44:16 +0300 Subject: [PATCH 06/49] Migrate avoid unnecessary return variable rule and its tests (#237) * Updated avoid_unnecessary_return_variable rule and its visitor to use AnalysisRule. * Updated tests for avoid_unnecessary_return_variable rule * Deleted previous test file for avoid_unnecessary_return_variable rule. * Updated ReturnVariableUsageVisitor description. * Updated avoid_unnecessary_return_variable visitor from recursive to simple visitor. * Address PR suggestions * Updated rule to private in the visitor. --- ...void_unnecessary_return_variable_rule.dart | 133 +++---------- ...d_unnecessary_return_variable_visitor.dart | 105 +++++------ .../return_variable_usage_visitor.dart | 77 ++++++++ ...void_unnecessary_return_variable_test.dart | 125 ------------- pubspec.yaml | 2 +- ...unnecessary_return_variable_rule_test.dart | 175 ++++++++++++++++++ 6 files changed, 330 insertions(+), 287 deletions(-) create mode 100644 lib/src/lints/avoid_unnecessary_return_variable/visitors/return_variable_usage_visitor.dart delete mode 100644 lint_test/avoid_unnecessary_return_variable_test.dart create mode 100644 test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart diff --git a/lib/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart b/lib/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart index 90cd9c7d..4068d342 100644 --- a/lib/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart +++ b/lib/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart @@ -1,11 +1,8 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/element/element.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/common/visitors/select_expression_identifiers_visitor.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/visitors/avoid_unnecessary_return_variable_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// An `avoid_unnecessary_return_variable` rule which forbids returning /// an immutable variable if it can be rewritten in return statement itself. @@ -32,109 +29,37 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// } /// ``` /// -class AvoidUnnecessaryReturnVariableRule extends SolidLintRule { - /// This lint rule represents the error - /// when unnecessary return variable statement is found - static const lintName = 'avoid_unnecessary_return_variable'; +class AvoidUnnecessaryReturnVariableRule extends AnalysisRule { + /// The name of the lint rule. + static const _lintName = 'avoid_unnecessary_return_variable'; - AvoidUnnecessaryReturnVariableRule._(super.config); + /// The message shown when the lint is triggered. + static const String _lintMessage = """ +Avoid creating unnecessary variable only for return. +Rewrite the variable evaluation into return statement instead."""; + + /// Lint code. + static const LintCode _code = LintCode( + _lintName, + _lintMessage, + ); /// Creates a new instance of [AvoidUnnecessaryReturnVariableRule] - /// based on the lint configuration. - factory AvoidUnnecessaryReturnVariableRule.createRule( - CustomLintConfigs configs, - ) { - final rule = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (_) => """ -Avoid creating unnecessary variable only for return. -Rewrite the variable evaluation into return statement instead.""", - ); + AvoidUnnecessaryReturnVariableRule() + : super( + name: _lintName, + description: _lintMessage, + ); - return AvoidUnnecessaryReturnVariableRule._(rule); - } + @override + LintCode get diagnosticCode => _code; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addReturnStatement( - (statement) { - _checkReturnStatement( - statement: statement, - reporter: reporter, - context: context, - ); - }, - ); - } - - void _checkReturnStatement({ - required ReturnStatement statement, - required DiagnosticReporter reporter, - required CustomLintContext context, - }) { - final expr = statement.expression; - if (expr is! SimpleIdentifier) return; - - final element = expr.element; - if (element is! LocalVariableElement) return; - final returnVariableElement = element; - - if (!returnVariableElement.isFinal && !returnVariableElement.isConst) { - return; - } - - final blockBody = statement.parent; - if (blockBody == null) return; - - final visitor = AvoidUnnecessaryReturnVariableVisitor( - returnVariableElement, - statement, - ); - blockBody.visitChildren(visitor); - - if (!visitor.hasBadStatementCount()) return; - - //it is 100% bad if return statement follows declaration - if (!visitor.foundTokensBetweenDeclarationAndReturn) { - reporter.atNode(statement, code); - return; - } - - final declaration = visitor.variableDeclaration; - - //check if immutable - final initializer = declaration?.initializer; - if (initializer == null) return; - - if (!_isExpressionImmutable(initializer)) return; - - reporter.atNode(statement, code); - } - - bool _isExpressionImmutable(Expression expr) { - final visitor = SelectExpressionIdentifiersVisitor(); - expr.accept(visitor); - - return visitor.identifiers.every(_isSimpleIdentifierImmutable); - } - - bool _isSimpleIdentifierImmutable(SimpleIdentifier identifier) { - switch (identifier.element) { - case final VariableElement variable: - return variable.isFinal || variable.isConst; - - case ClassElement _: - return true; - - case GetterElement(:final PropertyInducingElement variable): - return variable.isFinal || variable.isConst; - } - - return false; + final visitor = AvoidUnnecessaryReturnVariableVisitor(this); + registry.addReturnStatement(this, visitor); } } diff --git a/lib/src/lints/avoid_unnecessary_return_variable/visitors/avoid_unnecessary_return_variable_visitor.dart b/lib/src/lints/avoid_unnecessary_return_variable/visitors/avoid_unnecessary_return_variable_visitor.dart index bbc7455d..ebfad8e4 100644 --- a/lib/src/lints/avoid_unnecessary_return_variable/visitors/avoid_unnecessary_return_variable_visitor.dart +++ b/lib/src/lints/avoid_unnecessary_return_variable/visitors/avoid_unnecessary_return_variable_visitor.dart @@ -1,79 +1,70 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/element/element.dart'; -import 'package:collection/collection.dart'; +import 'package:solid_lints/src/common/visitors/select_expression_identifiers_visitor.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/visitors/return_variable_usage_visitor.dart'; -/// This visitor is searches all uses of a single local variable, -/// including variable declaration. -class AvoidUnnecessaryReturnVariableVisitor extends RecursiveAstVisitor { - /// The problem expects that exactly 1 mention of return variable. - /// VariableDeclarationStatement doesn't count when visiting SimpleIdentifier. - /// Any other amount of variable mentions implies that it is used somewhere - /// except return, so its existence is justified. - static const _badStatementCount = 1; +/// Visitor for [AvoidUnnecessaryReturnVariableRule]. +class AvoidUnnecessaryReturnVariableVisitor extends SimpleAstVisitor { + /// The rule associated with this visitor. + final AvoidUnnecessaryReturnVariableRule _rule; - final LocalVariableElement _returnVariableElement; + /// Creates a new instance of [AvoidUnnecessaryReturnVariableVisitor]. + AvoidUnnecessaryReturnVariableVisitor(this._rule); - bool _foundTokensBetweenDeclarationAndReturn = false; - VariableDeclaration? _variableDeclaration; - int _variableStatementCounter = 0; + @override + void visitReturnStatement(ReturnStatement node) { + final expr = node.expression; + if (expr is! SimpleIdentifier) return; - final ReturnStatement _returnStatement; + final element = expr.element; + if (element is! LocalVariableElement) return; - /// After visiting holds info about whether there are any tokens - /// between variable declaration and return statement - bool get foundTokensBetweenDeclarationAndReturn => - _foundTokensBetweenDeclarationAndReturn; + if (!element.isFinal && !element.isConst) return; - /// Returns statement of local variable declaration - VariableDeclaration? get variableDeclaration => _variableDeclaration; + final block = node.parent; + if (block == null) return; - /// Creates a new instance of [AvoidUnnecessaryReturnVariableVisitor]. - AvoidUnnecessaryReturnVariableVisitor( - this._returnVariableElement, - this._returnStatement, - ); + final returnVariableUsageVisitor = ReturnVariableUsageVisitor( + node, + element, + ); - /// Defines whether the variables is used in return statement only. - bool hasBadStatementCount() => - _variableStatementCounter == _badStatementCount; + block.visitChildren(returnVariableUsageVisitor); + if (!returnVariableUsageVisitor.hasBadStatementCount) return; - @override - void visitVariableDeclarationStatement(VariableDeclarationStatement node) { - if (_collectVariableDeclaration(node)) { - _checkTokensInBetween(node, _returnStatement); + //it is 100% bad if return statement follows declaration + if (!returnVariableUsageVisitor.foundTokensBetweenDeclarationAndReturn) { + _rule.reportAtNode(node); + return; } - super.visitVariableDeclarationStatement(node); - } - @override - void visitSimpleIdentifier(SimpleIdentifier node) { - if (node.element?.id == _returnVariableElement.id) { - _variableStatementCounter++; - } + final declaration = returnVariableUsageVisitor.variableDeclaration; - super.visitSimpleIdentifier(node); - } + //check if immutable + final initializer = declaration?.initializer; + if (initializer == null) return; - bool _collectVariableDeclaration(VariableDeclarationStatement node) { - final targetVariable = node.variables.variables.firstWhereOrNull( - (v) => v.declaredFragment?.element.id == _returnVariableElement.id, - ); - if (targetVariable == null) return false; + if (!_isExpressionImmutable(initializer)) return; - _variableDeclaration = targetVariable; - return true; + _rule.reportAtNode(node); } - void _checkTokensInBetween( - VariableDeclarationStatement variableDeclaration, - ReturnStatement returnStatement, - ) { - final tokenBeforeReturn = - _returnStatement.findPrevious(_returnStatement.beginToken); + bool _isExpressionImmutable(Expression expr) { + final visitor = SelectExpressionIdentifiersVisitor(); + expr.accept(visitor); - if (tokenBeforeReturn != variableDeclaration.endToken) { - _foundTokensBetweenDeclarationAndReturn = true; - } + return visitor.identifiers.every(_isSimpleIdentifierImmutable); + } + + bool _isSimpleIdentifierImmutable(SimpleIdentifier identifier) { + return switch (identifier.element) { + ClassElement _ => true, + final VariableElement variable => variable.isFinal || variable.isConst, + GetterElement(:final PropertyInducingElement variable) => + variable.isFinal || variable.isConst, + _ => false, + }; } } diff --git a/lib/src/lints/avoid_unnecessary_return_variable/visitors/return_variable_usage_visitor.dart b/lib/src/lints/avoid_unnecessary_return_variable/visitors/return_variable_usage_visitor.dart new file mode 100644 index 00000000..27702549 --- /dev/null +++ b/lib/src/lints/avoid_unnecessary_return_variable/visitors/return_variable_usage_visitor.dart @@ -0,0 +1,77 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:collection/collection.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart'; + +/// Visitor for [AvoidUnnecessaryReturnVariableRule] which checks whether the +/// return variable is used somewhere except return statement and +/// whether it is immutable. +class ReturnVariableUsageVisitor extends RecursiveAstVisitor { + /// The problem expects that exactly 1 mention of return variable. + /// VariableDeclarationStatement doesn't count when visiting SimpleIdentifier. + /// Any other amount of variable mentions implies that it is used somewhere + /// except return, so its existence is justified. + static const _badStatementCount = 1; + + final ReturnStatement _returnStatement; + final LocalVariableElement _returnVariableElement; + + /// After visiting holds the declaration of return variable + VariableDeclaration? variableDeclaration; + + /// After visiting holds info about whether there are any tokens + bool foundTokensBetweenDeclarationAndReturn = false; + + int _variableStatementCounter = 0; + + /// Defines whether the variables is used in return statement only. + bool get hasBadStatementCount => + _variableStatementCounter == _badStatementCount; + + /// Creates a new instance of [ReturnVariableUsageVisitor]. + ReturnVariableUsageVisitor( + this._returnStatement, + this._returnVariableElement, + ); + + @override + void visitVariableDeclarationStatement(VariableDeclarationStatement node) { + if (_collectVariableDeclaration(node)) { + _checkTokensInBetween(node, _returnStatement); + } + super.visitVariableDeclarationStatement(node); + } + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + if (node.element?.id == _returnVariableElement.id) { + _variableStatementCounter++; + } + + super.visitSimpleIdentifier(node); + } + + bool _collectVariableDeclaration(VariableDeclarationStatement node) { + final targetVariable = node.variables.variables.firstWhereOrNull( + (v) => v.declaredFragment?.element.id == _returnVariableElement.id, + ); + if (targetVariable == null) return false; + variableDeclaration = targetVariable; + + return true; + } + + void _checkTokensInBetween( + VariableDeclarationStatement variableDeclaration, + ReturnStatement returnStatement, + ) { + final tokenBeforeReturn = _returnStatement.findPrevious( + _returnStatement.beginToken, + ); + + if (tokenBeforeReturn != variableDeclaration.endToken) { + foundTokensBetweenDeclarationAndReturn = true; + } + } +} diff --git a/lint_test/avoid_unnecessary_return_variable_test.dart b/lint_test/avoid_unnecessary_return_variable_test.dart deleted file mode 100644 index 1e068180..00000000 --- a/lint_test/avoid_unnecessary_return_variable_test.dart +++ /dev/null @@ -1,125 +0,0 @@ -// ignore_for_file: unused_local_variable, newline_before_return, no_empty_block - -/// Test the avoid_unnecessary_return_variable. -/// Good code, trivial case. -int returnVarTestGoodTrivial() { - return 1; -} - -/// Test the avoid_unnecessary_return_variable. -/// Returning mutable variable should not trigger the lint. -int returnVarTestReturnMutable() { - var a = 1; - a++; - - return a; -} - -int returnVarTestReturnParameter(int param) { - return param; -} - -/// Test the avoid_unnecessary_return_variable. -/// Caching mutable variable value. -/// Unpredictable: may be useful to cache value -/// before operation can change it. -int returnVarTestCachedMutable() { - var a = 1; - final result = a; - _doNothing(); - - return result; -} - -/// Test the avoid_unnecessary_return_variable. -/// Caching mutable variable value, but return goes -/// right after declaration, which makes it bad. -int returnVarTestReturnFollowsDeclaration() { - var a = 1; - final result = a; - - //Some comment here - - //expect_lint: avoid_unnecessary_return_variable - return result; -} - -/// Test the avoid_unnecessary_return_variable. -/// Caching another method result. -/// Unpredictable: may be useful to cache value -/// before operation can change it. -int returnVarTestCachedAnotherMethodResult() { - var a = 1; - final result = _testValueEval(); - _doNothing(); - - return result; -} - -/// Test the avoid_unnecessary_return_variable. -/// Caching value of object's field. -/// Unpredictable: may be useful to cache value -/// before operation can change it. -int returnVarTestCachedObjectField() { - final obj = _TestClass(); - final result = obj.varField; - _doNothing(); - - return result; -} - -/// Test the avoid_unnecessary_return_variable. -/// Good: variable is created not only for return -/// but is used in following expressions as well. -int returnVarTestUsedVariable() { - var a = 1; - final result = 2; - a += result; - - return result; -} - -/// Test the avoid_unnecessary_return_variable. -/// Bad code, trivial example. -int returnVarTestBadTrivial() { - final result = 1; - - //expect_lint: avoid_unnecessary_return_variable - return result; -} - -/// Test the avoid_unnecessary_return_variable. -/// Bad code: result expression is immutable, -/// so can be written in return statement directly. -int returnVarTestBadImmutableExpression() { - const constLocal = 1; - final finalLocal = 1; - final testObj = _TestClass(); - final result = constLocal + - finalLocal + - 1 + //const literal - _TestClass.constValue + - _TestClass.finalValue + - testObj.finalField; - _doNothing(); - - //expect_lint: avoid_unnecessary_return_variable - return result; -} - -int _testValueEval() { - return 1; -} - -/// This method is a placeholder for unpredictable behaviour -/// which can potentially change any mutable variables -void _doNothing() {} - -//ignore: prefer_match_file_name -class _TestClass { - static const constValue = 1; - static final finalValue = 1; - //ignore: member_ordering - final finalField = 1; - var varField = 1; -} diff --git a/pubspec.yaml b/pubspec.yaml index f85b1309..70168359 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ documentation: https://solid-software.github.io/solid_lints/docs/intro topics: [lints, linter, lint, analysis, analyzer] environment: - sdk: ">=3.5.0 <4.0.0" + sdk: ">=3.9.0 <4.0.0" dependencies: analyzer: ^10.0.1 diff --git a/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart b/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart new file mode 100644 index 00000000..d2e42b16 --- /dev/null +++ b/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart @@ -0,0 +1,175 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidUnnecessaryReturnVariableTest); + }); +} + +@reflectiveTest +class AvoidUnnecessaryReturnVariableTest extends AnalysisRuleTest { + @override + void setUp() { + rule = AvoidUnnecessaryReturnVariableRule(); + super.setUp(); + } + + @override + String get analysisRule => rule.name; + + void test_does_not_report_if_return_good_trivial() async { + await assertNoDiagnostics(r''' +int returnVarTestTrivial() { + return 1; +} + '''); + } + + void test_does_not_report_if_return_is_mutable() async { + await assertNoDiagnostics(r''' +int returnVarTestMutable() { + var a = 1; + a++; + + return a; +} + '''); + } + + void test_does_not_report_if_returns_parameter() async { + await assertNoDiagnostics(r''' +int returnVarTestReturnParameter(int param) { + return param; +} + '''); + } + + void test_does_not_report_if_return_is_cached_mutable() async { + await assertNoDiagnostics(r''' +int returnVarTestCachedMutable() { + var a = 1; + final result = a; + _doNothing(); + + return result; +} + +void _doNothing() {} + '''); + } + + void test_reports_if_return_follows_declaration() async { + await assertDiagnostics( + r''' +int returnVarTestReturnFollowsDeclaration() { + var a = 1; + final result = a; + + //Some comment here + + return result; +} + ''', + [lint(105, 14)], + ); + } + + void test_does_not_report_if_return_is_cached_another_method_result() async { + await assertNoDiagnostics(r''' +int returnVarTestCachedAnotherMethodResult() { + var a = 1; + final result = _testValueEval(); + _doNothing(); + + return result; +} + +int _testValueEval() { + return 1; +} + +void _doNothing() {} +'''); + } + + void test_does_not_report_if_return_is_cached_object_field() async { + await assertNoDiagnostics(r''' +int returnVarTestCachedObjectField() { + final obj = _TestClass(); + final result = obj.varField; + _doNothing(); + + return result; +} + +class _TestClass { + static const constValue = 1; + static final finalValue = 1; + //ignore: member_ordering + final finalField = 1; + var varField = 1; +} + +void _doNothing() {} +'''); + } + + void test_does_not_report_if_return_used_variable() async { + await assertNoDiagnostics(r''' +int returnVarTestUsedVariable() { + var a = 1; + final result = 2; + a += result; + + return result; +} +'''); + } + + void test_reports_if_return_is_bad_trivial() async { + await assertDiagnostics( + r''' +int returnVarTestBadTrivial() { + final result = 1; + + return result; +} +''', + [lint(55, 14)], + ); + } + + void test_reports_if_return_is_bad_immutable_expression() async { + await assertDiagnostics( + r''' +int returnVarTestBadImmutableExpression() { + const constLocal = 1; + final finalLocal = 1; + final testObj = _TestClass(); + final result = constLocal + + finalLocal + + 1 + //const literal + _TestClass.constValue + + _TestClass.finalValue + + testObj.finalField; + _doNothing(); + + return result; +} + +class _TestClass { + static const constValue = 1; + static final finalValue = 1; + //ignore: member_ordering + final finalField = 1; + var varField = 1; +} + +void _doNothing() {} +''', + [lint(304, 14)], + ); + } +} From 5bcbbf055e10c21787738082441319b08a4d0b34 Mon Sep 17 00:00:00 2001 From: daria-trusca-solid Date: Thu, 4 Jun 2026 16:46:06 +0300 Subject: [PATCH 07/49] Migrate avoid unnecessary set state rule and its tests (#238) * Updated avoid_unnecessary_set_state rule and its visitor to use AnalysisRule. * Fixed previous commit. * Updated tests for avoid_unnecessary_set_state rule. * Delete previous test file for avoid_unnecessary_set_state rule * Updated the avoid_unnecessary_set_state visitor from recursive to simple. * Removed unnecessary set up from avoid_unnecessary_set_state rule tests * Update lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart Co-authored-by: danylo-safonov-solid <116712879+danylo-safonov-solid@users.noreply.github.com> * Address PR suggestions * Updated sdk requirements * Updated rule to private in visitor --------- Co-authored-by: danylo-safonov-solid <116712879+danylo-safonov-solid@users.noreply.github.com> --- .../avoid_unnecessary_set_state_rule.dart | 61 ++-- .../avoid_unnecessary_set_state_visitor.dart | 35 +- .../newline_before_return_visitor.dart | 10 +- .../avoid_unnecessary_setstate_test.dart | 108 ------ ...avoid_unnecessary_set_state_rule_test.dart | 330 ++++++++++++++++++ 5 files changed, 392 insertions(+), 152 deletions(-) delete mode 100644 lint_test/avoid_unnecessary_setstate_test.dart create mode 100644 test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart diff --git a/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart b/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart index 0773e580..afdf2fdf 100644 --- a/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart +++ b/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart @@ -1,8 +1,8 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/visitors/avoid_unnecessary_set_state_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// A rule which warns when setState is called inside initState, didUpdateWidget /// or build methods and when it's called from a sync method that is called @@ -55,37 +55,36 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// if (mounted) setState(() => foo = 'bar'); /// } /// ``` -class AvoidUnnecessarySetStateRule extends SolidLintRule { - /// The lint name of this lint rule that represents - /// the error whether we use setState in inappropriate way. - static const lintName = 'avoid_unnecessary_setstate'; +class AvoidUnnecessarySetStateRule extends AnalysisRule { + /// The name of the lint rule. + static const _lintName = 'avoid_unnecessary_setstate'; - AvoidUnnecessarySetStateRule._(super.config); + /// The message shown when the lint rule is triggered. + static const _lintMessage = 'Avoid calling unnecessary setState. ' + 'Consider changing the state directly.'; - /// Creates a new instance of [AvoidUnnecessarySetStateRule] - /// based on the lint configuration. - factory AvoidUnnecessarySetStateRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - name: lintName, - configs: configs, - problemMessage: (_) => 'Avoid calling unnecessary setState. ' - 'Consider changing the state directly.', - ); - return AvoidUnnecessarySetStateRule._(rule); - } + /// The lint code for this rule. + static const _code = LintCode( + _lintName, + _lintMessage, + ); + + /// Creates a new instance of [AvoidUnnecessarySetStateRule]. + AvoidUnnecessarySetStateRule() + : super( + name: _lintName, + description: _lintMessage, + ); + + @override + LintCode get diagnosticCode => _code; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addClassDeclaration((node) { - final visitor = AvoidUnnecessarySetStateVisitor(); - visitor.visitClassDeclaration(node); - for (final element in visitor.setStateInvocations) { - reporter.atNode(element, code); - } - }); + final visitor = AvoidUnnecessarySetStateVisitor(this); + registry.addClassDeclaration(this, visitor); } } diff --git a/lib/src/lints/avoid_unnecessary_setstate/visitors/avoid_unnecessary_set_state_visitor.dart b/lib/src/lints/avoid_unnecessary_setstate/visitors/avoid_unnecessary_set_state_visitor.dart index 5679d5e8..4b47d939 100644 --- a/lib/src/lints/avoid_unnecessary_setstate/visitors/avoid_unnecessary_set_state_visitor.dart +++ b/lib/src/lints/avoid_unnecessary_setstate/visitors/avoid_unnecessary_set_state_visitor.dart @@ -24,11 +24,12 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:collection/collection.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/visitors/avoid_unnecessary_set_state_method_visitor.dart'; import 'package:solid_lints/src/utils/types_utils.dart'; -/// AST visitor which checks if class is State, in case yes checks its methods -class AvoidUnnecessarySetStateVisitor extends RecursiveAstVisitor { +/// Visitor for [AvoidUnnecessarySetStateRule]. +class AvoidUnnecessarySetStateVisitor extends SimpleAstVisitor { static const _checkedMethods = [ 'initState', 'didUpdateWidget', @@ -36,10 +37,13 @@ class AvoidUnnecessarySetStateVisitor extends RecursiveAstVisitor { 'build', ]; + /// The rule associated with this visitor. + final AvoidUnnecessarySetStateRule _rule; + final _setStateInvocations = []; - /// Unnecessary setState invocations - Iterable get setStateInvocations => _setStateInvocations; + /// Creates an instance of [AvoidUnnecessarySetStateVisitor]. + AvoidUnnecessarySetStateVisitor(this._rule); @override void visitClassDeclaration(ClassDeclaration node) { @@ -50,9 +54,16 @@ class AvoidUnnecessarySetStateVisitor extends RecursiveAstVisitor { return; } - final methods = node.members.whereType(); - final classMethodsNames = - methods.map((declaration) => declaration.name.lexeme).toSet(); + final body = node.body; + + if (body is! BlockClassBody) { + return; + } + + final methods = body.members.whereType(); + final classMethodsNames = methods + .map((declaration) => declaration.name.lexeme) + .toSet(); final methodBodies = methods.map((declaration) => declaration.body).toSet(); final checkedMethods = methods.where(_isMethodChecked); @@ -83,6 +94,10 @@ class AvoidUnnecessarySetStateVisitor extends RecursiveAstVisitor { ) .toList(), ); + + for (final element in _setStateInvocations) { + _rule.reportAtNode(element); + } } } @@ -105,8 +120,10 @@ class AvoidUnnecessarySetStateVisitor extends RecursiveAstVisitor { return true; } - final visitor = - AvoidUnnecessarySetStateMethodVisitor(classMethodsNames, bodies); + final visitor = AvoidUnnecessarySetStateMethodVisitor( + classMethodsNames, + bodies, + ); declaration.visitChildren(visitor); final hasSetState = visitor.setStateInvocations.isNotEmpty; diff --git a/lib/src/lints/newline_before_return/visitors/newline_before_return_visitor.dart b/lib/src/lints/newline_before_return/visitors/newline_before_return_visitor.dart index cad8b50a..febdad5c 100644 --- a/lib/src/lints/newline_before_return/visitors/newline_before_return_visitor.dart +++ b/lib/src/lints/newline_before_return/visitors/newline_before_return_visitor.dart @@ -57,12 +57,14 @@ class NewLineBeforeReturnVisitor extends RecursiveAstVisitor { ReturnStatement node, LineInfo lineInfo, ) { - final previousTokenLineNumber = - lineInfo.getLocation(node.returnKeyword.previous!.end).lineNumber; + final previousTokenLineNumber = lineInfo + .getLocation(node.returnKeyword.previous!.end) + .lineNumber; final lastNotEmptyLineToken = _optimalToken(node.returnKeyword, lineInfo); - final tokenLineNumber = - lineInfo.getLocation(lastNotEmptyLineToken.offset).lineNumber; + final tokenLineNumber = lineInfo + .getLocation(lastNotEmptyLineToken.offset) + .lineNumber; return tokenLineNumber > previousTokenLineNumber + 1; } diff --git a/lint_test/avoid_unnecessary_setstate_test.dart b/lint_test/avoid_unnecessary_setstate_test.dart deleted file mode 100644 index b308515b..00000000 --- a/lint_test/avoid_unnecessary_setstate_test.dart +++ /dev/null @@ -1,108 +0,0 @@ -// MIT License -// -// Copyright (c) 2020-2021 Dart Code Checker team -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -// ignore_for_file: member_ordering, prefer_match_file_name - -import 'package:flutter/material.dart'; - -/// Check unnecessary setstate fail -/// `avoid_unnecessary_setstate` -class MyWidget extends StatefulWidget { - @override - _MyWidgetState createState() => _MyWidgetState(); -} - -class _MyWidgetState extends State { - String _myString = ''; - final bool _condition = true; - - @override - void initState() { - super.initState(); - - methodWithoutSetState(); - - // expect_lint: avoid_unnecessary_setstate - setState(() { - _myString = "Hello"; - }); - - if (_condition) { - // expect_lint: avoid_unnecessary_setstate - setState(() { - _myString = "Hello"; - }); - } - - // expect_lint: avoid_unnecessary_setstate - myStateUpdateMethod(); - } - - @override - void didUpdateWidget(MyWidget oldWidget) { - super.didUpdateWidget(oldWidget); - // expect_lint: avoid_unnecessary_setstate - setState(() { - _myString = "Hello"; - }); - } - - void myStateUpdateMethod() { - setState(() { - _myString = "Hello"; - }); - } - - void methodWithoutSetState() { - _myString = 'hey'; - } - - @override - Widget build(BuildContext context) { - // expect_lint: avoid_unnecessary_setstate - setState(() { - _myString = "Hello"; - }); - - if (_condition) { - // expect_lint: avoid_unnecessary_setstate - setState(() { - _myString = "Hello"; - }); - } - - // expect_lint: avoid_unnecessary_setstate - myStateUpdateMethod(); - - return ElevatedButton( - onPressed: myStateUpdateMethod, - onHover: (_) => methodWithoutSetState(), - onLongPress: () { - setState(() { - _myString = 'data'; - }); - }, - child: Text(_myString), - ); - } -} diff --git a/test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart b/test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart new file mode 100644 index 00000000..0e160ae0 --- /dev/null +++ b/test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart @@ -0,0 +1,330 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidUnnecessarySetStateRuleTest); + }); +} + +@reflectiveTest +class AvoidUnnecessarySetStateRuleTest extends AnalysisRuleTest { + @override + void setUp() { + final flutter = newPackage('flutter'); + + flutter.addFile( + 'lib/src/widgets/framework.dart', + r''' +abstract class Widget {} + +abstract class StatefulWidget extends Widget {} + +class BuildContext {} + +abstract class State { + void initState() {} + void didUpdateWidget(T oldWidget) {} + void setState(void Function() fn) {} + + Widget build(BuildContext context); +} + +class Text extends Widget { + final Object? data; + Text(this.data); +} + +class ElevatedButton extends Widget { + final Function()? onPressed; + final Function()? onLongPress; + final Widget? child; + + ElevatedButton({this.onPressed, this.onLongPress, this.child}); +} +''', + ); + + rule = AvoidUnnecessarySetStateRule(); + super.setUp(); + } + + @override + String get analysisRule => rule.name; + + void test_reports_set_state_in_init_state() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + + @override + void initState() { + super.initState(); + + setState(() { + _myString = "Hello"; + }); + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(194, 47)], + ); + } + + void test_reports_set_state_in_init_state_with_condition() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + final bool _condition = true; + + @override + void initState() { + super.initState(); + + if (_condition) { + setState(() { + _myString = "Hello"; + }); + } + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(250, 51)], + ); + } + + void test_reports_set_state_in_init_state_through_method() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + final bool _condition = true; + + @override + void initState() { + super.initState(); + + myStateUpdateMethod(); + } + + void myStateUpdateMethod() { + setState(() { + _myString = "Hello"; + }); + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(226, 21)], + ); + } + + void test_reports_set_state_in_did_update_widget() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + + @override + void didUpdateWidget(StatefulWidget oldWidget) { + super.didUpdateWidget(oldWidget); + setState(() { + _myString = "Hello"; + }); + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(238, 47)], + ); + } + + void test_reports_set_state_in_build_method() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + + @override + Widget build(BuildContext context) { + setState(() { + _myString = "Hello"; + }); + + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(188, 47)], + ); + } + + void test_reports_set_state_in_build_method_with_condition() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + final bool _condition = true; + + @override + Widget build(BuildContext context) { + if (_condition) { + setState(() { + _myString = "Hello"; + }); + } + + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(244, 51)], + ); + } + + void test_reports_set_state_in_build_method_through_method() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + + void myStateUpdateMethod() { + setState(() { + _myString = "Hello"; + }); + } + + @override + Widget build(BuildContext context) { + myStateUpdateMethod(); + + return ElevatedButton( + child: Text(_myString), + ); + } +} + ''', + [lint(277, 21)], + ); + } + + void test_does_not_report_set_state_in_button_on_pressed() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + + void myStateUpdateMethod() { + setState(() { + _myString = "Hello"; + }); + } + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: myStateUpdateMethod, + child: Text(_myString), + ); + } +} + ''', + ); + } + + void test_does_not_report_set_state_in_button_on_long_press() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class _MyWidgetState extends State { + String _myString = ''; + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onLongPress: () { + setState(() { + _myString = 'data'; + }); + }, + child: Text(_myString), + ); + } +} + ''', + ); + } +} From adfd58596f9196422e4842ced9f34aa170e5bec4 Mon Sep 17 00:00:00 2001 From: Eslam Shaaban <159810907+Islam-Shaaban-Ibrahim@users.noreply.github.com> Date: Mon, 8 Jun 2026 22:15:14 +0300 Subject: [PATCH 08/49] migrate prefer early return rule with tests (#232) * migrate prefer early return rule with tests * fix: remove flutter dependency to allow running reflective tests * test: migrate all old tests * feat: migrate old lint code improve readability when possible * refactor: extract variable for readability * style: formatting * test: remove unnecessary test file test: add nested 3 ifs test case --------- Co-authored-by: Andrew Bekhiet --- .../prefer_early_return_rule.dart | 17 +- .../visitors/prefer_early_return_visitor.dart | 55 ++-- lint_test/prefer_early_return_test.dart | 299 ------------------ pubspec.yaml | 3 - .../prefer_early_return_rule_test.dart | 16 + 5 files changed, 53 insertions(+), 337 deletions(-) delete mode 100644 lint_test/prefer_early_return_test.dart diff --git a/lib/src/lints/prefer_early_return/prefer_early_return_rule.dart b/lib/src/lints/prefer_early_return/prefer_early_return_rule.dart index 4f420de0..582378df 100644 --- a/lib/src/lints/prefer_early_return/prefer_early_return_rule.dart +++ b/lib/src/lints/prefer_early_return/prefer_early_return_rule.dart @@ -32,23 +32,20 @@ import 'package:solid_lints/src/lints/prefer_early_return/visitors/prefer_early_ /// } /// ``` class PreferEarlyReturnRule extends AnalysisRule { - /// The name of the lint + /// Lint name static const String lintName = 'prefer_early_return'; - /// The message shown when the lint is triggered - static const String lintMessage = 'Use reverse if to reduce nesting'; - /// Lint code static const LintCode _code = LintCode( lintName, - lintMessage, + "Use reverse if to reduce nesting", ); - /// Creates a new instance of [PreferEarlyReturnRule] + /// Creates an instance of [PreferEarlyReturnRule] PreferEarlyReturnRule() : super( name: lintName, - description: lintMessage, + description: 'Use reverse if to reduce nesting', ); @override @@ -59,7 +56,11 @@ class PreferEarlyReturnRule extends AnalysisRule { RuleVisitorRegistry registry, RuleContext context, ) { - final visitor = PreferEarlyReturnVisitor(this); + final visitor = PreferEarlyReturnVisitor( + rule: this, + context: context, + ); + registry.addBlockFunctionBody(this, visitor); } } diff --git a/lib/src/lints/prefer_early_return/visitors/prefer_early_return_visitor.dart b/lib/src/lints/prefer_early_return/visitors/prefer_early_return_visitor.dart index a8342ef5..eb30616c 100644 --- a/lib/src/lints/prefer_early_return/visitors/prefer_early_return_visitor.dart +++ b/lib/src/lints/prefer_early_return/visitors/prefer_early_return_visitor.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:solid_lints/src/lints/prefer_early_return/prefer_early_return_rule.dart'; @@ -6,11 +7,17 @@ import 'package:solid_lints/src/lints/prefer_early_return/visitors/throw_express /// Visitor for [PreferEarlyReturnRule]. class PreferEarlyReturnVisitor extends RecursiveAstVisitor { - /// The rule associated with this visitor. + /// The rule associated with the visitor. final PreferEarlyReturnRule rule; - /// Creates an instance of [PreferEarlyReturnVisitor]. - PreferEarlyReturnVisitor(this.rule); + /// The context associated with the visitor. + final RuleContext context; + + /// Constructor for [PreferEarlyReturnVisitor]. + PreferEarlyReturnVisitor({ + required this.rule, + required this.context, + }); @override void visitBlockFunctionBody(BlockFunctionBody node) { @@ -18,7 +25,9 @@ class PreferEarlyReturnVisitor extends RecursiveAstVisitor { if (node.block.statements.isEmpty) return; - final (ifStatements, nextStatement) = _getStartIfStatements(node); + final (ifStatements, nextStatement) = _getIfStatementsAndNextStatement( + node, + ); if (ifStatements.isEmpty) return; // limit visitor to only work with functions @@ -29,25 +38,24 @@ class PreferEarlyReturnVisitor extends RecursiveAstVisitor { if (!nextStatementIsEmptyReturn && !nextStatementIsNull) return; - _handleIfStatement(ifStatements.last); - } + final lastIf = ifStatements.last; - void _handleIfStatement(IfStatement node) { - if (_isElseIfStatement(node)) return; - if (_hasElseStatement(node)) return; - if (_hasReturnStatement(node)) return; - if (_hasThrowExpression(node)) return; + if (lastIf case IfStatement(elseStatement: Statement())) return; + if (_hasReturnStatement(lastIf) || _hasThrowExpression(lastIf)) return; - rule.reportAtNode(node); + context.currentUnit?.diagnosticReporter.atNode( + lastIf, + rule.diagnosticCode, + ); } -// returns a list of if statements at the start of the function -// and the next statement after it -// examples: -// [if, if, if, return] -> ([if, if, if], return) -// [if, if, if, _doSomething, return] -> ([if, if, if], _doSomething) -// [if, if, if] -> ([if, if, if], null) - (List, Statement?) _getStartIfStatements( + // returns a list of if statements at the start of the function + // and the next statement after it + // examples: + // [if, if, if, return] -> ([if, if, if], return) + // [if, if, if, _doSomething, return] -> ([if, if, if], _doSomething) + // [if, if, if] -> ([if, if, if], null) + (List, Statement?) _getIfStatementsAndNextStatement( BlockFunctionBody body, ) { final List ifStatements = []; @@ -58,15 +66,8 @@ class PreferEarlyReturnVisitor extends RecursiveAstVisitor { return (ifStatements, statement); } } - return (ifStatements, null); - } - bool _hasElseStatement(IfStatement node) { - return node.elseStatement != null; - } - - bool _isElseIfStatement(IfStatement node) { - return node.elseStatement != null && node.elseStatement is IfStatement; + return (ifStatements, null); } bool _hasReturnStatement(Statement node) { diff --git a/lint_test/prefer_early_return_test.dart b/lint_test/prefer_early_return_test.dart deleted file mode 100644 index d9108186..00000000 --- a/lint_test/prefer_early_return_test.dart +++ /dev/null @@ -1,299 +0,0 @@ -// ignore_for_file: dead_code, cyclomatic_complexity, no_equal_then_else, prefer_match_file_name - -// ignore: no_empty_block -void _doSomething() {} - -void oneIf() { - //expect_lint: prefer_early_return - if (true) { - _doSomething(); - } -} - -int oneIfWithReturnValue() { - //no lint - if (true) { - _doSomething(); - } - - return 1; -} - -void oneIfWithReturn() { - //expect_lint: prefer_early_return - if (true) { - _doSomething(); - } - - return; -} - -void nestedIf2() { - //expect_lint: prefer_early_return - if (true) { - if (true) { - _doSomething(); - } - } -} - -int nestedIf2WithReturnValue() { - //no lint - if (true) { - if (true) { - _doSomething(); - } - } - - return 1; -} - -void nestedIf3() { - //expect_lint: prefer_early_return - if (true) { - if (true) { - if (true) { - _doSomething(); - } - } - } -} - -void oneNestedIf2WithReturn() { - //expect_lint: prefer_early_return - if (true) { - if (true) { - if (true) { - _doSomething(); - } - } - } - - return; -} - -void oneIfElse() { - //no lint - if (true) { - _doSomething(); - } else { - _doSomething(); - } -} - -void oneIfElseReturn() { - //no lint - if (true) { - _doSomething(); - } else { - return; - } -} - -void twoIfElse() { - //no lint - if (true) { - if (true) { - _doSomething(); - } - } else { - _doSomething(); - } -} - -void twoIfElseInner() { - //expect_lint: prefer_early_return - if (true) { - if (true) { - _doSomething(); - } else { - _doSomething(); - } - } -} - -void threeIf() { - //expect_lint: prefer_early_return - if (true) { - if (true) { - if (true) { - _doSomething(); - } - } - } -} - -void threeIfElse1() { - //no lint - if (true) { - if (true) { - if (true) { - _doSomething(); - } - } - } else { - _doSomething(); - } -} - -void threeIfElse2() { - //expect_lint: prefer_early_return - if (true) { - if (true) { - if (true) { - _doSomething(); - } - } else { - _doSomething(); - } - } -} - -void threeIfElse3() { - //expect_lint: prefer_early_return - if (true) { - if (true) { - if (true) { - _doSomething(); - } else { - _doSomething(); - } - } - } -} - -void twoSeqentialIf() { - if (false) return; - //expect_lint: prefer_early_return - if (true) { - _doSomething(); - } -} - -void twoSeqentialIfReturn() { - //no lint - if (false) return; - if (true) return; - - return; -} - -void twoSeqentialIfReturn2() { - //no lint - if (false) return; - //expect_lint: prefer_early_return - if (true) { - _doSomething(); - } - - return; -} - -void twoSeqentialIfSomething() { - //no lint - if (false) return; - //no lint - if (true) { - _doSomething(); - } - - _doSomething(); -} - -void twoSeqentialIfSomething2() { - //no lint - if (true) { - _doSomething(); - } - //expect_lint: prefer_early_return - if (true) { - _doSomething(); - } -} - -void threeSeqentialIfReturn() { - //no lint - if (false) return; - if (true) return; - //expect_lint: prefer_early_return - if (true) { - _doSomething(); - } - - return; -} - -void threeSeqentialIfReturn2() { - //no lint - if (false) return; - //no lint - if (true) { - _doSomething(); - } - //expect_lint: prefer_early_return - if (true) { - _doSomething(); - } -} - -void oneIfWithThrowWithReturn() { - //no lint - if (true) { - throw ''; - } - - return; -} - -void oneIfElseWithThrowReturn() { - //no lint - if (true) { - _doSomething(); - } else { - throw ''; - } -} - -void twoSeqentialIfWithThrow() { - if (false) throw ''; - //expect_lint: prefer_early_return - if (true) { - _doSomething(); - } -} - -void twoSeqentialIfWithThrowReturn2() { - //no lint - if (false) throw ''; - //expect_lint: prefer_early_return - if (true) { - _doSomething(); - } - - return; -} - -void threeSeqentialIfWithThrowReturn() { - //no lint - if (false) throw ''; - if (true) throw ''; - //expect_lint: prefer_early_return - if (true) { - _doSomething(); - } - - return; -} - -void threeSeqentialIfWithThrowReturn2() { - //no lint - if (false) throw ''; - //no lint - if (true) { - _doSomething(); - } - //expect_lint: prefer_early_return - if (true) { - _doSomething(); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 70168359..df5633f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,9 +17,6 @@ dependencies: glob: ^2.1.3 path: ^1.9.1 yaml: ^3.1.3 - # These packages are required for pana analysis to run correctly - flutter: - sdk: flutter test: ^1.25.14 dev_dependencies: diff --git a/test/lints/prefer_early_return/prefer_early_return_rule_test.dart b/test/lints/prefer_early_return/prefer_early_return_rule_test.dart index e39dae80..2e52493a 100644 --- a/test/lints/prefer_early_return/prefer_early_return_rule_test.dart +++ b/test/lints/prefer_early_return/prefer_early_return_rule_test.dart @@ -197,6 +197,22 @@ void test(bool a, bool b) { ); } + void test_reports_on_three_if() async { + await assertDiagnostics( + r''' +void threeIf(bool a, bool b, bool c) { + if (a) { + if (b) { + if (c) { + print('hello'); + } + } + } +}''', + [lint(41, 74)], + ); + } + void test_does_not_report_nested_3_with_else_1() async { await assertNoDiagnostics( r''' From be820c7b0223a64154b7f9ea623bc93e7e259c28 Mon Sep 17 00:00:00 2001 From: daria-trusca-solid Date: Wed, 10 Jun 2026 16:12:55 +0300 Subject: [PATCH 09/49] Migrate avoid unrelated type assertions and its tests (#239) * Updated avoid_unrelated_type_assertions and its visitor to use AnalysisRule. * Updated tests for avoid_unrelated_type_assertions rule * Delete previous test file for avoid_unrelated_type_assertions * Update variable modifiers to private * Updated avoid_unrelared_type_assertions visitor from recursive to simple --- .../avoid_unrelated_type_assertions_rule.dart | 67 ++++---- ...oid_unrelated_type_assertions_visitor.dart | 39 +++-- .../avoid_unrelated_type_assertions_test.dart | 49 ------ ...d_unrelated_type_assertions_rule_test.dart | 148 ++++++++++++++++++ 4 files changed, 201 insertions(+), 102 deletions(-) delete mode 100644 lint_test/avoid_unrelated_type_assertions_test.dart create mode 100644 test/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart diff --git a/lib/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart b/lib/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart index 0b63cc37..6bb9c2d3 100644 --- a/lib/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart +++ b/lib/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart @@ -1,50 +1,41 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/avoid_unrelated_type_assertions/visitors/avoid_unrelated_type_assertions_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// A `avoid_unrelated_type_assertions` rule which /// warns about unnecessary usage of `as` operator -class AvoidUnrelatedTypeAssertionsRule extends SolidLintRule { - /// This lint rule represents - /// the error whether we use bad formatted double literals. - static const lintName = 'avoid_unrelated_type_assertions'; +class AvoidUnrelatedTypeAssertionsRule extends AnalysisRule { + /// The name of the lint rule. + static const _lintName = 'avoid_unrelated_type_assertions'; - AvoidUnrelatedTypeAssertionsRule._(super.config); + /// The message shown when the lint rule is triggered. + static const _lintMessage = + 'Avoid unrelated "is" assertion. The result is always "{0}".'; - /// Creates a new instance of [AvoidUnrelatedTypeAssertionsRule] - /// based on the lint configuration. - factory AvoidUnrelatedTypeAssertionsRule.createRule( - CustomLintConfigs configs, - ) { - final rule = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (_) => - 'Avoid unrelated "is" assertion. The result is always "{0}".', - ); + /// Lint code for this rule. + static const LintCode _code = LintCode( + _lintName, + _lintMessage, + ); - return AvoidUnrelatedTypeAssertionsRule._(rule); - } + /// Creates a new instance of [AvoidUnrelatedTypeAssertionsRule]. + AvoidUnrelatedTypeAssertionsRule() + : super( + name: _lintName, + description: _lintMessage, + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, - ) { - context.registry.addIsExpression((node) { - final visitor = AvoidUnrelatedTypeAssertionsVisitor(); - visitor.visitIsExpression(node); + LintCode get diagnosticCode => _code; - for (final element in visitor.expressions.entries) { - reporter.atNode( - element.key, - code, - arguments: [element.value.toString()], - ); - } - }); + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + final visitor = AvoidUnrelatedTypeAssertionsVisitor(this); + registry.addIsExpression(this, visitor); } } diff --git a/lib/src/lints/avoid_unrelated_type_assertions/visitors/avoid_unrelated_type_assertions_visitor.dart b/lib/src/lints/avoid_unrelated_type_assertions/visitors/avoid_unrelated_type_assertions_visitor.dart index e0b4aa6a..2f0e33e3 100644 --- a/lib/src/lints/avoid_unrelated_type_assertions/visitors/avoid_unrelated_type_assertions_visitor.dart +++ b/lib/src/lints/avoid_unrelated_type_assertions/visitors/avoid_unrelated_type_assertions_visitor.dart @@ -26,14 +26,15 @@ import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:collection/collection.dart'; +import 'package:solid_lints/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart'; -/// AST Visitor which finds all is expressions and checks if they are -/// unrelated (result always false) -class AvoidUnrelatedTypeAssertionsVisitor extends RecursiveAstVisitor { - final _expressions = {}; +/// Visitor for [AvoidUnrelatedTypeAssertionsRule]. +class AvoidUnrelatedTypeAssertionsVisitor extends SimpleAstVisitor { + /// The rule associated with this visitor. + final AvoidUnrelatedTypeAssertionsRule _rule; - /// Map of unrelated type checks and their results - Map get expressions => _expressions; + /// Creates an instance of [AvoidUnrelatedTypeAssertionsVisitor]. + AvoidUnrelatedTypeAssertionsVisitor(this._rule); @override void visitIsExpression(IsExpression node) { @@ -47,7 +48,10 @@ class AvoidUnrelatedTypeAssertionsVisitor extends RecursiveAstVisitor { final objectType = node.expression.staticType; if (_isUnrelatedTypeCheck(objectType, castedType)) { - _expressions[node] = node.notOperator != null; + _rule.reportAtNode( + node, + arguments: [if (node.notOperator != null) 'true' else 'false'], + ); } } @@ -64,10 +68,14 @@ class AvoidUnrelatedTypeAssertionsVisitor extends RecursiveAstVisitor { return false; } - final objectCastedType = - _foundCastedTypeInObjectTypeHierarchy(objectType, castedType); - final castedObjectType = - _foundCastedTypeInObjectTypeHierarchy(castedType, objectType); + final objectCastedType = _foundCastedTypeInObjectTypeHierarchy( + objectType, + castedType, + ); + final castedObjectType = _foundCastedTypeInObjectTypeHierarchy( + castedType, + objectType, + ); if (objectCastedType == null && castedObjectType == null) { return true; } @@ -94,8 +102,8 @@ class AvoidUnrelatedTypeAssertionsVisitor extends RecursiveAstVisitor { final correctObjectType = objectType is InterfaceType && objectType.isDartAsyncFutureOr - ? objectType.typeArguments.first - : objectType; + ? objectType.typeArguments.first + : objectType; if ((correctObjectType.element == castedType.element) || castedType is DynamicType || @@ -105,8 +113,9 @@ class AvoidUnrelatedTypeAssertionsVisitor extends RecursiveAstVisitor { } if (correctObjectType is InterfaceType) { - return correctObjectType.allSupertypes - .firstWhereOrNull((value) => value.element == castedType.element); + return correctObjectType.allSupertypes.firstWhereOrNull( + (value) => value.element == castedType.element, + ); } return null; diff --git a/lint_test/avoid_unrelated_type_assertions_test.dart b/lint_test/avoid_unrelated_type_assertions_test.dart deleted file mode 100644 index 209a5615..00000000 --- a/lint_test/avoid_unrelated_type_assertions_test.dart +++ /dev/null @@ -1,49 +0,0 @@ -// ignore_for_file: prefer_const_declarations, prefer_match_file_name, unused_element -// ignore_for_file: unnecessary_nullable_for_final_variable_declarations -// ignore_for_file: unused_local_variable - -/// Check the `avoid_unrelated_type_assertions` rule -class Foo {} - -class Bar {} - -class ChildFoo extends Foo {} - -void fun() { - final testString = ''; - final testList = [1, 2, 3]; - final testMap = {'A': 'B'}; - final Foo foo = Foo(); - final childFoo = ChildFoo(); - - // expect_lint: avoid_unrelated_type_assertions - final result = testString is int; - - // expect_lint: avoid_unrelated_type_assertions - final result2 = testList is List; - - // expect_lint: avoid_unrelated_type_assertions - final result3 = foo is Bar; - - // expect_lint: avoid_unrelated_type_assertions - final result4 = childFoo is Bar; - - // expect_lint: avoid_unrelated_type_assertions - final result5 = testMap['A'] is double; -} - -class _A {} - -class _B extends _A {} - -class _C {} - -void lint() { - final _A a = _B(); - // Always false - // expect_lint: avoid_unrelated_type_assertions - if (a is _C) return; - // Always true - // expect_lint: avoid_unrelated_type_assertions - if (a is! _C) return; -} diff --git a/test/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart b/test/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart new file mode 100644 index 00000000..a2260724 --- /dev/null +++ b/test/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart @@ -0,0 +1,148 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidUnrelatedTypeAssertionsRuleTest); + }); +} + +@reflectiveTest +class AvoidUnrelatedTypeAssertionsRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = AvoidUnrelatedTypeAssertionsRule(); + super.setUp(); + } + + @override + String get analysisRule => rule.name; + + void test_reports_unrelated_string_is_int() async { + await assertDiagnostics( + r''' +void fun() { + final testString = ''; + + final result = testString is int; +} +''', + [ + lint(56, 17, messageContainsAll: ['false']), + ], + ); + } + + void test_reports_unrelated_int_list_is_string_list() async { + await assertDiagnostics( + r''' +void fun() { + final testList = [1, 2, 3]; + + final result = testList is List; +} +''', + [ + lint(61, 24, messageContainsAll: ['false']), + ], + ); + } + + void test_reports_unrelated_string_map_is_double_map() async { + await assertDiagnostics( + r''' +void fun() { + final testMap = {'A': 'B'}; + + final result = testMap['A'] is double; +} +''', + [ + lint(61, 22, messageContainsAll: ['false']), + ], + ); + } + + void test_reports_unrelated_class_is_another_class() async { + await assertDiagnostics( + r''' +class Foo {} + +class Bar {} + +void fun() { + final Foo foo = Foo(); + + final result = foo is Bar; +} +''', + [ + lint(84, 10, messageContainsAll: ['false']), + ], + ); + } + + void test_reports_unrelated_child_class_is_another_class() async { + await assertDiagnostics( + r''' +class Foo {} + +class Bar {} + +class ChildFoo extends Foo {} + +void fun() { + final childFoo = ChildFoo(); + + final result = childFoo is Bar; +} +''', + [ + lint(121, 15, messageContainsAll: ['false']), + ], + ); + } + + void test_reports_unrelated_is_condition() async { + await assertDiagnostics( + r''' +class _A {} + +class _B extends _A {} + +class _C {} + +void lint() { + final _A a = _B(); + + if (a is _C) return; +} +''', + [ + lint(92, 7, messageContainsAll: ['false']), + ], + ); + } + + void test_reports_unrelated_is_not_condition() async { + await assertDiagnostics( + r''' +class _A {} + +class _B extends _A {} + +class _C {} + +void lint() { + final _A a = _B(); + + if (a is! _C) return; +} +''', + [ + lint(92, 8, messageContainsAll: ['true']), + ], + ); + } +} From 7e8403a85210ec534d921f2b56df0a0506f33955 Mon Sep 17 00:00:00 2001 From: daria-trusca-solid Date: Wed, 10 Jun 2026 16:17:59 +0300 Subject: [PATCH 10/49] Migrate newline before return rule and its tests (#240) * Updated newline_before_return_visitor rule and its visitor * Merged tests for newline_return_rule * Delete previous test file for newline_before_return * Updated min version of sdk * Added safety handle for the null case * Update newline_before_return_rule_test.dart * Added test for multiple comments and for a newline before a comment. --- .../newline_before_return_rule.dart | 59 +++--- .../newline_before_return_visitor.dart | 29 +-- lint_test/newline_before_return_test.dart | 37 ---- .../newline_before_return_rule_test.dart | 171 ++++++++++++++++++ 4 files changed, 215 insertions(+), 81 deletions(-) delete mode 100644 lint_test/newline_before_return_test.dart create mode 100644 test/lints/newline_before_return/newline_before_return_rule_test.dart diff --git a/lib/src/lints/newline_before_return/newline_before_return_rule.dart b/lib/src/lints/newline_before_return/newline_before_return_rule.dart index 3624a7c6..68e8360e 100644 --- a/lib/src/lints/newline_before_return/newline_before_return_rule.dart +++ b/lib/src/lints/newline_before_return/newline_before_return_rule.dart @@ -21,11 +21,11 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/newline_before_return/visitors/newline_before_return_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; // Inspired by ESLint (https://eslint.org/docs/rules/newline-before-return) @@ -72,38 +72,35 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// } /// } /// ``` -class NewlineBeforeReturnRule extends SolidLintRule { - /// This lint rule represents the error if - /// newline is missing before return statement - static const String lintName = 'newline_before_return'; +class NewlineBeforeReturnRule extends AnalysisRule { + /// The name of the lint rule. + static const String _lintName = 'newline_before_return'; - NewlineBeforeReturnRule._(super.config); + /// The message shown when the lint is triggered. + static const String _lintMessage = 'Missing blank line before return.'; - /// Creates a new instance of [NewlineBeforeReturnRule] - /// based on the lint configuration. - factory NewlineBeforeReturnRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (value) => "Missing blank line before return.", - ); + /// Lint code for this rule. + static const LintCode _code = LintCode( + _lintName, + _lintMessage, + ); - return NewlineBeforeReturnRule._(rule); - } + /// Creates a new instance of [NewlineBeforeReturnRule]. + NewlineBeforeReturnRule() + : super( + name: _lintName, + description: _lintMessage, + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, - ) { - context.registry.addReturnStatement((node) { - final visitor = NewLineBeforeReturnVisitor(resolver.lineInfo); - visitor.visitReturnStatement(node); + LintCode get diagnosticCode => _code; - for (final element in visitor.statements) { - reporter.atNode(element, code); - } - }); + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + final visitor = NewLineBeforeReturnVisitor(this); + registry.addReturnStatement(this, visitor); } } diff --git a/lib/src/lints/newline_before_return/visitors/newline_before_return_visitor.dart b/lib/src/lints/newline_before_return/visitors/newline_before_return_visitor.dart index febdad5c..b2e74cdc 100644 --- a/lib/src/lints/newline_before_return/visitors/newline_before_return_visitor.dart +++ b/lib/src/lints/newline_before_return/visitors/newline_before_return_visitor.dart @@ -25,17 +25,14 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/source/line_info.dart'; +import 'package:solid_lints/src/lints/newline_before_return/newline_before_return_rule.dart'; -/// The AST visitor that will all return statements. -class NewLineBeforeReturnVisitor extends RecursiveAstVisitor { - final LineInfo _lineInfo; - final _statements = []; +/// Visitor for [NewlineBeforeReturnRule]. +class NewLineBeforeReturnVisitor extends SimpleAstVisitor { + final NewlineBeforeReturnRule _rule; /// Creates instance of [NewLineBeforeReturnVisitor] with line info - NewLineBeforeReturnVisitor(this._lineInfo); - - /// List of all return statements - Iterable get statements => _statements; + NewLineBeforeReturnVisitor(this._rule); @override void visitReturnStatement(ReturnStatement node) { @@ -43,9 +40,9 @@ class NewLineBeforeReturnVisitor extends RecursiveAstVisitor { if (!_statementIsInBlock(node)) return; if (_statementIsFirstInBlock(node)) return; - if (_statementHasNewLineBefore(node, _lineInfo)) return; + if (_statementHasNewLineBefore(node)) return; - _statements.add(node); + _rule.reportAtNode(node); } static bool _statementIsInBlock(ReturnStatement node) => node.parent is Block; @@ -55,12 +52,18 @@ class NewLineBeforeReturnVisitor extends RecursiveAstVisitor { static bool _statementHasNewLineBefore( ReturnStatement node, - LineInfo lineInfo, ) { + final root = node.root; + if (root is! CompilationUnit) return true; + + final lineInfo = root.lineInfo; + final previousToken = node.returnKeyword.previous; + + if (previousToken == null) return true; + final previousTokenLineNumber = lineInfo - .getLocation(node.returnKeyword.previous!.end) + .getLocation(previousToken.end) .lineNumber; - final lastNotEmptyLineToken = _optimalToken(node.returnKeyword, lineInfo); final tokenLineNumber = lineInfo .getLocation(lastNotEmptyLineToken.offset) diff --git a/lint_test/newline_before_return_test.dart b/lint_test/newline_before_return_test.dart deleted file mode 100644 index 282057ad..00000000 --- a/lint_test/newline_before_return_test.dart +++ /dev/null @@ -1,37 +0,0 @@ -// ignore_for_file: unused_local_variable, prefer_match_file_name -// ignore_for_file: member_ordering -// ignore_for_file: avoid_unused_parameters - -/// Check the `newline_before_return` rule -class Foo { - int method() { - final a = 0; - // expect_lint: newline_before_return - return 1; - } - - void anotherMethod() { - final a = 1; - // expect_lint: newline_before_return - return; - } - - void bar(void Function()) { - return; - } -} - -void fun() { - final foo = Foo(); - foo.bar(() { - // This comment is ignored and line above is checked to be a newline - return; - }); - foo.bar(() { - final a = 1; - // expect_lint: newline_before_return - return; - }); - // expect_lint: newline_before_return - return; -} diff --git a/test/lints/newline_before_return/newline_before_return_rule_test.dart b/test/lints/newline_before_return/newline_before_return_rule_test.dart new file mode 100644 index 00000000..db805301 --- /dev/null +++ b/test/lints/newline_before_return/newline_before_return_rule_test.dart @@ -0,0 +1,171 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/newline_before_return/newline_before_return_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(NewlineBeforeReturnRuleTest); + }); +} + +@reflectiveTest +class NewlineBeforeReturnRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = NewlineBeforeReturnRule(); + super.setUp(); + } + + @override + String get analysisRule => rule.name; + + void test_reports_no_newline_before_return_value() async { + await assertDiagnostics( + r''' +int method() { + final a = 0; + return 1; +} + ''', + [lint(32, 9)], + ); + } + + void test_reports_no_newline_before_return() async { + await assertDiagnostics( + r''' +void method() { + final a = 0; + return; +} + ''', + [lint(33, 7)], + ); + } + + void test_reports_no_newline_before_return_with_comment() async { + await assertDiagnostics( + r''' +void method() { + final a = 0; + // Comment + return; +} + ''', + [lint(46, 7)], + ); + } + + void test_reports_no_newline_before_return_with_multiple_comments() async { + await assertDiagnostics( + r''' +void method() { + final a = 0; + // Comment 1 + // Comment 2 + return; +} + ''', + [lint(63, 7)], + ); + } + + void test_does_not_report_newline_before_return_with_comment() async { + await assertNoDiagnostics(r''' +void method() { + final a = 0; + + // Comment + return; +} + '''); + } + + void test_does_not_report_no_newline_before_single_statement_return() async { + await assertNoDiagnostics(r''' +void method() { + return; +} + '''); + } + + void test_does_not_report_newline_before_return() async { + await assertNoDiagnostics(r''' +void method() { + final a = 0; + + return; +} + '''); + } + + void + test_does_not_report_no_newline_before_single_statement_return_value() async { + await assertNoDiagnostics(r''' +int method() { + return 1; +} + '''); + } + + void + test_does_not_report_no_newline_before_single_statement_nested_return() async { + await assertNoDiagnostics(r''' +class Foo{ + void bar(void Function()) { + return; + } +} + +void fun() { + final foo = Foo(); + foo.bar(() { + return; + }); +} + '''); + } + + void test_reports_no_newline_before_return_nested() async { + await assertDiagnostics( + r''' +class Foo{ + void bar(void Function()) { + return; + } +} + +void fun() { + final foo = Foo(); + foo.bar(() { + final a = 1; + return; + }); +} + ''', + [lint(130, 7)], + ); + } + + void test_reports_no_newline_before_two_return_nested() async { + await assertDiagnostics( + r''' +class Foo{ + void bar(void Function()) { + return; + } +} + +void fun() { + final foo = Foo(); + foo.bar(() { + final a = 1; + return; + }); + return; +} + ''', + [lint(130, 7), lint(146, 7)], + ); + } +} From 4a31bb20b6c07a9035e756eb8c33276092ee2d24 Mon Sep 17 00:00:00 2001 From: daria-trusca-solid Date: Wed, 10 Jun 2026 16:19:10 +0300 Subject: [PATCH 11/49] Migrate no equal then else rule and its tests (#241) * Updated no_equal_then_else rule and its visitor * Merged tests for no_equal_then_else rule * Delete previous test file for no_equal_then_else * Applied PR suggestions from Gemini --- .../no_equal_then_else_rule.dart | 60 ++++++------- .../visitors/no_equal_then_else_visitor.dart | 17 ++-- lint_test/no_equal_then_else_test.dart | 30 ------- .../no_equal_then_else_rule_test.dart | 88 +++++++++++++++++++ 4 files changed, 126 insertions(+), 69 deletions(-) delete mode 100644 lint_test/no_equal_then_else_test.dart create mode 100644 test/lints/no_equal_then_else/no_equal_then_else_rule_test.dart diff --git a/lib/src/lints/no_equal_then_else/no_equal_then_else_rule.dart b/lib/src/lints/no_equal_then_else/no_equal_then_else_rule.dart index eaf8e389..8099a2c8 100644 --- a/lib/src/lints/no_equal_then_else/no_equal_then_else_rule.dart +++ b/lib/src/lints/no_equal_then_else/no_equal_then_else_rule.dart @@ -1,8 +1,8 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/no_equal_then_else/visitors/no_equal_then_else_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; // Inspired by PVS-Studio (https://www.viva64.com/en/w/v6004/) @@ -41,38 +41,36 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// /// selectedValue = condition ? valueA : valueB; /// ``` -class NoEqualThenElseRule extends SolidLintRule { - /// This lint rule represents the error if - /// 'if' statements or conditional expression is redundant - static const String lintName = 'no_equal_then_else'; +class NoEqualThenElseRule extends AnalysisRule { + /// The name of the lint rule. + static const String _lintName = 'no_equal_then_else'; - NoEqualThenElseRule._(super.config); + /// The message shown when the lint is triggered. + static const String _lintMessage = 'Then and else branches are equal.'; - /// Creates a new instance of [NoEqualThenElseRule] - /// based on the lint configuration. - factory NoEqualThenElseRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (value) => "Then and else branches are equal.", - ); + /// Lint code + static const _code = LintCode( + _lintName, + _lintMessage, + ); - return NoEqualThenElseRule._(rule); - } + /// Create a new instance of [NoEqualThenElseRule] + NoEqualThenElseRule() + : super( + name: _lintName, + description: _lintMessage, + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, - ) { - context.registry.addCompilationUnit((node) { - final visitor = NoEqualThenElseVisitor(); - node.accept(visitor); + LintCode get diagnosticCode => _code; - for (final element in visitor.nodes) { - reporter.atNode(element, code); - } - }); + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + final visitor = NoEqualThenElseVisitor(this); + registry.addIfStatement(this, visitor); + registry.addConditionalExpression(this, visitor); } } diff --git a/lib/src/lints/no_equal_then_else/visitors/no_equal_then_else_visitor.dart b/lib/src/lints/no_equal_then_else/visitors/no_equal_then_else_visitor.dart index 0ec18307..98b90230 100644 --- a/lib/src/lints/no_equal_then_else/visitors/no_equal_then_else_visitor.dart +++ b/lib/src/lints/no_equal_then_else/visitors/no_equal_then_else_visitor.dart @@ -23,14 +23,15 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/no_equal_then_else/no_equal_then_else_rule.dart'; -/// The AST visitor that will collect all unnecessary if statements and -/// conditional expressions. -class NoEqualThenElseVisitor extends RecursiveAstVisitor { - final _nodes = []; +/// Visitor for [NoEqualThenElseRule]. +class NoEqualThenElseVisitor extends SimpleAstVisitor { + /// The rule associated with this visitor. + final NoEqualThenElseRule _rule; - /// All unnecessary if statements and conditional expressions. - Iterable get nodes => _nodes; + /// Creates an instance of [NoEqualThenElseVisitor] + NoEqualThenElseVisitor(this._rule); @override void visitIfStatement(IfStatement node) { @@ -39,7 +40,7 @@ class NoEqualThenElseVisitor extends RecursiveAstVisitor { if (node.elseStatement != null && node.elseStatement is! IfStatement && node.thenStatement.toString() == node.elseStatement.toString()) { - _nodes.add(node); + _rule.reportAtNode(node); } } @@ -48,7 +49,7 @@ class NoEqualThenElseVisitor extends RecursiveAstVisitor { super.visitConditionalExpression(node); if (node.thenExpression.toString() == node.elseExpression.toString()) { - _nodes.add(node); + _rule.reportAtNode(node); } } } diff --git a/lint_test/no_equal_then_else_test.dart b/lint_test/no_equal_then_else_test.dart deleted file mode 100644 index c9caa40c..00000000 --- a/lint_test/no_equal_then_else_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// ignore_for_file: unused_local_variable -// ignore_for_file: cyclomatic_complexity -// ignore_for_file: no_magic_number -// ignore_for_file: prefer_conditional_expressions - -/// Check the `no_equal_then_else` rule -void fun() { - final _valueA = 1; - final _valueB = 2; - - int _result = 0; - - // expect_lint: no_equal_then_else - if (_valueA == 1) { - _result = _valueA; - } else { - _result = _valueA; - } - - if (_valueA == 1) { - _result = _valueA; - } else { - _result = _valueB; - } - - // expect_lint: no_equal_then_else - _result = _valueA == 2 ? _valueA : _valueA; - - _result = _valueA == 2 ? _valueA : _valueB; -} diff --git a/test/lints/no_equal_then_else/no_equal_then_else_rule_test.dart b/test/lints/no_equal_then_else/no_equal_then_else_rule_test.dart new file mode 100644 index 00000000..8eecbb3f --- /dev/null +++ b/test/lints/no_equal_then_else/no_equal_then_else_rule_test.dart @@ -0,0 +1,88 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/no_equal_then_else/no_equal_then_else_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(NoEqualThenElseRuleTest); + }); +} + +@reflectiveTest +class NoEqualThenElseRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = NoEqualThenElseRule(); + super.setUp(); + } + + @override + String get analysisRule => rule.name; + + void test_reports_then_and_else_equal() async { + await assertDiagnostics( + r''' +void fun() { + final _valueA = 1; + final _valueB = 2; + + int _result = 0; + + if (_valueA == 1) { + _result = _valueA; + } else { + _result = _valueA; + } +} +''', + [lint(78, 80)], + ); + } + + void test_does_not_report_then_and_else_different() async { + await assertNoDiagnostics(r''' +void fun() { + final _valueA = 1; + final _valueB = 2; + + int _result = 0; + + if (_valueA == 1) { + _result = _valueA; + } else { + _result = _valueB; + } +} +'''); + } + + void test_reports_conditional_expression_then_and_else_equal() async { + await assertDiagnostics( + r''' +void fun() { + final _valueA = 1; + final _valueB = 2; + + int _result = 0; + + _result = _valueA == 2 ? _valueA : _valueA; +} +''', + [lint(88, 32)], + ); + } + + void + test_does_not_report_conditional_expression_then_and_else_different() async { + await assertNoDiagnostics(r''' +void fun() { + final _valueA = 1; + final _valueB = 2; + + int _result = 0; + + _result = _valueA == 2 ? _valueA : _valueB; +} +'''); + } +} From e5eef63c2e873973d3a49c733efd0596bc2b2b3a Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Wed, 10 Jun 2026 17:27:25 +0300 Subject: [PATCH 12/49] refactor: migrate double_literal_format to analysis_server_plugin (#281) * refactor: migrate double_literal_format to analysis_server_plugin docs: improve code docs * fix: handle literals with capital E test: handle exponentials in test cases * fix: include unnecessary leading zeroes in lint description --- lib/main.dart | 8 + .../double_literal_format_rule.dart | 145 +++++++++++------- .../double_literal_format_utils.dart | 9 +- .../fixes/double_literal_format_fix.dart | 106 +++++++------ .../double_literal_format_visitor.dart | 46 ++++++ lint_test/double_literal_format_test.dart | 38 ----- pubspec.yaml | 4 +- .../double_literal_format_rule_test.dart | 139 +++++++++++++++++ 8 files changed, 350 insertions(+), 145 deletions(-) create mode 100644 lib/src/lints/double_literal_format/visitors/double_literal_format_visitor.dart delete mode 100644 lint_test/double_literal_format_test.dart create mode 100644 test/lints/double_literal_format/double_literal_format_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index 5657b7a1..010dcaff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,8 @@ import 'package:analysis_server_plugin/registry.dart'; import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; +import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; +import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; /// The entry point for the Solid Lints analyser server plugin. @@ -33,5 +35,11 @@ class SolidLintsPlugin extends Plugin { registry.registerLintRule( ProperSuperCallsRule(), ); + + final doubleLiteralFormatRule = DoubleLiteralFormatRule(); + registry.registerLintRule(doubleLiteralFormatRule); + for (final code in doubleLiteralFormatRule.diagnosticCodes) { + registry.registerFixForRule(code, DoubleLiteralFormatFix.new); + } } } diff --git a/lib/src/lints/double_literal_format/double_literal_format_rule.dart b/lib/src/lints/double_literal_format/double_literal_format_rule.dart index d472d04c..d90d3726 100644 --- a/lib/src/lints/double_literal_format/double_literal_format_rule.dart +++ b/lib/src/lints/double_literal_format/double_literal_format_rule.dart @@ -1,17 +1,14 @@ -import 'package:analyzer/diagnostic/diagnostic.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:analyzer/source/source_range.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; - -part 'double_literal_format_utils.dart'; -part 'fixes/double_literal_format_fix.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/double_literal_format/visitors/double_literal_format_visitor.dart'; /// A `double_literal_format` rule which /// checks that double literals should begin with 0. instead of just ., /// and should not end with a trailing 0. /// +/// {@template solid_lints.double_literal_format.example} /// ### Example /// /// #### BAD: @@ -25,75 +22,105 @@ part 'fixes/double_literal_format_fix.dart'; /// ```dart /// var a = 5.23, b = 0.16e+5, c = -0.25, d = -0.4e-5; /// ``` -class DoubleLiteralFormatRule extends SolidLintRule { +/// {@endtemplate} +class DoubleLiteralFormatRule extends MultiAnalysisRule { /// This lint rule represents /// the error whether we use bad formatted double literals. static const lintName = 'double_literal_format'; // Use different messages for different issues - /// This lint rule represents - /// the error whether we use double literals with a redundant leading 0. - static const _leadingZeroCode = LintCode( - name: lintName, - problemMessage: "Double literals shouldn't have redundant leading `0`.", + /// Reported when the double literal has a redundant leading 0 + /// + /// ### Example + /// + /// #### BAD: + /// + /// ```dart + /// var a = 05.23; + /// ``` + /// + /// #### GOOD: + /// + /// ```dart + /// var a = 5.23; + /// ``` + static const leadingZeroCode = LintCode( + lintName, + "Double literals shouldn't have redundant leading `0`.", correctionMessage: "Remove redundant leading `0`.", + uniqueName: 'leadingZero', ); - /// This lint rule represents - /// the error whether we use double literals with a leading decimal point. - static const _leadingDecimalCode = LintCode( - name: lintName, - problemMessage: - "Double literals shouldn't begin with the decimal point `.`.", + /// Reported when the double literal has a leading decimal point + /// without a zero before it. + /// + /// ### Example + /// + /// #### BAD: + /// + /// ```dart + /// var a = .23; + /// ``` + /// + /// #### GOOD: + /// + /// ```dart + /// var a = 0.23; + /// ``` + static const leadingDecimalCode = LintCode( + lintName, + "Double literals shouldn't begin with the decimal point `.`.", correctionMessage: "Add missing leading `0`.", + uniqueName: 'leadingDecimal', ); - /// This lint rule represents - /// the error whether we use double literals with a trailing 0. - static const _trailingZeroCode = LintCode( - name: lintName, - problemMessage: "Double literals should not end with a trailing `0`.", + /// Reported when the double literal has a redundant trailing 0. + /// + /// ### Example + /// + /// #### BAD: + /// + /// ```dart + /// var a = 5.230; + /// ``` + /// + /// #### GOOD: + /// + /// ```dart + /// var a = 5.23; + /// ``` + static const trailingZeroCode = LintCode( + lintName, + "Double literals should not end with a trailing `0`.", correctionMessage: "Remove redundant trailing `0`.", + uniqueName: 'trailingZero', ); - DoubleLiteralFormatRule._(super.config); + @override + List get diagnosticCodes => [ + leadingZeroCode, + leadingDecimalCode, + trailingZeroCode, + ]; /// Creates a new instance of [DoubleLiteralFormatRule] - /// based on the lint configuration. - factory DoubleLiteralFormatRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (_) => 'Double literal formatting issue', - ); - - return DoubleLiteralFormatRule._(rule); - } + DoubleLiteralFormatRule() + : super( + name: lintName, + description: + 'Double literals should begin with `0.` instead of just `.`, ' + 'should not end with a trailing 0 and ' + 'should not start with a leading 0', + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addDoubleLiteral((node) { - final lexeme = node.literal.lexeme; + super.registerNodeProcessors(registry, context); - if (lexeme.hasLeadingZero) { - reporter.atNode(node, _leadingZeroCode); - return; - } - if (lexeme.hasLeadingDecimalPoint) { - reporter.atNode(node, _leadingDecimalCode); - return; - } - if (lexeme.hasTrailingZero) { - reporter.atNode(node, _trailingZeroCode); - return; - } - }); + final visitor = DoubleLiteralFormatVisitor(this); + registry.addDoubleLiteral(this, visitor); } - - @override - List getFixes() => [_DoubleLiteralFormatFix()]; } diff --git a/lib/src/lints/double_literal_format/double_literal_format_utils.dart b/lib/src/lints/double_literal_format/double_literal_format_utils.dart index 99e7166e..9b9d75b6 100644 --- a/lib/src/lints/double_literal_format/double_literal_format_utils.dart +++ b/lib/src/lints/double_literal_format/double_literal_format_utils.dart @@ -1,7 +1,8 @@ -part of 'double_literal_format_rule.dart'; +import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; -/// Useful extensions for double literals representation -extension _StringDoubleEx on String { +/// Extension to quickly check double literal formatting according to +/// [DoubleLiteralFormatRule]. +extension DoubleLiteralFormatUtils on String { /// Returns true if a double literal starts with 00 bool get hasLeadingZero => startsWith('0') && this[1] != '.'; @@ -10,7 +11,7 @@ extension _StringDoubleEx on String { /// Returns true if a mantissa of a double literal ends with 0 bool get hasTrailingZero { - final mantissa = split('e').first; + final mantissa = toLowerCase().split('e').first; return mantissa.contains('.') && mantissa.endsWith('0') && diff --git a/lib/src/lints/double_literal_format/fixes/double_literal_format_fix.dart b/lib/src/lints/double_literal_format/fixes/double_literal_format_fix.dart index b3b5af12..9b7e0426 100644 --- a/lib/src/lints/double_literal_format/fixes/double_literal_format_fix.dart +++ b/lib/src/lints/double_literal_format/fixes/double_literal_format_fix.dart @@ -1,44 +1,62 @@ -part of '../double_literal_format_rule.dart'; +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; +import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; +import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_utils.dart'; -/// A Quick fix for `double_literal_format` rule +/// A Quick fix for [DoubleLiteralFormatRule] rule /// Suggests the correct value for an issue -class _DoubleLiteralFormatFix extends DartFix { +class DoubleLiteralFormatFix extends ParsedCorrectionProducer { + static const _doubleLiteralFormatKind = FixKind( + 'solid_lints.fix.${DoubleLiteralFormatRule.lintName}', + DartFixKindPriority.standard, + "Fix double literal format", + ); + + /// Creates a new instance of [DoubleLiteralFormatFix]. + DoubleLiteralFormatFix({required super.context}); + + @override + FixKind get fixKind => _doubleLiteralFormatKind; + @override - void run( - CustomLintResolver resolver, - ChangeReporter reporter, - CustomLintContext context, - Diagnostic analysisError, - List others, - ) { - context.registry.addDoubleLiteral((node) { - // checks that the literal declaration is where our warning is located - if (!analysisError.sourceRange.intersects(node.sourceRange)) return; + FixKind get multiFixKind => const FixKind( + 'solid_lints.fix.multi.${DoubleLiteralFormatRule.lintName}', + DartFixKindPriority.standard, + "Fix double literal format across files", + ); - final lexeme = node.literal.lexeme; - String? correctLexeme; + @override + CorrectionApplicability get applicability => + CorrectionApplicability.automatically; + + @override + Future compute(ChangeBuilder builder) async { + final doubleLiteralNode = node; + if (doubleLiteralNode is! DoubleLiteral) return; - if (lexeme.hasLeadingZero) { - correctLexeme = _correctLeadingZeroLexeme(lexeme); - } else if (lexeme.hasLeadingDecimalPoint) { - correctLexeme = _correctLeadingDecimalPointLexeme(lexeme); - } else if (lexeme.hasTrailingZero) { - correctLexeme = _correctTrailingZeroLexeme(lexeme); - } + final lexeme = doubleLiteralNode.literal.lexeme; + if (!lexeme.hasLeadingZero && + !lexeme.hasLeadingDecimalPoint && + !lexeme.hasTrailingZero) { + return; + } - if (correctLexeme != null) { - final changeBuilder = reporter.createChangeBuilder( - message: 'Replace by $correctLexeme', - priority: 1, - ); + final correctLexeme = _correctTrailingZeroLexeme( + _correctLeadingZeroLexeme( + _correctLeadingDecimalPointLexeme( + lexeme, + ), + ), + ); - changeBuilder.addDartFileEdit((builder) { - builder.addSimpleReplacement( - SourceRange(node.offset, node.length), - correctLexeme!, - ); - }); - } + await builder.addDartFileEdit(file, (builder) { + builder.addSimpleReplacement( + doubleLiteralNode.sourceRange, + correctLexeme, + ); }); } @@ -46,19 +64,21 @@ class _DoubleLiteralFormatFix extends DartFix { ? lexeme : _correctLeadingZeroLexeme(lexeme.substring(1)); - String _correctLeadingDecimalPointLexeme(String lexeme) => '0$lexeme'; + String _correctLeadingDecimalPointLexeme(String lexeme) => + lexeme.hasLeadingDecimalPoint ? '0$lexeme' : lexeme; String _correctTrailingZeroLexeme(String lexeme) { if (!lexeme.hasTrailingZero) { return lexeme; - } else { - final mantissa = lexeme.split('e').first; - return _correctTrailingZeroLexeme( - lexeme.replaceFirst( - mantissa, - mantissa.substring(0, mantissa.length - 1), - ), - ); } + + final mantissa = lexeme.toLowerCase().split('e').first; + + return _correctTrailingZeroLexeme( + lexeme.replaceFirst( + mantissa, + mantissa.substring(0, mantissa.length - 1), + ), + ); } } diff --git a/lib/src/lints/double_literal_format/visitors/double_literal_format_visitor.dart b/lib/src/lints/double_literal_format/visitors/double_literal_format_visitor.dart new file mode 100644 index 00000000..fa6e4e96 --- /dev/null +++ b/lib/src/lints/double_literal_format/visitors/double_literal_format_visitor.dart @@ -0,0 +1,46 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart' + show DoubleLiteralFormatRule; +import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_utils.dart'; + +/// A visitor that checks that double literals are formatted according to +/// [DoubleLiteralFormatRule]. +/// {@macro solid_lints.double_literal_format.example} +class DoubleLiteralFormatVisitor extends SimpleAstVisitor { + final DoubleLiteralFormatRule _rule; + + /// Creates a new instance of [DoubleLiteralFormatVisitor]. + DoubleLiteralFormatVisitor(this._rule); + + @override + void visitDoubleLiteral(DoubleLiteral node) { + super.visitDoubleLiteral(node); + + final lexeme = node.literal.lexeme; + + if (lexeme.hasLeadingZero) { + _rule.reportAtNode( + node, + diagnosticCode: DoubleLiteralFormatRule.leadingZeroCode, + ); + return; + } + + if (lexeme.hasLeadingDecimalPoint) { + _rule.reportAtNode( + node, + diagnosticCode: DoubleLiteralFormatRule.leadingDecimalCode, + ); + return; + } + + if (lexeme.hasTrailingZero) { + _rule.reportAtNode( + node, + diagnosticCode: DoubleLiteralFormatRule.trailingZeroCode, + ); + return; + } + } +} diff --git a/lint_test/double_literal_format_test.dart b/lint_test/double_literal_format_test.dart deleted file mode 100644 index 195b01e8..00000000 --- a/lint_test/double_literal_format_test.dart +++ /dev/null @@ -1,38 +0,0 @@ -// ignore_for_file: avoid_global_state -// ignore_for_file: type_annotate_public_apis -// ignore_for_file: unused_local_variable - -/// Check the `double_literal_format` rule - -// expect_lint: double_literal_format -var badA = 05.23; -var goodA = 5.23; -var stringA = '05.23'; - -var intA = 0; - -// expect_lint: double_literal_format -double badB = -01.2; -double goodB = -1.2; - -// expect_lint: double_literal_format -double badC = -001.2; - -// expect_lint: double_literal_format -double badExpr = 5.23 + 05.23; - -double goodExpr = 5.23 + 5.23; - -class DoubleLiteralFormatTest { - // expect_lint: double_literal_format - var badA = .16e+5; - var goodA = 0.16e+5; - - void someMethod() { - // expect_lint: double_literal_format - const badA = -0.250; - const goodA = -0.25; - // expect_lint: double_literal_format - const badB = 0.160e+5; - } -} diff --git a/pubspec.yaml b/pubspec.yaml index df5633f6..7b7dd9b2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,9 @@ environment: sdk: ">=3.9.0 <4.0.0" dependencies: + # Needed until required types for fixes are exported by analyzer_server_plugin + # More details: https://github.com/dart-lang/sdk/issues/61821 + analyzer_plugin: ^0.14.2 analyzer: ^10.0.1 collection: ^1.19.0 analysis_server_plugin: ^0.3.3 @@ -23,4 +26,3 @@ dev_dependencies: args: ^2.6.0 analyzer_testing: ^0.1.9 test_reflective_loader: ^0.3.0 - diff --git a/test/lints/double_literal_format/double_literal_format_rule_test.dart b/test/lints/double_literal_format/double_literal_format_rule_test.dart new file mode 100644 index 00000000..d3c8c2a7 --- /dev/null +++ b/test/lints/double_literal_format/double_literal_format_rule_test.dart @@ -0,0 +1,139 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(DoubleLiteralFormatRuleTest); + }); +} + +@reflectiveTest +class DoubleLiteralFormatRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = DoubleLiteralFormatRule(); + super.setUp(); + } + + Future test_reports_on_leading_zeros() async { + await assertDiagnostics( + r''' +var badA = 05.23; +double badB = -01.2; +double badC = -001.2; +double badExpr = 5.23 + 05.23; + +class Test { + var badA = 05.23; + double badB = -01.2; + double badC = -001.2; + double badExpr = 5.23 + 05.23; +} +''', + [ + lint(11, 5), + lint(33, 4), + lint(54, 5), + lint(85, 5), + + lint(119, 5), + lint(143, 4), + lint(166, 5), + lint(199, 5), + ], + ); + } + + Future test_reports_on_trailing_zeros() async { + await assertDiagnostics( + r''' +class Test { + var badA = 5.230; + final badB = -1.20; + double get badC => -1.200; + double badExpr = 5.23 + 5.230; + var badD = -0.400e-5; + + void someMethod() { + var badA = 5.230; + double badB = -1.20; + double badC = -1.200; + double badExpr = 5.23 + 5.230; + var badD = -0.400E-5; + } +} +''', + [ + lint(26, 5), + lint(49, 4), + lint(77, 5), + lint(110, 5), + lint(131, 8), + + lint(179, 5), + lint(205, 4), + lint(230, 5), + lint(265, 5), + lint(288, 8), + ], + ); + } + + Future test_reports_on_leading_decimal_point() async { + await assertDiagnostics( + r''' +var badA = .23; +double badB = -.2; +double badExpr = 5.23 + .23; +var badD = .4e-5; + +class Test { + var badA = .23; + double badB = -.2; + double get badExpr => 5.23 + .23; + double get badD => -.4E-5; +} +''', + [ + lint(11, 3), + lint(31, 2), + lint(59, 3), + lint(75, 5), + + lint(109, 3), + lint(131, 2), + lint(166, 3), + lint(193, 5), + ], + ); + } + + void test_does_not_report_on_non_double_literals() async { + await assertNoDiagnostics(r''' +var badA = '05.23'; +var stringA = '5.23'; +final badB = '.04'; +var intA = 0; +'''); + } + + Future test_does_not_report_on_good_literals() async { + await assertNoDiagnostics(r''' +var goodA = 5.23; +double goodB = -1.2; + +double goodExpr = 5.23 + 5.23; +class DoubleLiteralFormatTest { + var goodA = 0.16e+5; + var goodB = 0.16E+5; + double goodC = -0.4E-5; + double goodE = 5.23 + 0.4e-5; + + void someMethod() { + const goodA = -0.25; + } +} +'''); + } +} From 1cd1032273a120513306e6459d4598d16289b018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tru=C8=99c=C4=83=20Daria=20Maria?= <115713732+Dariaa14@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:30:10 +0300 Subject: [PATCH 13/49] Implementation of parameter parser (#234) * Created analysis_options.yaml rules parser * Improved yaml parser and added the analysis_options loader * Improved rules loader from yaml * Added verification before looking for .yaml's path * Fields and getters are now declared before the constructor * Added method to get options of a rule by it's name * Made suggested changes to file upward finder * Removed top-level variable * Improved name of variable in loadRuleFromContext * Updated analysis options to have rules for each configuration file path * Updated file upward finder to not mix File from dart.io with file from analyzer * Added usage example in avoid_global_state_rule * style: move getters and fields before constructor * style: improve readability * fix: don't parse enabled if the rule has configured options style: improve variable names refactor: use root package path instead of library path refactor: use pattern matching to reduce nesting * feat: reload rules from file if newer refactor: allow mocking resource provider refactor: extract CachedPackageRules model * test: add AnalysisOptionsLoaderTest * feat(SolidLintRule): add parameter parsing refactor: use Map instead of LintOptions as the enabled field is implicitly true for all rules that the analyzer processes remove RuleConfig as it is no longer needed * fix: use Map for raw rule config * fix: method name * fix: make sure rules options are loaded before getting parameters * refactor: remove unused AnalysisOptionsLoader from AvoidGlobalStateRule refactor: use for loop to register rules * refactor: extract duplicate logic --------- Co-authored-by: Andrew Bekhiet --- lib/main.dart | 24 +- .../analysis_options_loader.dart | 109 +++++++++ .../cached_package_rules.dart | 14 ++ lib/src/models/rule_config.dart | 43 ---- lib/src/models/solid_lint_rule.dart | 45 +++- .../analysis_options_loader_test.dart | 225 ++++++++++++++++++ 6 files changed, 398 insertions(+), 62 deletions(-) create mode 100644 lib/src/common/parameter_parser/analysis_options_loader.dart create mode 100644 lib/src/common/parameter_parser/cached_package_rules.dart delete mode 100644 lib/src/models/rule_config.dart create mode 100644 test/src/common/parameter_parser/analysis_options_loader_test.dart diff --git a/lib/main.dart b/lib/main.dart index 010dcaff..e758297e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:analysis_server_plugin/plugin.dart'; import 'package:analysis_server_plugin/registry.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; @@ -23,21 +24,24 @@ class SolidLintsPlugin extends Plugin { @override void register(PluginRegistry registry) { - registry.registerLintRule( + final analysisLoader = AnalysisOptionsLoader(); + + final doubleLiteralFormatRule = DoubleLiteralFormatRule(); + final lintRules = [ AvoidGlobalStateRule(), - ); - registry.registerLintRule( AvoidNonNullAssertionRule(), - ); - registry.registerLintRule( AvoidDebugPrintInReleaseRule(), - ); - registry.registerLintRule( + doubleLiteralFormatRule, ProperSuperCallsRule(), - ); + // TODO: Add more lint rules and use analysisLoader + // for rules that need parameters + // For example: `CyclomaticComplexityRule(analysisLoader)` + ]; + + for (final lintRule in lintRules) { + registry.registerLintRule(lintRule); + } - final doubleLiteralFormatRule = DoubleLiteralFormatRule(); - registry.registerLintRule(doubleLiteralFormatRule); for (final code in doubleLiteralFormatRule.diagnosticCodes) { registry.registerFixForRule(code, DoubleLiteralFormatFix.new); } diff --git a/lib/src/common/parameter_parser/analysis_options_loader.dart b/lib/src/common/parameter_parser/analysis_options_loader.dart new file mode 100644 index 00000000..ab22ecc9 --- /dev/null +++ b/lib/src/common/parameter_parser/analysis_options_loader.dart @@ -0,0 +1,109 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:solid_lints/src/common/parameter_parser/cached_package_rules.dart'; +import 'package:yaml/yaml.dart'; + +/// Loads and parses analysis options from a Dart project's YAML file. +class AnalysisOptionsLoader { + final ResourceProvider _resourceProvider; + final Map _rulesCache = {}; + + /// Creates an instance of [AnalysisOptionsLoader] + AnalysisOptionsLoader({ResourceProvider? resourceProvider}) + : _resourceProvider = + resourceProvider ?? PhysicalResourceProvider.INSTANCE; + + /// Gets the options for a specific rule by its name. + Map? getRuleOptions(RuleContext context, String ruleName) => + _withNearestAnalysisOptionsFilePathForContext?>( + context, + (path) => _rulesCache[path]?.rules[ruleName], + ); + + /// Loads lint rules from the analysis options file for all rules + /// using the provided [RuleContext]. + void loadRulesOptionsFromContext(RuleContext context) => + _withNearestAnalysisOptionsFilePathForContext( + context, + _loadRulesOptionsIfNewer, + ); + + T? _withNearestAnalysisOptionsFilePathForContext( + RuleContext context, + T Function(String) f, + ) { + final packageRootPath = context.package?.root.path; + if (packageRootPath == null) return null; + + final yamlPath = _findNearestAnalysisOptionsFilePath(packageRootPath); + if (yamlPath == null) return null; + + return f(yamlPath); + } + + void _loadRulesOptionsIfNewer(String yamlPath) { + final analysisOptionsFile = _resourceProvider.getFile(yamlPath); + final modificationStamp = analysisOptionsFile.modificationStamp; + final cachedRules = _rulesCache[yamlPath]; + + if (cachedRules?.modificationStamp == modificationStamp) { + return; + } + + final rules = _getRules(analysisOptionsFile); + _rulesCache[yamlPath] = CachedPackageRules( + modificationStamp: modificationStamp, + rules: rules, + ); + } + + String? _findNearestAnalysisOptionsFilePath(String packageRootPath) { + final pathContext = _resourceProvider.pathContext; + String currentDirectoryPath = packageRootPath; + + while (pathContext.dirname(currentDirectoryPath) != currentDirectoryPath) { + final candidatePath = + pathContext.join(currentDirectoryPath, 'analysis_options.yaml'); + final candidateFile = _resourceProvider.getFile(candidatePath); + + if (candidateFile.exists) { + return candidatePath; + } + + final parentDir = pathContext.dirname(currentDirectoryPath); + currentDirectoryPath = parentDir; + } + + return null; + } + + Map> _getRules(File? analysisOptionsFile) { + if (analysisOptionsFile == null || !analysisOptionsFile.exists) { + return {}; + } + + final optionsString = analysisOptionsFile.readAsStringSync(); + Object? yaml; + try { + yaml = loadYaml(optionsString) as Object?; + } catch (err) { + return {}; + } + + if (yaml + case {'plugins': {'solid_lints': {'diagnostics': final diagnostics?}}} + when diagnostics is Map) { + return Map.fromEntries( + diagnostics.entries.where((e) => e.key is String && e.value is Map).map( + (e) => MapEntry( + e.key as String, + Map.from(e.value as Map), + ), + ), + ); + } + + return {}; + } +} diff --git a/lib/src/common/parameter_parser/cached_package_rules.dart b/lib/src/common/parameter_parser/cached_package_rules.dart new file mode 100644 index 00000000..a65201d9 --- /dev/null +++ b/lib/src/common/parameter_parser/cached_package_rules.dart @@ -0,0 +1,14 @@ +/// Cached rules for a dart package +class CachedPackageRules { + /// The last modification stamp of the analysis options file + final int modificationStamp; + + /// Cached rules options by rule name for the package + final Map> rules; + + /// Creates an instance of [CachedPackageRules] + const CachedPackageRules({ + required this.modificationStamp, + required this.rules, + }); +} diff --git a/lib/src/models/rule_config.dart b/lib/src/models/rule_config.dart deleted file mode 100644 index 35c35b96..00000000 --- a/lib/src/models/rule_config.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:analyzer/error/error.dart' as error; -import 'package:custom_lint_builder/custom_lint_builder.dart'; - -/// Type definition of a value factory which allows us to map data from -/// YAML configuration to an object of type [T]. -typedef RuleParameterParser = T Function(Map json); - -/// Type definition for a problem message factory after finding a problem -/// by a given lint. -typedef RuleProblemFactory = String Function(T value); - -/// [RuleConfig] allows us to quickly parse a lint rule and -/// declare basic configuration for it. -class RuleConfig { - /// Constructor for [RuleConfig] model. - RuleConfig({ - required this.name, - required CustomLintConfigs configs, - required RuleProblemFactory problemMessage, - RuleParameterParser? paramsParser, - }) : enabled = configs.rules[name]?.enabled ?? false, - parameters = paramsParser?.call(configs.rules[name]?.json ?? {}) as T, - _problemMessageFactory = problemMessage; - - /// This lint rule represents the error. - final String name; - - /// A flag which indicates whether this rule was enabled by the user. - final bool enabled; - - /// Value with a configuration for a particular rule. - final T parameters; - - /// Factory for generating error messages. - final RuleProblemFactory _problemMessageFactory; - - /// [LintCode] which is generated based on the provided data. - LintCode get lintCode => LintCode( - name: name, - problemMessage: _problemMessageFactory(parameters), - errorSeverity: error.DiagnosticSeverity.WARNING, - ); -} diff --git a/lib/src/models/solid_lint_rule.dart b/lib/src/models/solid_lint_rule.dart index a536eb4f..9f5bee56 100644 --- a/lib/src/models/solid_lint_rule.dart +++ b/lib/src/models/solid_lint_rule.dart @@ -1,16 +1,43 @@ -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; + +/// A function that parses the rule parameters from analysis options json +typedef RuleParametersParser = T Function(Map); /// A base class for emitting information about /// issues with user's `.dart` files. -abstract class SolidLintRule extends DartLintRule { +abstract class SolidLintRule extends AnalysisRule { + final AnalysisOptionsLoader? _analysisOptionsLoader; + + final RuleParametersParser? _parametersParser; + /// Constructor for [SolidLintRule] model. - SolidLintRule(this.config) : super(code: config.lintCode); + SolidLintRule({ + required super.name, + required super.description, + super.state, + }) : _analysisOptionsLoader = null, + _parametersParser = null; + + /// Constructor for [SolidLintRule] model with parameters. + SolidLintRule.withParameters({ + required AnalysisOptionsLoader analysisOptionsLoader, + required RuleParametersParser parametersParser, + required super.name, + required super.description, + super.state, + }) : _analysisOptionsLoader = analysisOptionsLoader, + _parametersParser = parametersParser; + + /// Reads the rule parameters from analysis options and parses them to [T] + T? getParametersForContext(RuleContext context) { + _analysisOptionsLoader?.loadRulesOptionsFromContext(context); - /// Configuration for a particular rule with all the - /// defined custom parameters. - final RuleConfig config; + final unparsedParameters = + _analysisOptionsLoader?.getRuleOptions(context, name); + if (unparsedParameters == null) return null; - /// A flag which indicates whether this rule was enabled by the user. - bool get enabled => config.enabled; + return _parametersParser?.call(unparsedParameters); + } } diff --git a/test/src/common/parameter_parser/analysis_options_loader_test.dart b/test/src/common/parameter_parser/analysis_options_loader_test.dart new file mode 100644 index 00000000..a727136f --- /dev/null +++ b/test/src/common/parameter_parser/analysis_options_loader_test.dart @@ -0,0 +1,225 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/workspace/workspace.dart'; +import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:test/test.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AnalysisOptionsLoaderTest); + }); +} + +@reflectiveTest +class AnalysisOptionsLoaderTest extends PubPackageResolutionTest { + // TODO: use actual [Rule.lintName] after migrating to analyzer_server_plugin + // Can't be used right now because they have compile errors + static const _mockRuleThatNeedsConfigName = 'mock_rule_that_needs_config'; + static const _mockRule2Name = 'mock_rule_2'; + static const _cyclomaticComplexityName = 'cyclomatic_complexity'; + + static const _mockAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: + abc: def + $_mockRule2Name: + foo: bar + exclude: + - class_name: MockClass + method_name: mockMethod + $_cyclomaticComplexityName: + max_complexity: 10 + exclude: + - class_name: MockClass + method_name: mockMethod + - method_name: mockMethod2 + '''; + static const _mockDifferentAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: + abc: ghi + $_mockRule2Name: + foo: baz + exclude: + - class_name: MockOtherClass + method_name: mockOtherMethod + $_cyclomaticComplexityName: + max_complexity: 20 + exclude: + - class_name: MockOtherClass + method_name: mockOtherMethod + - method_name: mockOtherMethod2 + '''; + + late AnalysisOptionsLoader analysisOptionsLoader; + late RuleContext mockRuleContext; + + @override + void setUp() { + super.setUp(); + + analysisOptionsLoader = + AnalysisOptionsLoader(resourceProvider: resourceProvider); + mockRuleContext = _createMockContextForPackage(testPackageRootPath); + + _writeMockAnalysisOptionsYamlFile(); + + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + } + + void _writeMockAnalysisOptionsYamlFile() { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _mockAnalysisOptionsContent, + ); + } + + void test_cached_response_is_scoped_to_package_and_rule() { + const otherPackageRootPath = '/home/other'; + + newFolder(otherPackageRootPath); + newPubspecYamlFile(otherPackageRootPath, 'name: other'); + newAnalysisOptionsYamlFile( + otherPackageRootPath, + _mockDifferentAnalysisOptionsContent, + ); + + for (final ruleName in [ + _mockRuleThatNeedsConfigName, + _mockRule2Name, + _cyclomaticComplexityName + ]) { + final currentPackageOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + ruleName, + ); + final otherPackageOptions = analysisOptionsLoader.getRuleOptions( + _createMockContextForPackage(otherPackageRootPath), + ruleName, + ); + + expect( + currentPackageOptions, + isNot(equals(otherPackageOptions)), + ); + } + } + + void test_each_rule_gets_its_options() { + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final mockRuleThatNeedsConfigOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + final mockRule2Options = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRule2Name, + ); + final cyclomaticComplexityOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _cyclomaticComplexityName, + ); + + expect(mockRuleThatNeedsConfigOptions, isNotNull); + expect(mockRuleThatNeedsConfigOptions, {'abc': 'def'}); + + expect(mockRule2Options, isNotNull); + expect(mockRule2Options, { + 'foo': 'bar', + 'exclude': [ + {'class_name': 'MockClass', 'method_name': 'mockMethod'}, + ] + }); + + expect(cyclomaticComplexityOptions, isNotNull); + expect(cyclomaticComplexityOptions, { + 'max_complexity': 10, + 'exclude': [ + {'class_name': 'MockClass', 'method_name': 'mockMethod'}, + {'method_name': 'mockMethod2'}, + ] + }); + } + + void test_invalidates_cache_when_analysis_options_changed() { + final initialOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + _mockDifferentAnalysisOptionsContent, + ); + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final updatedOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + expect(initialOptions, {'abc': 'def'}); + expect(updatedOptions, {'abc': 'ghi'}); + expect(updatedOptions, isNot(same(initialOptions))); + } + + void test_loads_and_parses_rule_options_from_yaml_file() { + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final options = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + expect(options, isNotNull); + expect(options, {'abc': 'def'}); + } + + void test_returns_cached_response_for_same_rule_name() { + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final firstOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + final secondOptions = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + expect(secondOptions, same(firstOptions)); + } + + RuleContext _createMockContextForPackage(String packageRootPath) { + return _TestRuleContext( + _TestWorkspacePackage(getFolder(packageRootPath)), + ); + } +} + +class _TestRuleContext implements RuleContext { + @override + final WorkspacePackage? package; + + _TestRuleContext(this.package); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _TestWorkspacePackage implements WorkspacePackage { + @override + final Folder root; + + _TestWorkspacePackage(this.root); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} From 17d049adb5dd1847052151c1d11857bb0132eab7 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 15 Jun 2026 21:01:04 +0300 Subject: [PATCH 14/49] feat: migrate avoid_final_with_getter (#242) * feat: migrate avoid_final_with_getter refactor: extract GetterVariableVisitor into a file refactor: extract GetterReferenceId into a file test(avoid_final_with_getter): migrate tests * fix: also check block getters with one return statements fix: also check getter returning private final fields with this keyword * fix: also rename private field references --- lib/main.dart | 8 ++ .../avoid_final_with_getter_rule.dart | 57 +++----- .../fixes/avoid_final_with_getter_fix.dart | 110 ++++++++++----- .../utils/getter_reference_id.dart | 37 +++++ .../avoid_final_with_getter_visitor.dart | 55 +++++--- .../visitors/getter_variable_visitor.dart | 34 ++--- .../visitors/variable_references_visitor.dart | 32 +++++ lint_test/avoid_final_with_getter_test.dart | 35 ----- .../avoid_final_with_getter_rule_test.dart | 131 ++++++++++++++++++ 9 files changed, 351 insertions(+), 148 deletions(-) create mode 100644 lib/src/lints/avoid_final_with_getter/utils/getter_reference_id.dart create mode 100644 lib/src/lints/avoid_final_with_getter/visitors/variable_references_visitor.dart delete mode 100644 lint_test/avoid_final_with_getter_test.dart create mode 100644 test/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index e758297e..bb876ec5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,8 @@ import 'package:analysis_server_plugin/plugin.dart'; import 'package:analysis_server_plugin/registry.dart'; import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; +import 'package:solid_lints/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart'; +import 'package:solid_lints/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; @@ -28,6 +30,7 @@ class SolidLintsPlugin extends Plugin { final doubleLiteralFormatRule = DoubleLiteralFormatRule(); final lintRules = [ + AvoidFinalWithGetterRule(), AvoidGlobalStateRule(), AvoidNonNullAssertionRule(), AvoidDebugPrintInReleaseRule(), @@ -45,5 +48,10 @@ class SolidLintsPlugin extends Plugin { for (final code in doubleLiteralFormatRule.diagnosticCodes) { registry.registerFixForRule(code, DoubleLiteralFormatFix.new); } + + registry.registerFixForRule( + AvoidFinalWithGetterRule.code, + AvoidFinalWithGetterFix.new, + ); } } diff --git a/lib/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart b/lib/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart index d495c5db..727e3dfe 100644 --- a/lib/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart +++ b/lib/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart @@ -1,12 +1,8 @@ -import 'package:analyzer/diagnostic/diagnostic.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:analyzer/source/source_range.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/avoid_final_with_getter/visitors/avoid_final_with_getter_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; - -part 'fixes/avoid_final_with_getter_fix.dart'; /// Avoid using final private fields with getters. /// @@ -34,45 +30,32 @@ part 'fixes/avoid_final_with_getter_fix.dart'; /// } /// ``` /// -class AvoidFinalWithGetterRule extends SolidLintRule { +class AvoidFinalWithGetterRule extends AnalysisRule { /// This lint rule represents /// the error whether we use final private fields with getters. static const lintName = 'avoid_final_with_getter'; - final _diagnosticsInfoExpando = Expando(); - - AvoidFinalWithGetterRule._(super.config); + /// The code to report for a violation + static const LintCode code = LintCode( + lintName, + 'Avoid final private fields with getters.', + correctionMessage: 'Remove the getter and make the field public.', + ); /// Creates a new instance of [AvoidFinalWithGetterRule] - /// based on the lint configuration. - factory AvoidFinalWithGetterRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (_) => 'Avoid final private fields with getters.', - ); + AvoidFinalWithGetterRule() + : super(name: lintName, description: code.problemMessage); - return AvoidFinalWithGetterRule._(rule); - } + @override + LintCode get diagnosticCode => code; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addCompilationUnit((node) { - final visitor = AvoidFinalWithGetterVisitor(); - node.accept(visitor); + final visitor = AvoidFinalWithGetterVisitor(this); - for (final element in visitor.getters) { - final diagnostic = reporter.atNode(element.getter, code); - - _diagnosticsInfoExpando[diagnostic] = element; - } - }); + registry.addCompilationUnit(this, visitor); } - - @override - List getFixes() => [_FinalWithGetterFix(_diagnosticsInfoExpando)]; } diff --git a/lib/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.dart b/lib/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.dart index 98e08fb4..dd810642 100644 --- a/lib/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.dart +++ b/lib/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.dart @@ -1,40 +1,84 @@ -part of '../avoid_final_with_getter_rule.dart'; +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; +import 'package:solid_lints/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart'; +import 'package:solid_lints/src/lints/avoid_final_with_getter/visitors/getter_variable_visitor.dart'; +import 'package:solid_lints/src/lints/avoid_final_with_getter/visitors/variable_references_visitor.dart'; -class _FinalWithGetterFix extends DartFix { - final Expando _diagnosticsInfoExpando; +/// A Quick fix for [AvoidFinalWithGetterRule] rule +class AvoidFinalWithGetterFix extends ResolvedCorrectionProducer { + static const _avoidFinalWithGetterKind = FixKind( + 'solid_lints.fix.${AvoidFinalWithGetterRule.lintName}', + DartFixKindPriority.standard, + "Remove the getter and make the field public", + ); - _FinalWithGetterFix(this._diagnosticsInfoExpando); + /// Creates a new instance of [AvoidFinalWithGetterFix] + AvoidFinalWithGetterFix({required super.context}); @override - void run( - CustomLintResolver resolver, - ChangeReporter reporter, - CustomLintContext context, - Diagnostic diagnostic, - List others, - ) { - context.registry.addMethodDeclaration((node) { - if (!diagnostic.sourceRange.intersects(node.sourceRange)) return; - - final info = _diagnosticsInfoExpando[diagnostic]; - if (info == null) return; - - _addReplacement(reporter, info); - }); - } + CorrectionApplicability get applicability => + CorrectionApplicability.acrossFiles; + + @override + FixKind get fixKind => _avoidFinalWithGetterKind; + + @override + Future compute(ChangeBuilder builder) async { + final getterNode = node; + if (getterNode + case MethodDeclaration( + isGetter: true, + declaredFragment: ExecutableFragment( + element: GetterElement( + isAbstract: false, + isPublic: true, + ), + ), + )) { + final compilationUnit = node.thisOrAncestorOfType(); + if (compilationUnit == null) return; + + final getterVariableVisitor = GetterVariableVisitor(getterNode); + compilationUnit.accept(getterVariableVisitor); + + final variableDeclaration = getterVariableVisitor.variable; + if (variableDeclaration == null) return; + + final referencesVisitor = VariableReferencesVisitor(variableDeclaration); + compilationUnit.accept(referencesVisitor); + + final variableReferences = referencesVisitor.references; + + final variableName = variableDeclaration.name.lexeme; + final newPublicVariableName = variableName.startsWith('_') + ? variableName.substring(1) + : variableName; + + await builder.addDartFileEdit(file, (builder) { + builder.addDeletion(getterNode.sourceRange); + + builder.addSimpleReplacement( + variableDeclaration.name.sourceRange, + newPublicVariableName, + ); + + for (final reference in variableReferences) { + if (reference.sourceRange.intersects(getterNode.sourceRange)) { + continue; + } + + builder.addSimpleReplacement( + reference.sourceRange, + newPublicVariableName, + ); + } - void _addReplacement( - ChangeReporter reporter, - FinalWithGetterInfo info, - ) { - final changeBuilder = reporter.createChangeBuilder( - message: "Remove getter and make variable public.", - priority: 1, - ); - - changeBuilder.addDartFileEdit((builder) { - builder.addDeletion(info.getter.sourceRange); - builder.addDeletion(SourceRange(info.variable.name.offset, 1)); - }); + builder.format(compilationUnit.sourceRange); + }); + } } } diff --git a/lib/src/lints/avoid_final_with_getter/utils/getter_reference_id.dart b/lib/src/lints/avoid_final_with_getter/utils/getter_reference_id.dart new file mode 100644 index 00000000..0aa47984 --- /dev/null +++ b/lib/src/lints/avoid_final_with_getter/utils/getter_reference_id.dart @@ -0,0 +1,37 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; + +/// Extension method to get the reference id of the getter. +extension GetterReferenceId on MethodDeclaration { + /// Get the reference id of the getter. + int? get getterReferenceId { + final returnExpression = switch (body) { + ExpressionFunctionBody(expression: final expr) || + BlockFunctionBody( + block: Block( + statements: [ + ReturnStatement(expression: final expr?), + ], + ) + ) => + expr, + _ => null, + }; + + final identifier = switch (returnExpression) { + SimpleIdentifier() => returnExpression, + PropertyAccess( + target: ThisExpression(), + :final propertyName, + ) => + propertyName, + _ => null, + }; + + return switch (identifier) { + SimpleIdentifier(element: PropertyAccessorElement(:final variable)) => + variable.id, + _ => null, + }; + } +} diff --git a/lib/src/lints/avoid_final_with_getter/visitors/avoid_final_with_getter_visitor.dart b/lib/src/lints/avoid_final_with_getter/visitors/avoid_final_with_getter_visitor.dart index 6e5bfbf1..ee1aa7de 100644 --- a/lib/src/lints/avoid_final_with_getter/visitors/avoid_final_with_getter_visitor.dart +++ b/lib/src/lints/avoid_final_with_getter/visitors/avoid_final_with_getter_visitor.dart @@ -1,18 +1,24 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/element/element.dart'; -import 'package:solid_lints/src/lints/avoid_final_with_getter/visitors/getter_variable_visitor.dart'; +import 'package:solid_lints/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart'; +import 'package:solid_lints/src/lints/avoid_final_with_getter/utils/getter_reference_id.dart'; /// A visitor that checks for final private fields with getters. /// If a final private field has a getter, it is considered as a public field. class AvoidFinalWithGetterVisitor extends RecursiveAstVisitor { - final _getters = {}; + final AvoidFinalWithGetterRule _rule; - /// List of getters - Set get getters => _getters; + final _gettersPairLookup = {}; + final _fieldsPairLookup = {}; + + /// Creates a new instance of [AvoidFinalWithGetterVisitor] + AvoidFinalWithGetterVisitor(this._rule); @override void visitMethodDeclaration(MethodDeclaration node) { + super.visitMethodDeclaration(node); + if (node case MethodDeclaration( isGetter: true, @@ -21,29 +27,36 @@ class AvoidFinalWithGetterVisitor extends RecursiveAstVisitor { isAbstract: false, isPublic: true, ) - ) + ), + getterReferenceId: final getterId?, )) { - final visitor = GetterVariableVisitor(node); - node.parent?.accept(visitor); + _gettersPairLookup[getterId] = node; - final variable = visitor.variable; - - if (variable != null) { - _getters.add(FinalWithGetterInfo(node, variable)); + if (_fieldsPairLookup.containsKey(getterId)) { + _rule.reportAtNode(node); } } - super.visitMethodDeclaration(node); } -} -/// Information about the final private field with a getter. -class FinalWithGetterInfo { - /// The getter method declaration. - final MethodDeclaration getter; + @override + void visitVariableDeclaration(VariableDeclaration node) { + super.visitVariableDeclaration(node); - /// The variable declaration. - final VariableDeclaration variable; + if (node + case VariableDeclaration( + declaredFragment: VariableFragment( + element: VariableElement( + isPrivate: true, + isFinal: true, + id: final variableId, + ) + ) + )) { + _fieldsPairLookup[variableId] = node; - /// Creates a new instance of [FinalWithGetterInfo] - const FinalWithGetterInfo(this.getter, this.variable); + if (_gettersPairLookup[variableId] case final getter?) { + _rule.reportAtNode(getter); + } + } + } } diff --git a/lib/src/lints/avoid_final_with_getter/visitors/getter_variable_visitor.dart b/lib/src/lints/avoid_final_with_getter/visitors/getter_variable_visitor.dart index 978b44d6..0e9b4195 100644 --- a/lib/src/lints/avoid_final_with_getter/visitors/getter_variable_visitor.dart +++ b/lib/src/lints/avoid_final_with_getter/visitors/getter_variable_visitor.dart @@ -1,9 +1,9 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/element/element.dart'; +import 'package:solid_lints/src/lints/avoid_final_with_getter/utils/getter_reference_id.dart'; -/// A visitor that checks the association of the getter with -/// the final private variable +/// A visitor that gets the final private variable associated with the getter. class GetterVariableVisitor extends RecursiveAstVisitor { final int? _getterId; VariableDeclaration? _variable; @@ -17,29 +17,19 @@ class GetterVariableVisitor extends RecursiveAstVisitor { @override void visitVariableDeclaration(VariableDeclaration node) { - final element = node.declaredFragment?.element; - if (element != null && - element.isPrivate && - element.isFinal && - element.id == _getterId) { + if (node + case VariableDeclaration( + declaredFragment: VariableFragment( + element: VariableElement( + isPrivate: true, + isFinal: true, + :final id, + ) + ) + ) when id == _getterId) { _variable = node; } super.visitVariableDeclaration(node); } } - -extension on MethodDeclaration { - int? get getterReferenceId { - if (body - case ExpressionFunctionBody( - expression: SimpleIdentifier( - element: PropertyAccessorElement(:final variable) - ) - )) { - return variable.id; - } - - return null; - } -} diff --git a/lib/src/lints/avoid_final_with_getter/visitors/variable_references_visitor.dart b/lib/src/lints/avoid_final_with_getter/visitors/variable_references_visitor.dart new file mode 100644 index 00000000..9f3fee2c --- /dev/null +++ b/lib/src/lints/avoid_final_with_getter/visitors/variable_references_visitor.dart @@ -0,0 +1,32 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/syntactic_entity.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; + +/// Finds all references to a variable declaration. +class VariableReferencesVisitor extends RecursiveAstVisitor { + final VariableDeclaration _variableDeclaration; + final List _references = []; + + /// List of found references to the variable declaration. + List get references => _references; + + /// Creates a new instance of [VariableReferencesVisitor] + VariableReferencesVisitor(this._variableDeclaration); + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + super.visitSimpleIdentifier(node); + + final variableId = _variableDeclaration.declaredFragment?.element.id; + final referencedVariableId = switch (node.element) { + PropertyAccessorElement(:final variable) => variable.id, + final element? => element.id, + _ => null, + }; + + if (referencedVariableId == variableId) { + _references.add(node); + } + } +} diff --git a/lint_test/avoid_final_with_getter_test.dart b/lint_test/avoid_final_with_getter_test.dart deleted file mode 100644 index 4f0fa323..00000000 --- a/lint_test/avoid_final_with_getter_test.dart +++ /dev/null @@ -1,35 +0,0 @@ -// ignore_for_file: type_annotate_public_apis, prefer_match_file_name, unused_local_variable - -/// Check final private field with getter fail -/// `avoid_final_with_getter` - -class Fail { - final int _myField = 0; - - // expect_lint: avoid_final_with_getter - int get myField => _myField; -} - -class FailOtherName { - final int _myField = 0; - - // expect_lint: avoid_final_with_getter - int get myFieldInt => _myField; -} - -class FailStatic { - static final int _myField = 0; - - // expect_lint: avoid_final_with_getter - static int get myField => _myField; -} - -class Skip { - final int _myField = 0; - - int get myField => _myField + 1; // it is not a getter for the field -} - -class Good { - final int myField = 0; -} diff --git a/test/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule_test.dart b/test/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule_test.dart new file mode 100644 index 00000000..2149a1c0 --- /dev/null +++ b/test/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule_test.dart @@ -0,0 +1,131 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidFinalWithGetterRuleTest); + }); +} + +@reflectiveTest +class AvoidFinalWithGetterRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = AvoidFinalWithGetterRule(); + super.setUp(); + } + + Future test_reports_on_getter_with_same_name_as_field() async { + await assertDiagnostics( + r''' +class Test { + final int _myField = 0; + + int get myField => _myField; +} +''', + [lint(44, 28)], + ); + } + + Future test_reports_on_getter_with_different_name_from_field() async { + await assertDiagnostics( + r''' +class Test { + final int _myField = 0; + + int get myFieldInt => _myField; +} +''', + [lint(44, 31)], + ); + } + + Future test_reports_on_static_getter_with_private_field() async { + await assertDiagnostics( + r''' +class Test { + static final int _myField = 0; + + static int get myField => _myField; +} +''', + [lint(51, 35)], + ); + } + + Future test_reports_on_getter_with_this_property_access() async { + await assertDiagnostics( + r''' +class Test { + final int _myField = 0; + + int get myField => this._myField; +} +''', + [lint(44, 33)], + ); + } + + Future + test_reports_on_block_body_getter_returning_private_field() async { + await assertDiagnostics( + r''' +class Test { + final int _myField = 0; + + int get myField { + return _myField; + } +} +''', + [lint(44, 42)], + ); + } + + Future test_does_not_report_on_getter_that_contains_logic() async { + await assertNoDiagnostics(r''' +class Test { + final int _myField = 0; + final int _myField2 = 1; + final int _myField3 = 2; + + int get myField => _myField + 1; + + int get myField2 => this._myField2 + 1; + + int get myField3 { + return this._myField3 + 1; + } +} +'''); + } + + Future test_does_not_report_on_public_final_field() async { + await assertNoDiagnostics(r''' +class Test { + final int myField = 0; +} +'''); + } + + Future + test_does_not_report_on_getter_returning_mutable_private_field() async { + await assertNoDiagnostics(r''' +class Test { + int _myField = 0; + int _myField2 = 1; + int _myField3 = 2; + + int get myField => _myField; + + int get myField2 => this._myField2; + + int get myField3 { + return _myField3; + } +} +'''); + } +} From c54bc508c43387b4adeebf114f996d897d3a502a Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 15 Jun 2026 21:09:36 +0300 Subject: [PATCH 15/49] refactor: migrate avoid_returning_widgets rule (#243) feat: also check FunctionDeclarationStatement --- lib/main.dart | 9 +- .../excluded_identifiers_list_parameter.dart | 2 +- .../avoid_returning_widgets_rule.dart | 84 ++--- .../avoid_returning_widgets_parameters.dart | 7 + .../avoid_returning_widgets_visitor.dart | 84 +++++ .../analysis_options.yaml | 11 - .../avoid_returning_widget_test.dart | 96 ------ pubspec.yaml | 1 + .../avoid_returning_widgets_rule_test.dart | 296 ++++++++++++++++++ 9 files changed, 423 insertions(+), 167 deletions(-) create mode 100644 lib/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart delete mode 100644 lint_test/avoid_returning_widget_test/analysis_options.yaml delete mode 100644 lint_test/avoid_returning_widget_test/avoid_returning_widget_test.dart create mode 100644 test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index bb876ec5..f2bab9aa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,8 @@ import 'package:solid_lints/src/lints/avoid_final_with_getter/avoid_final_with_g import 'package:solid_lints/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; +import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; +import 'package:solid_lints/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; @@ -36,9 +38,10 @@ class SolidLintsPlugin extends Plugin { AvoidDebugPrintInReleaseRule(), doubleLiteralFormatRule, ProperSuperCallsRule(), - // TODO: Add more lint rules and use analysisLoader - // for rules that need parameters - // For example: `CyclomaticComplexityRule(analysisLoader)` + AvoidReturningWidgetsRule( + analysisOptionsLoader: analysisLoader, + parametersParser: AvoidReturningWidgetsParameters.fromJson, + ), ]; for (final lintRule in lintRules) { diff --git a/lib/src/common/parameters/excluded_identifiers_list_parameter.dart b/lib/src/common/parameters/excluded_identifiers_list_parameter.dart index 8a4b1097..526201a4 100644 --- a/lib/src/common/parameters/excluded_identifiers_list_parameter.dart +++ b/lib/src/common/parameters/excluded_identifiers_list_parameter.dart @@ -82,7 +82,7 @@ class ExcludedIdentifiersListParameter { final classDeclaration = node.thisOrAncestorOfType(); return classDeclaration != null && - classDeclaration.name.toString() == className; + classDeclaration.namePart.typeName.lexeme == className; } } } diff --git a/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart b/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart index 6bd15911..c90f2d97 100644 --- a/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart +++ b/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart @@ -1,11 +1,9 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; +import 'package:solid_lints/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; -import 'package:solid_lints/src/utils/types_utils.dart'; /// A rule which warns about returning widgets from functions and methods. /// @@ -54,64 +52,38 @@ class AvoidReturningWidgetsRule /// This lint rule represents /// the error whether we return a widget. static const lintName = 'avoid_returning_widgets'; - static const _override = 'override'; - AvoidReturningWidgetsRule._(super.config); + static const _code = LintCode( + lintName, + 'Returning a widget from a function is considered an anti-pattern. ' + 'Unless you are overriding an existing method, ' + 'consider extracting your widget to a separate class.', + ); - /// Creates a new instance of [AvoidReturningWidgetsRule] - /// based on the lint configuration. - factory AvoidReturningWidgetsRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - paramsParser: AvoidReturningWidgetsParameters.fromJson, - problemMessage: (_) => - 'Returning a widget from a function is considered an anti-pattern. ' - 'Unless you are overriding an existing method, ' - 'consider extracting your widget to a separate class.', - ); + @override + DiagnosticCode get diagnosticCode => _code; - return AvoidReturningWidgetsRule._(rule); - } + /// Creates a new instance of [AvoidReturningWidgetsRule] + AvoidReturningWidgetsRule({ + required super.analysisOptionsLoader, + required super.parametersParser, + }) : super.withParameters( + name: _code.lowerCaseName, + description: _code.problemMessage, + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addDeclaration((node) { - // Check if declaration is function or method, - // simultaneously checks if return type is [DartType] - final DartType? returnType = switch (node) { - FunctionDeclaration(returnType: TypeAnnotation(:final type?)) || - MethodDeclaration(returnType: TypeAnnotation(:final type?)) => - type, - _ => null, - }; - - if (returnType == null) { - return; - } - - final isWidgetReturned = hasWidgetType(returnType); + super.registerNodeProcessors(registry, context); - final isIgnored = config.parameters.exclude.shouldIgnore(node); + final parameters = getParametersForContext(context) ?? + AvoidReturningWidgetsParameters.empty(); - final isOverriden = switch (node) { - FunctionDeclaration(:final functionExpression) => - functionExpression.parent is MethodDeclaration && - (functionExpression.parent! as MethodDeclaration) - .metadata - .any((m) => m.name.name == _override), - MethodDeclaration(:final metadata) => - metadata.any((m) => m.name.name == _override), - _ => false, - }; + final visitor = AvoidReturningWidgetsVisitor(this, parameters); - if (isWidgetReturned && !isOverriden && !isIgnored) { - reporter.atNode(node, code); - } - }); + registry.addCompilationUnit(this, visitor); } } diff --git a/lib/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart b/lib/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart index 7947ea64..14c27036 100644 --- a/lib/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart +++ b/lib/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart @@ -11,6 +11,13 @@ class AvoidReturningWidgetsParameters { required this.exclude, }); + /// Empty [AvoidReturningWidgetsParameters] model, excludes nothing. + factory AvoidReturningWidgetsParameters.empty() { + return AvoidReturningWidgetsParameters( + exclude: ExcludedIdentifiersListParameter(exclude: []), + ); + } + /// Method for creating from json data factory AvoidReturningWidgetsParameters.fromJson(Map json) { return AvoidReturningWidgetsParameters( diff --git a/lib/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart b/lib/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart new file mode 100644 index 00000000..b81f4c39 --- /dev/null +++ b/lib/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart @@ -0,0 +1,84 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; +import 'package:solid_lints/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; + +/// A visitor that reports on functions that return widgets. +class AvoidReturningWidgetsVisitor extends RecursiveAstVisitor { + final AvoidReturningWidgetsRule _rule; + final AvoidReturningWidgetsParameters _parameters; + + /// Creates a new instance of [AvoidReturningWidgetsVisitor] + AvoidReturningWidgetsVisitor(this._rule, this._parameters); + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + super.visitFunctionDeclaration(node); + + _visitDeclaration(node); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + super.visitMethodDeclaration(node); + + _visitDeclaration(node); + } + + @override + void visitFunctionDeclarationStatement(FunctionDeclarationStatement node) { + super.visitFunctionDeclarationStatement(node); + + _visitDeclaration(node.functionDeclaration); + } + + void _visitDeclaration(Declaration node) { + if (node is! FunctionDeclaration && node is! MethodDeclaration) { + return; + } + + final returnType = switch (node) { + Declaration( + declaredFragment: ExecutableFragment( + element: ExecutableElement(type: FunctionType(:final returnType)) + ) + ) => + returnType, + MethodDeclaration(returnType: TypeAnnotation(:final type)) => type, + FunctionDeclaration(returnType: TypeAnnotation(:final type)) => type, + _ => null, + }; + if (returnType == null) return; + + final isWidgetReturned = hasWidgetType(returnType); + if (!isWidgetReturned) return; + + final isIgnored = _parameters.exclude.shouldIgnore(node); + if (isIgnored) return; + + if (_isOverridden(node)) return; + + _rule.reportAtNode(node); + } + + bool _isOverridden(Declaration node) { + return switch (node) { + Declaration( + declaredFragment: Fragment( + element: Element( + name: final String name, + enclosingElement: final InterfaceElement enclosingElement + ) + ), + ) => + enclosingElement.getInheritedMember( + Name.forLibrary(enclosingElement.library, name), + ) != + null, + _ => false, + }; + } +} diff --git a/lint_test/avoid_returning_widget_test/analysis_options.yaml b/lint_test/avoid_returning_widget_test/analysis_options.yaml deleted file mode 100644 index 0953ea1e..00000000 --- a/lint_test/avoid_returning_widget_test/analysis_options.yaml +++ /dev/null @@ -1,11 +0,0 @@ -analyzer: - plugins: - - ../custom_lint - -custom_lint: - rules: - - avoid_returning_widgets: - exclude: - - class_name: ExcludeWidget - method_name: excludeWidgetMethod - - method_name: excludeMethod diff --git a/lint_test/avoid_returning_widget_test/avoid_returning_widget_test.dart b/lint_test/avoid_returning_widget_test/avoid_returning_widget_test.dart deleted file mode 100644 index ad4398ba..00000000 --- a/lint_test/avoid_returning_widget_test/avoid_returning_widget_test.dart +++ /dev/null @@ -1,96 +0,0 @@ -// ignore_for_file: unused_element, prefer_match_file_name -// ignore_for_file: member_ordering - -/// Check returning a widget fail -/// `avoid_returning_widgets` - -import 'package:flutter/material.dart'; - -// expect_lint: avoid_returning_widgets -Widget avoidReturningWidgets() => const SizedBox(); - -class BaseWidget extends StatelessWidget { - const BaseWidget({super.key}); - - // Not allowed even though overriding it is alllowed - // expect_lint: avoid_returning_widgets - Widget get box => SizedBox(); - - // expect_lint: avoid_returning_widgets - Widget decoratedBox() { - return DecoratedBox(decoration: BoxDecoration()); - } - - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} - -class MyWidget extends BaseWidget { - const MyWidget({super.key}); - - // expect_lint: avoid_returning_widgets - Widget _test1() => const SizedBox(); - - // expect_lint: avoid_returning_widgets - Widget _test2() { - return const SizedBox( - child: SizedBox(), - ); - } - - // expect_lint: avoid_returning_widgets - Widget get _test3 => const SizedBox(); - - // Allowed - @override - Widget decoratedBox() { - return super.decoratedBox(); - } - - // Allowed - @override - Widget get box => ColoredBox(color: Colors.pink); - - // Allowed - @override - Widget build(BuildContext context) { - return const SizedBox(); - } -} - -// expect_lint: avoid_returning_widgets -Widget build() { - return Offstage(); -} - -// no lint -SizedBox excludeMethod() => const SizedBox(); - -class ExcludeWidget extends StatelessWidget { - const ExcludeWidget({super.key}); - - @override - Widget build(BuildContext context) { - return const Placeholder(); - } - - // no lint - Widget excludeWidgetMethod() => const SizedBox(); - - // expect_lint: avoid_returning_widgets - Widget excludeWidgetMethod2() => const SizedBox(); -} - -class NotExcludeWidget extends StatelessWidget { - const NotExcludeWidget({super.key}); - - @override - Widget build(BuildContext context) { - return const Placeholder(); - } - - // expect_lint: avoid_returning_widgets - Widget excludeWidgetMethod() => const SizedBox(); -} diff --git a/pubspec.yaml b/pubspec.yaml index 7b7dd9b2..b1a226bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: glob: ^2.1.3 path: ^1.9.1 yaml: ^3.1.3 + # These packages are required for pana analysis to run correctly test: ^1.25.14 dev_dependencies: diff --git a/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart b/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart new file mode 100644 index 00000000..f780c3df --- /dev/null +++ b/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart @@ -0,0 +1,296 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; +import 'package:solid_lints/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidReturningWidgetsRuleTest); + }); +} + +@reflectiveTest +class AvoidReturningWidgetsRuleTest extends AnalysisRuleTest { + static const _importFlutterWidgets = "import 'package:flutter/widgets.dart';"; + static const _mockFlutterWidgetsContent = ''' +abstract class Widget { + final String key; + + const Widget({required this.key}); +} + +class StatelessWidget implements Widget { + const StatelessWidget({super.key}); + + @override + Widget build(BuildContext context); +} + +class StatefulWidget implements Widget { + const StatefulWidget({super.key}); + + @override + Widget build(BuildContext context); +} + +abstract interface class BuildContext {} + +class Placeholder extends StatelessWidget { + const Placeholder({super.key}); + + @override + Widget build(BuildContext context) => throw 'unimplemented'; +} + +class SizedBox extends Widget { + final Widget? child; + + const SizedBox({this.child}); + + @override + Widget build(BuildContext context) => child ?? const SizedBox(); +} + +class BoxDecoration extends Widget { + const BoxDecoration(); + + @override + Widget build(BuildContext context) => throw 'unimplemented'; +} + +class DecoratedBox extends Widget { + const DecoratedBox({required this.decoration}); + + final BoxDecoration decoration; + + @override + Widget build(BuildContext context) => throw 'unimplemented'; +} +'''; + static const _mockAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + avoid_returning_widgets: + exclude: + - class_name: ExcludeWidget + method_name: excludeWidgetMethod + - method_name: excludeMethod + '''; + + void _addBaseWidgetFile() { + newFile('$testPackageLibPath/base_widget.dart', ''' +$_importFlutterWidgets +class BaseWidget extends StatelessWidget { + const BaseWidget({super.key}); + + Widget get box => SizedBox(); + + Widget decoratedBox() => DecoratedBox(decoration: BoxDecoration()); + + set box(Widget value) { + throw 'unimplemented'; + } +} +'''); + } + + @override + void setUp() { + rule = AvoidReturningWidgetsRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + parametersParser: AvoidReturningWidgetsParameters.fromJson, + ); + newPackage('flutter') + ..addFile('lib/widgets.dart', _mockFlutterWidgetsContent); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); + } + + Future test_reports_on_static_function() async { + await assertDiagnostics( + ''' +$_importFlutterWidgets + +Widget avoidReturningWidgets() => const SizedBox(); + +Widget build() { + return SizedBox(); +} +''', + [lint(40, 51), lint(93, 39)], + ); + } + + Future test_reports_on_methods() async { + await assertDiagnostics( + ''' +$_importFlutterWidgets + +class BaseWidget extends StatelessWidget { + const BaseWidget({super.key}); + + Widget decoratedBox() { + return DecoratedBox(decoration: BoxDecoration()); + } +} +''', + [lint(119, 81)], + ); + } + + Future test_reports_on_getters_but_not_setters() async { + await assertDiagnostics( + ''' +$_importFlutterWidgets + +class BaseWidget extends StatelessWidget { + const BaseWidget({super.key}); + + Widget get box => SizedBox(); + + set box(Widget value) { + throw 'unimplemented'; + } +} +''', + [lint(119, 29)], + ); + } + + Future test_reports_on_private_members() async { + _addBaseWidgetFile(); + + await assertDiagnostics( + ''' +$_importFlutterWidgets +import 'base_widget.dart'; + +class MyWidget extends BaseWidget { + const MyWidget({super.key}); + + Widget _test1() => const SizedBox(); + + Widget _test2() { + return const SizedBox( + child: SizedBox(), + ); + } + + Widget get _test3 => const SizedBox(); + + @override + Widget decoratedBox() { + return super.decoratedBox(); + } + + @override + Widget get box => SizedBox(); + + @override + Widget build(BuildContext context) { + return const SizedBox(); + } +} +''', + [ + lint(137, 36), + lint(177, 80), + lint(261, 38), + ], + ); + } + + Future test_does_not_report_on_overridden_members() async { + _addBaseWidgetFile(); + + // Shouldn't report even if not annotated with @override + await assertNoDiagnostics( + ''' +$_importFlutterWidgets +import 'base_widget.dart'; + +class MyWidget extends BaseWidget { + const MyWidget({super.key}); + + Widget decoratedBox() { + return super.decoratedBox(); + } + + Widget get box => SizedBox(); + + Widget build(BuildContext context) { + return const SizedBox(); + } +} +''', + ); + } + + Future test_does_not_report_on_excluded() async { + await assertNoDiagnostics( + ''' +$_importFlutterWidgets + +SizedBox excludeMethod() => const SizedBox(); + +class ExcludeWidget extends StatelessWidget { + const ExcludeWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } + + Widget excludeWidgetMethod() => const SizedBox(); +} + +''', + ); + } + + Future test_reports_on_non_matching_excluded() async { + await assertDiagnostics( + ''' +$_importFlutterWidgets + +SizedBox excludeMethod() => const SizedBox(); + +class ExcludeWidget extends StatelessWidget { + const ExcludeWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } + + Widget notExcludeWidgetMethod() => const Placeholder(); +} + +class NotExcludeWidget extends StatelessWidget { + const NotExcludeWidget({super.key}); + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } + + Widget excludeWidgetMethod() => const SizedBox(); +} +''', + [ + lint(260, 55), + lint(498, 49), + ], + ); + } +} From 13fdf86d5690e0ba929bfa9b3b9aee7c1ec1b697 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Thu, 18 Jun 2026 11:55:27 +0300 Subject: [PATCH 16/49] refactor: migrate avoid_unnecessary_type_assertions (#282) * refactor: migrate unnecessary_type_assertion * refactor: migrate unnecessary_type_assertion fix * fix: use fix kind instead of assist kind * fix: use thisOrAncestorOfType to get the nearest node of type T fix: remove leading space when removing is operator * feat: display removed part in the fix message refactor: simplify removal code * fix: handle custom iterable types * test: add test for custom iterables with subtypes * refactor: improve readability * fix: improve AutoTestLintOffsets reliability handles edge cases when there are multiple similar code fragments --- lib/main.dart | 9 + ...void_unnecessary_type_assertions_rule.dart | 150 +++---------- ...avoid_unnecessary_type_assertions_fix.dart | 96 +++++---- .../unnecessary_is_expression_visitor.dart | 63 ++++++ .../unnecessary_where_type_visitor.dart | 74 +++++++ ...void_unnecessary_type_assertions_test.dart | 47 ----- test/lints/auto_test_lint_offsets.dart | 55 +++++ ...unnecessary_type_assertions_rule_test.dart | 199 ++++++++++++++++++ 8 files changed, 491 insertions(+), 202 deletions(-) create mode 100644 lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_is_expression_visitor.dart create mode 100644 lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_where_type_visitor.dart delete mode 100644 lint_test/avoid_unnecessary_type_assertions_test.dart create mode 100644 test/lints/auto_test_lint_offsets.dart create mode 100644 test/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index f2bab9aa..018d598f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,8 @@ import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; import 'package:solid_lints/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; @@ -30,11 +32,14 @@ class SolidLintsPlugin extends Plugin { void register(PluginRegistry registry) { final analysisLoader = AnalysisOptionsLoader(); + final avoidUnnecessaryTypeAssertionsRule = + AvoidUnnecessaryTypeAssertionsRule(); final doubleLiteralFormatRule = DoubleLiteralFormatRule(); final lintRules = [ AvoidFinalWithGetterRule(), AvoidGlobalStateRule(), AvoidNonNullAssertionRule(), + avoidUnnecessaryTypeAssertionsRule, AvoidDebugPrintInReleaseRule(), doubleLiteralFormatRule, ProperSuperCallsRule(), @@ -56,5 +61,9 @@ class SolidLintsPlugin extends Plugin { AvoidFinalWithGetterRule.code, AvoidFinalWithGetterFix.new, ); + registry.registerFixForRule( + avoidUnnecessaryTypeAssertionsRule.diagnosticCode, + AvoidUnnecessaryTypeAssertionsFix.new, + ); } } diff --git a/lib/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart b/lib/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart index 6f95a11d..906c934b 100644 --- a/lib/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart +++ b/lib/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart @@ -1,134 +1,52 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:analyzer/diagnostic/diagnostic.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:analyzer/source/source_range.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; -import 'package:solid_lints/src/utils/typecast_utils.dart'; -import 'package:solid_lints/src/utils/types_utils.dart'; - -part 'fixes/avoid_unnecessary_type_assertions_fix.dart'; - -/// The name of 'is' operator -const operatorIsName = 'is'; - -/// The name of 'whereType' method -const whereTypeMethodName = 'whereType'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_is_expression_visitor.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_where_type_visitor.dart'; /// Warns about unnecessary usage of `is` and `whereType` operators. /// /// ### Example: -/// #### BAD: -/// ```dart -/// final testList = [1.0, 2.0, 3.0]; -/// final result = testList is List; // LINT -/// final negativeResult = testList is! List; // LINT -/// testList.whereType(); // LINT -/// -/// final double d = 2.0; -/// final casted = d is double; // LINT -/// ``` -/// -/// #### GOOD: -/// ```dart -/// final dynamicList = [1.0, 2.0]; -/// dynamicList.whereType(); -/// -/// final double? nullableD = 2.0; -/// // casting `Type? is Type` is allowed -/// final castedD = nullableD is double; -/// ``` -class AvoidUnnecessaryTypeAssertions extends SolidLintRule { +/// {@macro solid_lints.avoid_unnecessary_type_assertions.example_is} +/// {@macro solid_lints.avoid_unnecessary_type_assertions.example_where} +class AvoidUnnecessaryTypeAssertionsRule extends AnalysisRule { + /// The name of 'is' operator + static const operatorIsName = 'is'; + + /// The name of 'whereType' method + static const whereTypeMethodName = 'whereType'; + /// This lint rule represents /// the error whether we use bad formatted double literals. static const lintName = 'avoid_unnecessary_type_assertions'; - static const _unnecessaryIsCode = LintCode( - name: lintName, - problemMessage: "Unnecessary usage of the '$operatorIsName' operator.", + static const _unnecessaryTypeAssertionsCode = LintCode( + lintName, + "Unnecessary usage of the {0}.", ); - static const _unnecessaryWhereTypeCode = LintCode( - name: lintName, - problemMessage: "Unnecessary usage of the '$whereTypeMethodName' method.", - ); - - AvoidUnnecessaryTypeAssertions._(super.config); - - /// Creates a new instance of [AvoidUnnecessaryTypeAssertions] - /// based on the lint configuration. - factory AvoidUnnecessaryTypeAssertions.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (_) => "Unnecessary usage of typecast operators.", - ); - - return AvoidUnnecessaryTypeAssertions._(rule); - } - @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, - ) { - context.registry.addIsExpression((node) { - if (_isUnnecessaryIsExpression(node)) { - reporter.atNode(node, _unnecessaryIsCode); - } - }); + DiagnosticCode get diagnosticCode => _unnecessaryTypeAssertionsCode; - context.registry.addMethodInvocation((node) { - if (_isUnnecessaryWhereType(node)) { - reporter.atNode(node, _unnecessaryWhereTypeCode); - } - }); - } + /// Creates a new instance of [AvoidUnnecessaryTypeAssertionsRule] + AvoidUnnecessaryTypeAssertionsRule() + : super( + name: lintName, + description: "Unnecessary usage of typecast operators.", + ); @override - List getFixes() => [_UnnecessaryTypeAssertionsFix()]; - - bool _isUnnecessaryIsExpression(IsExpression node) { - final objectType = node.expression.staticType; - final castedType = node.type.type; - - if (objectType == null || castedType == null) { - return false; - } - final typeCast = TypeCast( - source: objectType, - target: castedType, - isReversed: node.notOperator != null, - ); - return typeCast.isUnnecessaryTypeCheck; - } - - bool _isUnnecessaryWhereType(MethodInvocation node) { - if (node - case MethodInvocation( - methodName: Identifier(name: whereTypeMethodName), - target: Expression(staticType: final targetType), - realTarget: Expression(staticType: final realTargetType), - typeArguments: TypeArgumentList(arguments: final arguments), - ) - when targetType is ParameterizedType && - isIterable(realTargetType) && - arguments.isNotEmpty) { - final objectType = targetType.typeArguments.first; - final castedType = arguments.first.type; - - if (castedType == null) { - return false; - } + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + super.registerNodeProcessors(registry, context); - final typeCast = TypeCast(source: objectType, target: castedType); + final unnecessaryIsExpressionVisitor = UnnecessaryIsExpressionVisitor(this); + registry.addIsExpression(this, unnecessaryIsExpressionVisitor); - return typeCast.isUnnecessaryTypeCheck; - } else { - return false; - } + final unnecessaryWhereTypeVisitor = UnnecessaryWhereTypeVisitor(this); + registry.addMethodInvocation(this, unnecessaryWhereTypeVisitor); } } diff --git a/lib/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart b/lib/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart index 3c429e66..814520c1 100644 --- a/lib/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart +++ b/lib/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart @@ -1,50 +1,68 @@ -part of '../avoid_unnecessary_type_assertions_rule.dart'; +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; /// A Quick fix for `avoid_unnecessary_type_assertions` rule /// Suggests to remove unnecessary assertions -class _UnnecessaryTypeAssertionsFix extends DartFix { +class AvoidUnnecessaryTypeAssertionsFix extends ResolvedCorrectionProducer { + static const _avoidUnnecessaryTypeAssertionsFixKind = FixKind( + 'solid_lints.fix.${AvoidUnnecessaryTypeAssertionsRule.lintName}', + DartFixKindPriority.standard, + 'Remove the unnecessary {0}', + ); + + SourceRange? _partToRemove; + + @override + List? get fixArguments => [ + if (_partToRemove case final partToRemove?) + '"${utils.getRangeText(partToRemove).trim()}"' + else + 'type assertion', + ]; + @override - void run( - CustomLintResolver resolver, - ChangeReporter reporter, - CustomLintContext context, - Diagnostic analysisError, - List others, - ) { - context.registry.addIsExpression((node) { - if (analysisError.sourceRange.intersects(node.sourceRange)) { - _addDeletion(reporter, operatorIsName, node, node.isOperator.offset); - } - }); - - context.registry.addMethodInvocation((node) { - if (analysisError.sourceRange.intersects(node.sourceRange)) { - _addDeletion( - reporter, - whereTypeMethodName, - node, - node.operator?.offset ?? node.offset, - ); - } - }); + FixKind get fixKind => _avoidUnnecessaryTypeAssertionsFixKind; + + @override + CorrectionApplicability get applicability => + CorrectionApplicability.automatically; + + /// Creates a new instance of [AvoidUnnecessaryTypeAssertionsFix] + AvoidUnnecessaryTypeAssertionsFix({required super.context}); + + @override + Future compute(ChangeBuilder builder) async { + final isExpressionNode = node.thisOrAncestorOfType(); + if (isExpressionNode != null) { + final operatorOffset = isExpressionNode.isOperator.offset - 1; + _partToRemove = _removedPartRange(isExpressionNode, operatorOffset); + } + + final whereTypeNode = node.thisOrAncestorOfType(); + if (whereTypeNode != null && _partToRemove == null) { + final operatorOffset = + whereTypeNode.operator?.offset ?? whereTypeNode.offset; + _partToRemove = _removedPartRange(whereTypeNode, operatorOffset); + } + + final partToRemove = _partToRemove; + if (partToRemove == null) return; + + await builder.addDartFileEdit( + file, + (builder) => builder.addDeletion(partToRemove), + ); } - void _addDeletion( - ChangeReporter reporter, - String itemToDelete, - Expression node, - int operatorOffset, - ) { + SourceRange _removedPartRange(Expression node, int operatorOffset) { final targetNameLength = operatorOffset - node.offset; final removedPartLength = node.length - targetNameLength; - final changeBuilder = reporter.createChangeBuilder( - message: "Remove unnecessary '$itemToDelete'", - priority: 1, - ); - - changeBuilder.addDartFileEdit((builder) { - builder.addDeletion(SourceRange(operatorOffset, removedPartLength)); - }); + return SourceRange(operatorOffset, removedPartLength); } } diff --git a/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_is_expression_visitor.dart b/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_is_expression_visitor.dart new file mode 100644 index 00000000..7fd7b1d2 --- /dev/null +++ b/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_is_expression_visitor.dart @@ -0,0 +1,63 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; +import 'package:solid_lints/src/utils/typecast_utils.dart'; + +/// Visitor for [AvoidUnnecessaryTypeAssertionsRule]. +/// Reports on unnecessary usage of 'is' operator. +/// +/// ### Example: +/// {@template solid_lints.avoid_unnecessary_type_assertions.example_is} +/// #### BAD: +/// ```dart +/// final testList = [1.0, 2.0, 3.0]; +/// final result = testList is List; // LINT +/// final negativeResult = testList is! List; // LINT +/// +/// final double d = 2.0; +/// final casted = d is double; // LINT +/// ``` +/// +/// #### GOOD: +/// ```dart +/// final double? nullableD = 2.0; +/// // casting `Type? is Type` is allowed +/// final castedD = nullableD is double; +/// ``` +/// {@endtemplate} +class UnnecessaryIsExpressionVisitor extends SimpleAstVisitor { + final AvoidUnnecessaryTypeAssertionsRule _rule; + + /// Creates a new instance of [UnnecessaryIsExpressionVisitor]. + UnnecessaryIsExpressionVisitor(this._rule); + + @override + void visitIsExpression(IsExpression node) { + super.visitIsExpression(node); + + if (!_isUnnecessaryIsExpression(node)) return; + + _rule.reportAtNode( + node, + arguments: [ + "'${AvoidUnnecessaryTypeAssertionsRule.operatorIsName}' operator", + ], + ); + } + + bool _isUnnecessaryIsExpression(IsExpression node) { + final objectType = node.expression.staticType; + final castedType = node.type.type; + + if (objectType == null || castedType == null) { + return false; + } + + final typeCast = TypeCast( + source: objectType, + target: castedType, + isReversed: node.notOperator != null, + ); + return typeCast.isUnnecessaryTypeCheck; + } +} diff --git a/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_where_type_visitor.dart b/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_where_type_visitor.dart new file mode 100644 index 00000000..13bca7f5 --- /dev/null +++ b/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_where_type_visitor.dart @@ -0,0 +1,74 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; +import 'package:solid_lints/src/utils/typecast_utils.dart'; + +/// Visitor for [AvoidUnnecessaryTypeAssertionsRule]. +/// Reports on unnecessary usage of 'whereType' method. +/// +/// ### Example: +/// {@template solid_lints.avoid_unnecessary_type_assertions.example_where} +/// #### BAD: +/// ```dart +/// final testList = [1.0, 2.0, 3.0]; +/// testList.whereType(); // LINT +/// ``` +/// +/// #### GOOD: +/// ```dart +/// final dynamicList = [1.0, 2.0]; +/// dynamicList.whereType(); +/// ``` +/// {@endtemplate} +class UnnecessaryWhereTypeVisitor extends SimpleAstVisitor { + final AvoidUnnecessaryTypeAssertionsRule _rule; + + /// Creates a new instance of [UnnecessaryWhereTypeVisitor] + UnnecessaryWhereTypeVisitor(this._rule); + + @override + void visitMethodInvocation(MethodInvocation node) { + super.visitMethodInvocation(node); + + if (!_isUnnecessaryWhereType(node)) return; + + _rule.reportAtNode( + node, + arguments: [ + "'${AvoidUnnecessaryTypeAssertionsRule.whereTypeMethodName}' method", + ], + ); + } + + bool _isUnnecessaryWhereType(MethodInvocation node) { + if (node case MethodInvocation( + methodName: Identifier( + name: AvoidUnnecessaryTypeAssertionsRule.whereTypeMethodName, + ), + target: Expression(staticType: final InterfaceType targetType), + typeArguments: TypeArgumentList(:final arguments), + ) when arguments.isNotEmpty) { + final targetIterableType = switch (targetType) { + InterfaceType(isDartCoreIterable: true) => targetType, + InterfaceType(:final allSupertypes) => allSupertypes.firstWhereOrNull( + (e) => e.isDartCoreIterable, + ), + }; + + final objectType = targetIterableType?.typeArguments.firstOrNull; + final castedType = arguments.first.type; + + if (castedType == null || objectType == null) { + return false; + } + + final typeCast = TypeCast(source: objectType, target: castedType); + + return typeCast.isUnnecessaryTypeCheck; + } + + return false; + } +} diff --git a/lint_test/avoid_unnecessary_type_assertions_test.dart b/lint_test/avoid_unnecessary_type_assertions_test.dart deleted file mode 100644 index b3f2f3c6..00000000 --- a/lint_test/avoid_unnecessary_type_assertions_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -// ignore_for_file: prefer_const_declarations, prefer_match_file_name, cyclomatic_complexity, unused_element -// ignore_for_file: unnecessary_nullable_for_final_variable_declarations -// ignore_for_file: unnecessary_type_check -// ignore_for_file: unused_local_variable - -/// Check the `avoid_unnecessary_type_assertions` rule - -void fun() { - final testList = [1.0, 2.0, 3.0]; - // expect_lint: avoid_unnecessary_type_assertions - final result = testList is List; - - // expect_lint: avoid_unnecessary_type_assertions - final negativeResult = testList is! List; - - // to check quick-fix => testList.length - // expect_lint: avoid_unnecessary_type_assertions - testList.whereType().length; - - final dynamicList = [1.0, 2.0]; - dynamicList.whereType(); - - // expect_lint: avoid_unnecessary_type_assertions - [1.0, 2.0].whereType(); - - final double d = 2.0; - // expect_lint: avoid_unnecessary_type_assertions - final casted = d is double; - - // expect_lint: avoid_unnecessary_type_assertions - final negativeCasted = d is! double; - - final double? nullableD = 2.0; - // casting `Type? is Type` is allowed - final castedD = nullableD is double; -} - -class _A {} - -class _B extends _A {} - -class _C extends _A {} - -void noLint() { - final _A a = _B(); - if (a is! _C) return; -} diff --git a/test/lints/auto_test_lint_offsets.dart b/test/lints/auto_test_lint_offsets.dart new file mode 100644 index 00000000..a9b4b298 --- /dev/null +++ b/test/lints/auto_test_lint_offsets.dart @@ -0,0 +1,55 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart'; +import 'package:collection/collection.dart'; + +mixin AutoTestLintOffsets on AnalysisRuleTest { + int _nextPlaceholderId = 0; + final Map _placeholderToCode = {}; + + Future assertAutoDiagnostics(String source) async { + try { + final placeholders = _placeholderToCode.entries + .map( + (entry) => ( + placeholder: entry.key, + code: entry.value, + index: source.indexOf(entry.key), + ), + ) + .sorted((a, b) => a.index.compareTo(b.index)); + + final expectedDiagnostics = []; + int replacedPlaceholdersDelta = 0; + + for (final match in placeholders) { + if (match.index == -1) { + throw StateError( + 'Expected lint placeholder "${match.placeholder}" was not found in source.', + ); + } + + expectedDiagnostics.add( + lint(match.index + replacedPlaceholdersDelta, match.code.length), + ); + replacedPlaceholdersDelta += + match.code.length - match.placeholder.length; + } + + final resolvedSource = _placeholderToCode.entries.fold( + source, + (resolved, entry) => resolved.replaceFirst(entry.key, entry.value), + ); + + await assertDiagnostics(resolvedSource, expectedDiagnostics); + } finally { + _nextPlaceholderId = 0; + _placeholderToCode.clear(); + } + } + + String expectLint(String code) { + final placeholder = '__AUTO_TEST_LINT_${_nextPlaceholderId++}__'; + _placeholderToCode[placeholder] = code; + return placeholder; + } +} diff --git a/test/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart b/test/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart new file mode 100644 index 00000000..602e60ba --- /dev/null +++ b/test/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart @@ -0,0 +1,199 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidUnnecessaryTypeAssertionsRuleTest); + }); +} + +@reflectiveTest +class AvoidUnnecessaryTypeAssertionsRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + @override + void setUp() { + rule = AvoidUnnecessaryTypeAssertionsRule(); + super.setUp(); + } + + @override + String get analysisRule => AvoidUnnecessaryTypeAssertionsRule.lintName; + + Future + test_does_not_report_if_is_expression_checks_nullable_source() async { + await assertNoDiagnostics(r''' +void fun() { + final double? nullableD = 2.0; + final castedD = nullableD is double; +} +'''); + } + + Future + test_does_not_report_if_is_not_expression_checks_unrelated_subtype() async { + await assertNoDiagnostics(r''' +class _A {} + +class _B extends _A {} + +class _C extends _A {} + +void fun() { + final _A a = _B(); + if (a is! _C) return; +} +'''); + } + + Future + test_does_not_report_if_is_expression_has_different_generic_type_argument() async { + await assertNoDiagnostics(r''' +void fun() { + final nums = [1, 2, 3]; + final result = nums is List; +} +'''); + } + + Future + test_does_not_report_if_where_type_filters_dynamic_iterable() async { + await assertNoDiagnostics(r''' +void fun() { + final dynamicList = [1.0, 2.0]; + dynamicList.whereType(); +} +'''); + } + + Future + test_does_not_report_if_where_type_has_different_generic_type_argument() async { + await assertNoDiagnostics(r''' +void fun() { + final nums = [1, 2, 3]; + nums.whereType(); +} +'''); + } + + Future test_does_not_report_if_where_type_omits_type_argument() async { + await assertNoDiagnostics(r''' +void fun() { + final values = [1.0, 2.0, 3.0]; + values.whereType(); +} +'''); + } + + Future test_reports_if_is_expression_checks_exact_generic_type() async { + await assertAutoDiagnostics(''' +// ignore_for_file: unnecessary_type_check + +void fun() { + final testList = [1.0, 2.0, 3.0]; + final result = ${expectLint('testList is List')}; +} +'''); + } + + Future + test_reports_if_is_not_expression_checks_exact_generic_type() async { + await assertAutoDiagnostics(''' +// ignore_for_file: unnecessary_type_check + +void fun() { + final testList = [1.0, 2.0, 3.0]; + final result = ${expectLint('testList is! List')}; +} +'''); + } + + Future test_reports_if_is_expression_checks_exact_scalar_type() async { + await assertAutoDiagnostics(''' +// ignore_for_file: unnecessary_type_check + +void fun() { + final double d = 2.0; + final casted = ${expectLint('d is double')}; +} +'''); + } + + Future + test_reports_if_is_not_expression_checks_exact_scalar_type() async { + await assertAutoDiagnostics(''' +// ignore_for_file: unnecessary_type_check + +void fun() { + final double d = 2.0; + final negativeCasted = ${expectLint('d is! double')}; +} +'''); + } + + Future test_reports_if_is_expression_checks_nullable_target() async { + await assertAutoDiagnostics(''' +// ignore_for_file: unnecessary_type_check + +void fun() { + final double d = 2.0; + final casted = ${expectLint('d is double?')}; +} +'''); + } + + Future test_reports_if_is_expression_checks_supertype() async { + await assertAutoDiagnostics(''' +// ignore_for_file: unnecessary_type_check + +void fun() { + final ints = [1, 2, 3]; + final result = ${expectLint('ints is Iterable')}; +} +'''); + } + + Future test_reports_if_where_type_filters_exact_type() async { + await assertAutoDiagnostics(''' +void fun() { + final testList = [1.0, 2.0, 3.0]; + ${expectLint('testList.whereType()')}.length; +} +'''); + } + + Future test_reports_if_where_type_filters_nullable_type() async { + await assertAutoDiagnostics(''' +void fun() { + ${expectLint('[1.0, 2.0].whereType()')}; +} +'''); + } + + Future test_reports_on_custom_types_with_default_generics() async { + await assertAutoDiagnostics(''' +// ignore_for_file: unnecessary_type_check + +abstract class MyIterable implements Iterable {} + +void test(MyIterable iterable) { + ${expectLint('iterable.whereType()')}; +} +'''); + } + + Future test_does_not_report_custom_types_when_using_subtype() async { + await assertNoDiagnostics(r''' +abstract class _A {} +abstract class _B extends _A {} + +abstract class AIterable implements Iterable<_A> {} + +void test(AIterable iterable) { + iterable.whereType<_B>(); +} +'''); + } +} From 4e0f6ac7c4de7fa3cded7d1da2a7c7553587dd27 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 18 Jun 2026 17:35:47 +0300 Subject: [PATCH 17/49] fix: update avoid_unnecessary_return_variable (#236) * fix: update avoid_unnecessary_return_variable to correctly traverse FunctionBody (#236) * refactor: simplify block retrieval in avoid_unnecessary_return_variable visitor * test: add test case for avoid_unnecessary_return_variable when variable is reused in nested blocks --------- Co-authored-by: Illia Aihistov --- ...d_unnecessary_return_variable_visitor.dart | 13 +++-- ...unnecessary_return_variable_rule_test.dart | 53 +++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/lib/src/lints/avoid_unnecessary_return_variable/visitors/avoid_unnecessary_return_variable_visitor.dart b/lib/src/lints/avoid_unnecessary_return_variable/visitors/avoid_unnecessary_return_variable_visitor.dart index ebfad8e4..95233f43 100644 --- a/lib/src/lints/avoid_unnecessary_return_variable/visitors/avoid_unnecessary_return_variable_visitor.dart +++ b/lib/src/lints/avoid_unnecessary_return_variable/visitors/avoid_unnecessary_return_variable_visitor.dart @@ -15,7 +15,7 @@ class AvoidUnnecessaryReturnVariableVisitor extends SimpleAstVisitor { @override void visitReturnStatement(ReturnStatement node) { - final expr = node.expression; + final expr = node.expression?.unParenthesized; if (expr is! SimpleIdentifier) return; final element = expr.element; @@ -23,7 +23,8 @@ class AvoidUnnecessaryReturnVariableVisitor extends SimpleAstVisitor { if (!element.isFinal && !element.isConst) return; - final block = node.parent; + //get enclosing block function body + final block = node.thisOrAncestorOfType()?.block; if (block == null) return; final returnVariableUsageVisitor = ReturnVariableUsageVisitor( @@ -34,16 +35,18 @@ class AvoidUnnecessaryReturnVariableVisitor extends SimpleAstVisitor { block.visitChildren(returnVariableUsageVisitor); if (!returnVariableUsageVisitor.hasBadStatementCount) return; + //check if declaration statement is found + final declaration = returnVariableUsageVisitor.variableDeclaration; + if (declaration == null) return; + //it is 100% bad if return statement follows declaration if (!returnVariableUsageVisitor.foundTokensBetweenDeclarationAndReturn) { _rule.reportAtNode(node); return; } - final declaration = returnVariableUsageVisitor.variableDeclaration; - //check if immutable - final initializer = declaration?.initializer; + final initializer = declaration.initializer; if (initializer == null) return; if (!_isExpressionImmutable(initializer)) return; diff --git a/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart b/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart index d2e42b16..704cc16d 100644 --- a/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart +++ b/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart @@ -172,4 +172,57 @@ void _doNothing() {} [lint(304, 14)], ); } + + void test_does_not_report_if_return_is_cached_nested_block() async { + await assertNoDiagnostics(r''' +Future testAvoidUnnecessaryReturnVariableNestedBlock() async { + final cached = 'cached'; + if (cached.isNotEmpty) { + // Should NOT trigger the avoid_unnecessary_return_variable lint + return cached; + } + return null; +} +'''); + } + + void test_reports_if_return_is_cached_and_only_returned_nested_block() async { + await assertDiagnostics( + r''' +int test(bool b) { + final a = 3; + if (b) { + return a; + } + return 0; +} +''', + [lint(49, 9)], + ); + } + + void test_reports_if_return_in_parentheses() async { + await assertDiagnostics( + r''' +int test() { + final a = 3; + return (a); +} +''', + [lint(30, 11)], + ); + } + + void test_does_not_report_if_return_is_cached_and_used_after_return_nested_block() async { + await assertNoDiagnostics(r''' +int test(bool b) { + final a = 3; + if (b) { + return a; + } + return a + 1; } +'''); + } +} + From d123c1d121d8bbeb9c0b4b5f98a4c3c70be888d0 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 18 Jun 2026 17:39:49 +0300 Subject: [PATCH 18/49] fix: support await expressions in proper_super_calls (#291) Co-authored-by: Illia Aihistov --- .../visitors/proper_super_calls_visitor.dart | 5 +- .../proper_super_calls_rule_test.dart | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart b/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart index 6cbe4759..871938ad 100644 --- a/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart +++ b/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart @@ -103,7 +103,10 @@ class ProperSuperCallsVisitor extends SimpleAstVisitor { return false; } - final expression = statement.expression; + var expression = statement.expression.unParenthesized; + if (expression is AwaitExpression) { + expression = expression.expression.unParenthesized; + } return expression is MethodInvocation && expression.target is SuperExpression && diff --git a/test/lints/proper_super_calls/proper_super_calls_rule_test.dart b/test/lints/proper_super_calls/proper_super_calls_rule_test.dart index 8969e73c..c043945c 100644 --- a/test/lints/proper_super_calls/proper_super_calls_rule_test.dart +++ b/test/lints/proper_super_calls/proper_super_calls_rule_test.dart @@ -164,6 +164,54 @@ class MyWidgetState extends State { super.initState(); } } +''', + ); + } + + void test_no_report_for_async_correct_usage() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class MyWidgetState extends State { + @override + Future initState() async { + // ignore: use_of_void_result + await super.initState(); + print(''); + } + + @override + Future dispose() async { + print(''); + // ignore: use_of_void_result + await super.dispose(); + } +} +''', + ); + } + + void test_no_report_for_parenthesized_and_async_correct_usage() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class MyWidgetState extends State { + @override + Future initState() async { + // ignore: use_of_void_result + await (super.initState()); + print(''); + } + + @override + Future dispose() async { + print(''); + // ignore: use_of_void_result + (await super.dispose()); + } +} ''', ); } From addb4b37fcc00b8741254765eeb4a0da6e4b9ca8 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Fri, 19 Jun 2026 12:37:57 +0300 Subject: [PATCH 19/49] refactor: migrate avoid_unused_parameters (#290) * refactor: migrate avoid_unused_parameters * fix: check if current function expression is inside an excluded declaration --- lib/main.dart | 6 +- .../avoid_returning_widgets_rule.dart | 11 +- .../avoid_unused_parameters_rule.dart | 99 +++-- .../models/avoid_unused_parameters.dart | 20 - .../avoid_unused_parameters_parameters.dart | 27 ++ .../avoid_unused_parameters_visitor.dart | 72 ++-- lib/src/utils/parameter_utils.dart | 22 - lint_test/avoid_unused_parameters_test.dart | 243 ----------- .../avoid_unused_parameters_rule_test.dart | 384 ++++++++++++++++++ test/utils/fake_analysis_options_loader.dart | 15 + 10 files changed, 534 insertions(+), 365 deletions(-) delete mode 100644 lib/src/lints/avoid_unused_parameters/models/avoid_unused_parameters.dart create mode 100644 lib/src/lints/avoid_unused_parameters/models/avoid_unused_parameters_parameters.dart delete mode 100644 lint_test/avoid_unused_parameters_test.dart create mode 100644 test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart create mode 100644 test/utils/fake_analysis_options_loader.dart diff --git a/lib/main.dart b/lib/main.dart index 018d598f..708b9fce 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,9 +7,9 @@ import 'package:solid_lints/src/lints/avoid_final_with_getter/fixes/avoid_final_ import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; -import 'package:solid_lints/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart'; +import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; @@ -45,7 +45,9 @@ class SolidLintsPlugin extends Plugin { ProperSuperCallsRule(), AvoidReturningWidgetsRule( analysisOptionsLoader: analysisLoader, - parametersParser: AvoidReturningWidgetsParameters.fromJson, + ), + AvoidUnusedParametersRule( + analysisOptionsLoader: analysisLoader, ), ]; diff --git a/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart b/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart index c90f2d97..be2c6ac3 100644 --- a/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart +++ b/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart @@ -66,11 +66,11 @@ class AvoidReturningWidgetsRule /// Creates a new instance of [AvoidReturningWidgetsRule] AvoidReturningWidgetsRule({ required super.analysisOptionsLoader, - required super.parametersParser, }) : super.withParameters( - name: _code.lowerCaseName, - description: _code.problemMessage, - ); + name: _code.lowerCaseName, + description: _code.problemMessage, + parametersParser: AvoidReturningWidgetsParameters.fromJson, + ); @override void registerNodeProcessors( @@ -79,7 +79,8 @@ class AvoidReturningWidgetsRule ) { super.registerNodeProcessors(registry, context); - final parameters = getParametersForContext(context) ?? + final parameters = + getParametersForContext(context) ?? AvoidReturningWidgetsParameters.empty(); final visitor = AvoidReturningWidgetsVisitor(this, parameters); diff --git a/lib/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart b/lib/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart index 383425e3..ab9d7e9c 100644 --- a/lib/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart +++ b/lib/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart @@ -1,14 +1,21 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/lints/avoid_unused_parameters/models/avoid_unused_parameters.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_unused_parameters/models/avoid_unused_parameters_parameters.dart'; import 'package:solid_lints/src/lints/avoid_unused_parameters/visitors/avoid_unused_parameters_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; -/// Warns about unused function, method, constructor or factory parameters. +/// Warns about unused function, method, constructor, or factory parameters. +/// +/// Named parameters are always allowed because they document the API surface. +/// Parameters whose names consist only of underscores are also ignored. +/// Overridden methods and methods used as tear-offs are skipped. +/// +/// {@template solid_lints.avoid_unused_parameters.example} +/// ### Example /// -/// ### Example: /// #### BAD: +/// /// ```dart /// typedef MaxFun = int Function(int a, int b); /// final MaxFun bad = (int a, int b) => 1; // LINT @@ -18,6 +25,7 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// final optional = (int a, [int b = 0]) { // LINT /// return a; /// }; +/// /// void fun(String x) {} // LINT /// void fun2(String x, String y) { // LINT /// print(y); @@ -35,6 +43,7 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// ``` /// /// #### GOOD: +/// /// ```dart /// typedef MaxFun = int Function(int a, int b); /// final MaxFun good = (int a, int b) => a + b; @@ -47,63 +56,67 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// } /// /// class TestClass { -/// static void staticMethod(int _) {} // OK -/// void method(String _) {} // OK +/// static void staticMethod(int _) {} +/// void method(String _) {} /// -/// TestClass([int _]); // OK -/// factory TestClass.named(int _) { // OK +/// TestClass([int _]); +/// factory TestClass.named(int _) { /// return TestClass(); /// } /// } /// ``` /// /// #### Allowed: +/// /// ```dart /// typedef Named = String Function({required String text}); /// final Named named = ({required text}) { -/// return ''; +/// return ''; /// }; -/// /// ``` -class AvoidUnusedParametersRule extends SolidLintRule { - /// This lint rule represents - /// the error whether we use bad formatted double literals. - static const String lintName = 'avoid_unused_parameters'; +/// {@endtemplate} +class AvoidUnusedParametersRule + extends SolidLintRule { + /// The name of this lint rule. + static const lintName = 'avoid_unused_parameters'; - AvoidUnusedParametersRule._(super.config); + /// Reported when a parameter is declared but never read in the body. + /// + /// {@macro solid_lints.avoid_unused_parameters.example} + static const LintCode _code = LintCode( + lintName, + 'Avoid unused parameters.', + correctionMessage: + 'Remove the parameter or replace its name with underscores.', + ); - /// Creates a new instance of [AvoidUnusedParametersRule] - /// based on the lint configuration. - factory AvoidUnusedParametersRule.createRule( - CustomLintConfigs configs, - ) { - final rule = RuleConfig( - configs: configs, - name: lintName, - paramsParser: AvoidUnusedParameters.fromJson, - problemMessage: (_) => 'Parameter is unused.', - ); + @override + LintCode get diagnosticCode => _code; - return AvoidUnusedParametersRule._(rule); - } + /// Creates a new instance of [AvoidUnusedParametersRule]. + AvoidUnusedParametersRule({ + required super.analysisOptionsLoader, + }) : super.withParameters( + name: lintName, + description: + 'Warns about unused function, method, constructor, or factory ' + 'parameters.', + parametersParser: AvoidUnusedParametersParameters.fromJson, + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addDeclaration((node) { - final isIgnored = config.parameters.exclude.shouldIgnore(node); + super.registerNodeProcessors(registry, context); - if (isIgnored) return; + final parameters = + getParametersForContext(context) ?? + AvoidUnusedParametersParameters.empty(); - final visitor = AvoidUnusedParametersVisitor(); - node.accept(visitor); + final visitor = AvoidUnusedParametersVisitor(this, parameters); - for (final element in visitor.unusedParameters) { - reporter.atNode(element, code); - } - }); + registry.addCompilationUnit(this, visitor); } } diff --git a/lib/src/lints/avoid_unused_parameters/models/avoid_unused_parameters.dart b/lib/src/lints/avoid_unused_parameters/models/avoid_unused_parameters.dart deleted file mode 100644 index f4dac97b..00000000 --- a/lib/src/lints/avoid_unused_parameters/models/avoid_unused_parameters.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart'; - -/// A data model class that represents the "avoid returning widgets" input -/// parameters. -class AvoidUnusedParameters { - /// A list of methods that should be excluded from the lint. - final ExcludedIdentifiersListParameter exclude; - - /// Constructor for [AvoidUnusedParameters] model - AvoidUnusedParameters({ - required this.exclude, - }); - - /// Method for creating from json data - factory AvoidUnusedParameters.fromJson(Map json) { - return AvoidUnusedParameters( - exclude: ExcludedIdentifiersListParameter.defaultFromJson(json), - ); - } -} diff --git a/lib/src/lints/avoid_unused_parameters/models/avoid_unused_parameters_parameters.dart b/lib/src/lints/avoid_unused_parameters/models/avoid_unused_parameters_parameters.dart new file mode 100644 index 00000000..a6a20cbd --- /dev/null +++ b/lib/src/lints/avoid_unused_parameters/models/avoid_unused_parameters_parameters.dart @@ -0,0 +1,27 @@ +import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart'; + +/// A data model class that represents the `avoid_unused_parameters` input +/// parameters. +class AvoidUnusedParametersParameters { + /// A list of methods that should be excluded from the lint. + final ExcludedIdentifiersListParameter exclude; + + /// Constructor for [AvoidUnusedParametersParameters] model. + AvoidUnusedParametersParameters({ + required this.exclude, + }); + + /// Empty [AvoidUnusedParametersParameters] model, excludes nothing. + factory AvoidUnusedParametersParameters.empty() { + return AvoidUnusedParametersParameters( + exclude: ExcludedIdentifiersListParameter(exclude: []), + ); + } + + /// Method for creating from json data. + factory AvoidUnusedParametersParameters.fromJson(Map json) { + return AvoidUnusedParametersParameters( + exclude: ExcludedIdentifiersListParameter.defaultFromJson(json), + ); + } +} diff --git a/lib/src/lints/avoid_unused_parameters/visitors/avoid_unused_parameters_visitor.dart b/lib/src/lints/avoid_unused_parameters/visitors/avoid_unused_parameters_visitor.dart index a3bc2f6e..e10fb310 100644 --- a/lib/src/lints/avoid_unused_parameters/visitors/avoid_unused_parameters_visitor.dart +++ b/lib/src/lints/avoid_unused_parameters/visitors/avoid_unused_parameters_visitor.dart @@ -25,16 +25,18 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:collection/collection.dart'; +import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart'; +import 'package:solid_lints/src/lints/avoid_unused_parameters/models/avoid_unused_parameters_parameters.dart'; import 'package:solid_lints/src/utils/node_utils.dart'; import 'package:solid_lints/src/utils/parameter_utils.dart'; -/// AST Visitor which finds all is expressions and checks if they are -/// unrelated (result always false) +/// A visitor that reports unused formal parameters. class AvoidUnusedParametersVisitor extends RecursiveAstVisitor { - final _unusedParameters = []; + final AvoidUnusedParametersRule _rule; + final AvoidUnusedParametersParameters _parameters; - /// List of unused parameters - Iterable get unusedParameters => _unusedParameters; + /// Creates a new instance of [AvoidUnusedParametersVisitor]. + AvoidUnusedParametersVisitor(this._rule, this._parameters); @override void visitConstructorDeclaration(ConstructorDeclaration node) { @@ -48,15 +50,18 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor { parameters.parameters.isEmpty) { return; } + + if (_isExcluded(node)) return; + final unused = _getUnusedParameters( node.body, parameters.parameters, initializers: node.initializers, ).whereNot(nameConsistsOfUnderscoresOnly); - _unusedParameters.addAll( - unused, - ); + for (final parameter in unused) { + _rule.reportAtNode(parameter); + } } @override @@ -73,34 +78,41 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor { return; } - final isTearOff = _usedAsTearOff(node); + if (_isExcluded(node) || isOverride(node.metadata) || _isATearOff(node)) { + return; + } - if (!isOverride(node.metadata) && !isTearOff) { - _unusedParameters.addAll( - _filterOutUnderscoresAndNamed( - node.body, - parameters.parameters, - ), - ); + for (final parameter in _filterOutUnderscoresAndNamed( + node.body, + parameters.parameters, + )) { + _rule.reportAtNode(parameter); } } @override void visitFunctionExpression(FunctionExpression node) { super.visitFunctionExpression(node); + + final declaration = + node.thisOrAncestorOfType() ?? + node.thisOrAncestorOfType() ?? + node.thisOrAncestorOfType(); + if (declaration != null && _isExcluded(declaration)) return; + final params = node.parameters; - if (params == null) { - return; - } + if (params == null) return; - _unusedParameters.addAll( - _filterOutUnderscoresAndNamed( - node.body, - params.parameters, - ), - ); + for (final parameter in _filterOutUnderscoresAndNamed( + node.body, + params.parameters, + )) { + _rule.reportAtNode(parameter); + } } + bool _isExcluded(Declaration node) => _parameters.exclude.shouldIgnore(node); + Iterable _filterOutUnderscoresAndNamed( AstNode body, Iterable parameters, @@ -109,9 +121,10 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor { body, parameters, ); - return unused.whereNot(nameConsistsOfUnderscoresOnly).where( - (param) => !param.isNamed, - ); + + return unused + .whereNot(nameConsistsOfUnderscoresOnly) + .where((param) => !param.isNamed); } Set _getUnusedParameters( @@ -145,7 +158,6 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor { isFieldFormalParameter = parameter.toSource().contains('this.'); isSuperFormalParameter = parameter.toSource().contains('super.'); } - // if (name != null && !isPresentInAll && @@ -158,7 +170,7 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor { return result; } - bool _usedAsTearOff(MethodDeclaration node) { + bool _isATearOff(MethodDeclaration node) { final name = node.name.lexeme; if (!Identifier.isPrivateName(name)) { return false; diff --git a/lib/src/utils/parameter_utils.dart b/lib/src/utils/parameter_utils.dart index 6e914ec6..a962673c 100644 --- a/lib/src/utils/parameter_utils.dart +++ b/lib/src/utils/parameter_utils.dart @@ -1,6 +1,5 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/error/error.dart' as error; -import 'package:custom_lint_builder/custom_lint_builder.dart'; /// Checks if parameter name consists only of underscores bool nameConsistsOfUnderscoresOnly(FormalParameter parameter) { @@ -21,24 +20,3 @@ error.DiagnosticSeverity? decodeErrorSeverity(String? severity) { _ => null, }; } - -/// Extension to create a copy of [LintCode] -extension LintCodeCopyWith on LintCode { - /// Returns a copy of [LintCode] with specified fields replaced - LintCode copyWith({ - String? name, - String? problemMessage, - String? correctionMessage, - String? uniqueName, - String? url, - error.DiagnosticSeverity? errorSeverity, - }) => - LintCode( - name: name ?? this.name, - problemMessage: problemMessage ?? this.problemMessage, - correctionMessage: correctionMessage ?? this.correctionMessage, - uniqueName: uniqueName ?? this.uniqueName, - url: url ?? this.url, - errorSeverity: errorSeverity ?? this.errorSeverity, - ); -} diff --git a/lint_test/avoid_unused_parameters_test.dart b/lint_test/avoid_unused_parameters_test.dart deleted file mode 100644 index 075269a6..00000000 --- a/lint_test/avoid_unused_parameters_test.dart +++ /dev/null @@ -1,243 +0,0 @@ -// ignore_for_file: prefer_const_declarations, prefer_match_file_name, avoid_global_state, named_parameters_ordering -// ignore_for_file: unnecessary_nullable_for_final_variable_declarations -// ignore_for_file: unused_local_variable -// ignore_for_file: unused_element -// ignore_for_file: newline_before_return -// ignore_for_file: no_empty_block -// ignore_for_file: member_ordering - -import 'package:flutter/material.dart'; - -/// Check the `avoid_unused_parameters` rule -/// -void testNamed() { - SomeAnotherClass( - func: ({required text}) {}, - ); -} - -typedef MaxFun = int Function(int a, int b); - -typedef Named = String Function({required String text}); - -typedef ReqNamed = void Function({required String text}); - -// expect_lint: avoid_unused_parameters -final MaxFun bad = (int a, int b) => 1; - -final MaxFun good = (int a, _) => a; - -final MaxFun ok = (int _, int __) => 1; - -final MaxFun goodMax = (int a, int b) { - return a + b; -}; - -final Named _named = ({required text}) { - return ''; -}; - -void ch({String text = ''}) {} - -final Named _named2 = ({required text}) { - return text; -}; - -final Named _named3 = ({required text}) => ''; - -final Named _named4 = ({required text}) => text; - -// expect_lint: avoid_unused_parameters -final optional = (int a, [int b = 0]) { - return a; -}; - -// expect_lint: avoid_unused_parameters -final named = (int a, {required int b, int c = 0}) { - return c; -}; - -// good -var k = (String g) { - return g; -}; - -// expect_lint: avoid_unused_parameters -var c = (String g) { - return '0'; -}; - -// expect_lint: avoid_unused_parameters -final MaxFun tetsFun = (int a, int b) { - return 4; -}; - -// expect_lint: avoid_unused_parameters -void fun(String s) { - return; -} - -// expect_lint: avoid_unused_parameters -void fun2(String s) { - return; -} - -class TestClass { - // expect_lint: avoid_unused_parameters - static void staticMethod(int a) {} - - // expect_lint: avoid_unused_parameters - void method(String s) { - return; - } - - void methodWithUnderscores(int _) {} -} - -class TestClass2 { - void method(String _) { - return; - } -} - -class SomeOtherClass { - // expect_lint: avoid_unused_parameters - final MaxFun maxFunLint = (int a, int b) => 1; - - // Good - final MaxFun good = (int a, int b) { - return a * b; - }; - - // expect_lint: avoid_unused_parameters - void method(String s) { - return; - } -} - -class SomeAnotherClass extends SomeOtherClass { - final ReqNamed func; - - SomeAnotherClass({ - required this.func, - }); - - @override - void method(String s) {} -} - -void someOtherFunction(String s) { - print(s); - return; -} - -class SomeOtherAnotherClass { - void method(String s) { - print(s); - return; - } - - // expect_lint: avoid_unused_parameters - void anonymousCallback(Function(int a) cb) {} -} - -// expect_lint: avoid_unused_parameters -void closure(int a) { - void internal(int a) { - print(a); - } -} - -// expect_lint: avoid_unused_parameters -final MaxFun maxFunInstance = (int a, int b) => 1; - -final MaxFun m = (_, __) => 1; - -class Foo { - final int a; - final int? b; - - Foo._(this.a, this.b); - - Foo.name(this.a, this.b); - - Foo.coolName({required this.a, required this.b}); - - // expect_lint: avoid_unused_parameters - Foo.another({required int c}) - : a = 1, - b = 0; - - // expect_lint: avoid_unused_parameters - factory Foo.aOnly(int a) { - return Foo._(1, null); - } -} - -class Bar extends Foo { - Bar.name(super.a, super.b) : super.name(); -} - -class TestWidget extends StatelessWidget { - const TestWidget({ - super.key, - // expect_lint: avoid_unused_parameters - int a = 1, - // expect_lint: avoid_unused_parameters - String k = '', - }); - - // expect_lint: avoid_unused_parameters - factory TestWidget.a([int b = 0]) { - return TestWidget(k: ''); - } - - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} - -class UsingConstructorParameterInInitializer { - final int _value; - - UsingConstructorParameterInInitializer(int value) : _value = value; - - void printValue() { - print(_value); - } -} - -// no lint -void excludeMethod(String s) { - return; -} - -// no lint -void simpleMethodName(String s) { - return; -} - -class Exclude { - // no lint - void excludeMethod(String s) { - return; - } - -// expect_lint: avoid_unused_parameters - void excludeMethod2(String s) { - return; - } -} - -class SimpleClassName { - // no lint - void simpleMethodName(String s) { - return; - } - -// expect_lint: avoid_unused_parameters - void simpleMethodName2(String s) { - return; - } -} diff --git a/test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart b/test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart new file mode 100644 index 00000000..440ee2e4 --- /dev/null +++ b/test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart @@ -0,0 +1,384 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../utils/fake_analysis_options_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidUnusedParametersRuleTest); + }); +} + +@reflectiveTest +class AvoidUnusedParametersRuleTest extends AnalysisRuleTest { + static const _importFlutterMaterial = + "import 'package:flutter/material.dart';"; + + static const _mockFlutterMaterialContent = ''' +abstract class Widget { + final Object? key; + + const Widget({this.key}); +} + +class StatelessWidget implements Widget { + const StatelessWidget({super.key}); + + Widget build(BuildContext context); +} + +abstract interface class BuildContext {} + +class Placeholder extends StatelessWidget { + const Placeholder({super.key}); + + @override + Widget build(BuildContext context) => throw 'unimplemented'; +} +'''; + + @override + void setUp() { + final FakeAnalysisOptionsLoader fakeAnalysisOptionsLoader = + FakeAnalysisOptionsLoader( + ruleOptions: { + 'exclude': [ + {'class_name': 'Exclude', 'method_name': 'excludeMethod'}, + {'method_name': 'excludeMethod'}, + 'simpleMethodName', + 'SimpleClassName', + 'exclude', + ], + }, + ); + + rule = AvoidUnusedParametersRule( + analysisOptionsLoader: fakeAnalysisOptionsLoader, + ); + newPackage('flutter') + ..addFile('lib/material.dart', _mockFlutterMaterialContent); + super.setUp(); + } + + Future test_reports_on_unused_function_expression_parameters() async { + await assertDiagnostics( + r''' +typedef MaxFun = int Function(int a, int b); + +final MaxFun bad = (int a, int b) => 1; + +final MaxFun tetsFun = (int a, int b) { + return 4; +}; + +var c = (String g) { + return '0'; +}; + +final MaxFun maxFunInstance = (int a, int b) => 1; +''', + [ + lint(66, 5), + lint(73, 5), + lint(111, 5), + lint(118, 5), + lint(152, 8), + lint(213, 5), + lint(220, 5), + ], + ); + } + + Future test_reports_on_unused_optional_and_named_parameters() async { + await assertDiagnostics( + r''' +final optional = (int a, [int b = 0]) { + return a; +}; + +final named = (int a, {required int b, int c = 0}) { + return c; +}; +''', + [lint(26, 9), lint(71, 5)], + ); + } + + Future test_reports_on_unused_top_level_functions() async { + await assertDiagnostics( + r''' +void fun(String s) { + return; +} + +void fun2(String s) { + return; +} + +void closure(int a) { + void internal(int a) { + print(a); + } +} +''', + [lint(9, 8), lint(44, 8), lint(82, 5)], + ); + } + + Future test_reports_on_unused_parameters_in_methods() async { + await assertDiagnostics( + r''' +class TestClass { + static void staticMethod(int a) {} + + void method(String s) { + return; + } +} + +class SomeOtherClass { + final MaxFun maxFunLint = (int a, int b) => 1; + + // Good + final MaxFun good = (int a, int b) { + return a * b; + }; + + void method(String s) { + return; + } +} + +typedef MaxFun = int Function(int a, int b); + +class SomeOtherAnotherClass { + void method(String s) { + print(s); + return; + } + + void anonymousCallback(Function(int a) cb) {} +} +''', + [ + lint(45, 5), + lint(70, 8), + lint(153, 5), + lint(160, 5), + lint(261, 8), + lint(450, 18), + ], + ); + } + + Future test_reports_on_unused_parameters_in_constructors() async { + await assertDiagnostics( + r''' +class Foo { + final int a; + final int? b; + + Foo.another({required int c}) + : a = 1, + b = 0; + + factory Foo.aOnly(int a) { + return Foo._(1, null); + } + + Foo._(this.a, this.b); +} +''', + [lint(59, 14), lint(127, 5)], + ); + } + + Future test_reports_on_unused_widget_constructor_parameters() async { + await assertDiagnostics( + ''' +$_importFlutterMaterial + +class TestWidget extends StatelessWidget { + const TestWidget({ + super.key, + int a = 1, + String k = '', + }); + + factory TestWidget.a([int b = 0]) { + return const TestWidget(); + } + + @override + Widget build(BuildContext context) { + return const Placeholder(); + } +} +''', + [lint(124, 9), lint(139, 13), lint(185, 9)], + ); + } + + Future test_does_not_report_on_used_parameters() async { + await assertNoDiagnostics(r''' +typedef MaxFun = int Function(int a, int b); + +final MaxFun good = (int a, int b) => a + b; + +final MaxFun goodMax = (int a, int b) { + return a + b; +}; + +String Function(String g) k = (String g) { + return g; +}; + +void someOtherFunction(String s) { + print(s); +} +'''); + } + + Future test_does_not_report_on_underscore_parameters() async { + await assertNoDiagnostics(r''' +typedef MaxFun = int Function(int a, int b); + +final MaxFun good = (int a, _) => a; + +final MaxFun ok = (int _, int __) => 1; + +final MaxFun m = (_, __) => 1; + +class TestClass { + void methodWithUnderscores(int _) {} +} + +class TestClass2 { + void method(String _) { + return; + } +} +'''); + } + + Future test_does_not_report_on_named_parameters() async { + await assertNoDiagnostics(r''' +typedef Named = String Function({required String text}); + +final Named _named = ({required text}) { + return ''; +}; + +void ch({String text = ''}) {} + +final Named _named2 = ({required text}) { + return text; +}; + +final Named _named3 = ({required text}) => ''; + +final Named _named4 = ({required text}) => text; + +void testNamed() { + SomeAnotherClass( + func: ({required text}) {}, + ); +} + +typedef ReqNamed = void Function({required String text}); + +class SomeAnotherClass { + final ReqNamed func; + + SomeAnotherClass({ + required this.func, + }); +} +'''); + } + + Future + test_does_not_report_on_override_and_field_formal_parameters() async { + await assertNoDiagnostics(r''' +class Foo { + final int a; + final int? b; + + Foo.name(this.a, this.b); + + Foo.coolName({required this.a, required this.b}); + + Foo._(this.a, this.b); +} + +class Bar extends Foo { + Bar.name(super.a, super.b) : super.name(); +} + +class SomeAnotherClass extends SomeOtherClass { + @override + void method(String s) {} +} + +class SomeOtherClass { + void method(String s) { + print(s); + } +} + +class UsingConstructorParameterInInitializer { + final int _value; + + UsingConstructorParameterInInitializer(int value) : _value = value; + + void printValue() { + print(_value); + } +} +'''); + } + + Future test_does_not_report_on_excluded_declarations() async { + await assertNoDiagnostics(r''' +void excludeMethod(String s) { + return; +} + +void simpleMethodName(String s) { + return; +} + +class Exclude { + void excludeMethod(String s) { + return; + } +} + +class SimpleClassName { + void simpleMethodName(String s) { + return; + } +} +'''); + } + + Future test_reports_on_non_matching_excluded_declarations() async { + await assertDiagnostics( + r''' +class Exclude { + void excludeMethod2(String s) { + return; + } +} + +class SimpleClassName { + void simpleMethodName2(String s) { + return; + } +} +''', + [lint(38, 8), lint(118, 8)], + ); + } +} diff --git a/test/utils/fake_analysis_options_loader.dart b/test/utils/fake_analysis_options_loader.dart new file mode 100644 index 00000000..c8683879 --- /dev/null +++ b/test/utils/fake_analysis_options_loader.dart @@ -0,0 +1,15 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; + +class FakeAnalysisOptionsLoader implements AnalysisOptionsLoader { + final Map ruleOptions; + + FakeAnalysisOptionsLoader({required this.ruleOptions}); + + @override + Map? getRuleOptions(RuleContext context, String ruleName) => + ruleOptions; + + @override + void loadRulesOptionsFromContext(RuleContext context) {} +} From a978a208431b5d3d93569d07ea64904b31826a49 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Fri, 19 Jun 2026 17:25:36 +0300 Subject: [PATCH 20/49] remove avoid_unnecessary_type_casts (#289) * refactor: migrate avoid_unnecessary_type_casts * fix: incorrect node * docs: improve code documentation * remove avoid_unnecessary_type_casts use dart linter's `unnecessary_cast` instead * docs: update changelog * replace avoid_unnecessary_type_casts rule with dart analyzer's unnecessary_cast docs: update changelog * remove no deleted rule * remove references to unnecessary_type_casts --- CHANGELOG.md | 5 ++ lib/analysis_options.yaml | 18 +++--- lib/main.dart | 1 + .../avoid_unnecessary_type_casts_rule.dart | 51 ----------------- .../avoid_unnecessary_type_casts_fix.dart | 39 ------------- .../avoid_unnecessary_type_casts_visitor.dart | 56 ------------------- lib/src/utils/typecast_utils.dart | 8 +-- lint_test/analysis_options.yaml | 1 - .../avoid_unnecessary_type_casts_test.dart | 35 ------------ 9 files changed, 19 insertions(+), 195 deletions(-) delete mode 100644 lib/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart delete mode 100644 lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart delete mode 100644 lib/src/lints/avoid_unnecessary_type_casts/visitors/avoid_unnecessary_type_casts_visitor.dart delete mode 100644 lint_test/avoid_unnecessary_type_casts_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b6c4c5ae..20743373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.0.0 + +- feat!: migrate to analyzer_server_plugin +- refactor: replace `avoid_unnecessary_type_casts` rule with dart analyzer's `unnecessary_cast` + ## 0.3.3 - Fix pub.dev analysis issue diff --git a/lib/analysis_options.yaml b/lib/analysis_options.yaml index 59b28dd2..21857abc 100644 --- a/lib/analysis_options.yaml +++ b/lib/analysis_options.yaml @@ -3,21 +3,21 @@ analyzer: - custom_lint exclude: # General generated files - - '**/*.g.dart' - - '**/*.gr.dart' + - "**/*.g.dart" + - "**/*.gr.dart" # Flutter - - 'lib/generated_plugin_registrant.dart' + - "lib/generated_plugin_registrant.dart" # mockito - - '*.mocks.dart' - - '**/*.mocks.dart' + - "*.mocks.dart" + - "**/*.mocks.dart" # freezed - - '**/*.freezed.dart' + - "**/*.freezed.dart" # protobuf - - '**/*.pb.dart' + - "**/*.pb.dart" # test_coverage - test/.test_coverage.dart @@ -33,6 +33,7 @@ analyzer: missing_required_param: error missing_return: error parameter_assignments: error + unnecessary_cast: warning custom_lint: rules: @@ -48,7 +49,6 @@ custom_lint: - avoid_unnecessary_return_variable - avoid_unnecessary_setstate - avoid_unnecessary_type_assertions - - avoid_unnecessary_type_casts - avoid_unrelated_type_assertions - avoid_unused_parameters - avoid_debug_print_in_release @@ -87,7 +87,7 @@ custom_lint: - number_of_parameters: max_parameters: 7 exclude: - - method_name: copyWith + - method_name: copyWith - prefer_conditional_expressions: ignore_nested: true diff --git a/lib/main.dart b/lib/main.dart index 708b9fce..e6473d17 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -35,6 +35,7 @@ class SolidLintsPlugin extends Plugin { final avoidUnnecessaryTypeAssertionsRule = AvoidUnnecessaryTypeAssertionsRule(); final doubleLiteralFormatRule = DoubleLiteralFormatRule(); + final lintRules = [ AvoidFinalWithGetterRule(), AvoidGlobalStateRule(), diff --git a/lib/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart b/lib/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart deleted file mode 100644 index 7db9ff71..00000000 --- a/lib/src/lints/avoid_unnecessary_type_casts/avoid_unnecessary_type_casts_rule.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/diagnostic/diagnostic.dart'; -import 'package:analyzer/error/listener.dart' as error_listener; -import 'package:analyzer/source/source_range.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/lints/avoid_unnecessary_type_casts/visitors/avoid_unnecessary_type_casts_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; - -part 'fixes/avoid_unnecessary_type_casts_fix.dart'; - -/// An `avoid_unnecessary_type_casts` rule which -/// warns about unnecessary usage of `as` operator -class AvoidUnnecessaryTypeCastsRule extends SolidLintRule { - /// This lint rule represents - /// the error whether we use bad formatted double literals. - static const lintName = 'avoid_unnecessary_type_casts'; - - AvoidUnnecessaryTypeCastsRule._(super.config); - - /// Creates a new instance of [AvoidUnnecessaryTypeCastsRule] - /// based on the lint configuration. - factory AvoidUnnecessaryTypeCastsRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (_) => "Avoid unnecessary usage of as operator.", - ); - - return AvoidUnnecessaryTypeCastsRule._(rule); - } - - @override - void run( - CustomLintResolver resolver, - error_listener.DiagnosticReporter reporter, - CustomLintContext context, - ) { - context.registry.addAsExpression((node) { - final visitor = AvoidUnnecessaryTypeCastsVisitor(); - visitor.visitAsExpression(node); - - for (final element in visitor.expressions.entries) { - reporter.atNode(element.key, code); - } - }); - } - - @override - List getFixes() => [_UnnecessaryTypeCastsFix()]; -} diff --git a/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart b/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart deleted file mode 100644 index d03b6fcc..00000000 --- a/lib/src/lints/avoid_unnecessary_type_casts/fixes/avoid_unnecessary_type_casts_fix.dart +++ /dev/null @@ -1,39 +0,0 @@ -part of '../avoid_unnecessary_type_casts_rule.dart'; - -/// A Quick fix for `avoid_unnecessary_type_casts` rule -/// Suggests to remove unnecessary assertions -class _UnnecessaryTypeCastsFix extends DartFix { - @override - void run( - CustomLintResolver resolver, - ChangeReporter reporter, - CustomLintContext context, - Diagnostic analysisError, - List others, - ) { - context.registry.addAsExpression((node) { - if (analysisError.sourceRange.intersects(node.sourceRange)) { - _addDeletion(reporter, 'as', node, node.asOperator.offset); - } - }); - } - - void _addDeletion( - ChangeReporter reporter, - String itemToDelete, - Expression node, - int operatorOffset, - ) { - final targetNameLength = operatorOffset - node.offset; - final removedPartLength = node.length - targetNameLength; - - final changeBuilder = reporter.createChangeBuilder( - message: "Remove unnecessary '$itemToDelete'", - priority: 1, - ); - - changeBuilder.addDartFileEdit((builder) { - builder.addDeletion(SourceRange(operatorOffset, removedPartLength)); - }); - } -} diff --git a/lib/src/lints/avoid_unnecessary_type_casts/visitors/avoid_unnecessary_type_casts_visitor.dart b/lib/src/lints/avoid_unnecessary_type_casts/visitors/avoid_unnecessary_type_casts_visitor.dart deleted file mode 100644 index a7b39e87..00000000 --- a/lib/src/lints/avoid_unnecessary_type_casts/visitors/avoid_unnecessary_type_casts_visitor.dart +++ /dev/null @@ -1,56 +0,0 @@ -// MIT License -// -// Copyright (c) 2020-2021 Dart Code Checker team -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/ast/visitor.dart'; -import 'package:solid_lints/src/utils/typecast_utils.dart'; - -/// AST Visitor which finds all as expressions and checks if they are -/// necessary -class AvoidUnnecessaryTypeCastsVisitor extends RecursiveAstVisitor { - final _expressions = {}; - - /// All as expressions - Map get expressions => _expressions; - - @override - void visitAsExpression(AsExpression node) { - super.visitAsExpression(node); - - final objectType = node.expression.staticType; - final castedType = node.type.type; - - if (objectType == null || castedType == null) { - return; - } - - final typeCast = TypeCast( - source: objectType, - target: castedType, - ); - - if (typeCast.isUnnecessaryTypeCheck) { - _expressions[node] = 'as'; - } - } -} diff --git a/lib/src/utils/typecast_utils.dart b/lib/src/utils/typecast_utils.dart index cb0bfb12..e90fe2e6 100644 --- a/lib/src/utils/typecast_utils.dart +++ b/lib/src/utils/typecast_utils.dart @@ -6,7 +6,6 @@ import 'package:collection/collection.dart'; // TODO: consider to move here common functions for rules // avoid_unnecessary_type_assertions -// avoid_unnecessary_type_casts (after implementation) // avoid_unrelated_type_assertions (after implementation) /// A class for representing arguments for types checking methods @@ -66,9 +65,10 @@ class TypeCast { return false; } - if (this case TypeCast(source: final objectType, target: final castedType) - when objectType is ParameterizedType && - castedType is ParameterizedType) { + if (this case TypeCast( + source: final objectType, + target: final castedType, + ) when objectType is ParameterizedType && castedType is ParameterizedType) { if (objectType.typeArguments.length != castedType.typeArguments.length) { return false; } diff --git a/lint_test/analysis_options.yaml b/lint_test/analysis_options.yaml index 286e774d..788633dc 100644 --- a/lint_test/analysis_options.yaml +++ b/lint_test/analysis_options.yaml @@ -30,7 +30,6 @@ custom_lint: - avoid_unnecessary_setstate - double_literal_format - avoid_unnecessary_type_assertions - - avoid_unnecessary_type_casts - avoid_unrelated_type_assertions - avoid_unused_parameters: exclude: diff --git a/lint_test/avoid_unnecessary_type_casts_test.dart b/lint_test/avoid_unnecessary_type_casts_test.dart deleted file mode 100644 index dffa6903..00000000 --- a/lint_test/avoid_unnecessary_type_casts_test.dart +++ /dev/null @@ -1,35 +0,0 @@ -// ignore_for_file: prefer_const_declarations -// ignore_for_file: unnecessary_nullable_for_final_variable_declarations -// ignore_for_file: unnecessary_cast -// ignore_for_file: unused_local_variable - -/// Check the `avoid_unnecessary_type_casts` rule - -void fun() { - final testList = [1.0, 2.0, 3.0]; - - // to check quick-fix => testList - // expect_lint: avoid_unnecessary_type_casts - final result = testList as List; - - final double? nullableD = 2.0; - // casting `Type? is Type` is allowed - final castedD = nullableD as double; - - final testMap = {'A': 'B'}; - - // expect_lint: avoid_unnecessary_type_casts - final castedMapValue = testMap['A'] as String?; - - // casting `Type? is Type` is allowed - final castedNotNullMapValue = testMap['A'] as String; - - final testString = 'String'; - // expect_lint: avoid_unnecessary_type_casts - _testFun(testString as String); -} - -void _testFun(String a) { - // expect_lint: avoid_unnecessary_type_casts - final result = (a as String).length; -} From 87365219bdf8ce2d6de462d402cfffca78de20ae Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Mon, 22 Jun 2026 17:12:29 +0300 Subject: [PATCH 21/49] feat: add use_nearest_context rule (#293) * feat: add use_nearest_context rule (#190) * fix: separate rename/replace quick fix applicability for use_nearest_context * refactor: simplify use_nearest_context test files * refactor: migrate use_nearest_context tests to AutoTestLintOffsets * refactor: simplify logic in use_nearest_context_visitor * refactor: move AST identifier utilities to node_utils and expose as SimpleIdentifier extension --------- Co-authored-by: Illia Aihistov --- CHANGELOG.md | 1 + lib/main.dart | 15 + .../rename_nearest_context_parameter_fix.dart | 58 +++ ...ce_with_nearest_context_parameter_fix.dart | 68 ++++ .../use_nearest_context_rule.dart | 82 +++++ .../utils/use_nearest_context_utils.dart | 23 ++ .../visitors/use_nearest_context_visitor.dart | 37 ++ lib/src/utils/node_utils.dart | 39 ++ lib/src/utils/types_utils.dart | 7 +- pubspec.yaml | 2 +- .../use_nearest_context_rule_test.dart | 332 ++++++++++++++++++ 11 files changed, 661 insertions(+), 3 deletions(-) create mode 100644 lib/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart create mode 100644 lib/src/lints/use_nearest_context/fixes/replace_with_nearest_context_parameter_fix.dart create mode 100644 lib/src/lints/use_nearest_context/use_nearest_context_rule.dart create mode 100644 lib/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart create mode 100644 lib/src/lints/use_nearest_context/visitors/use_nearest_context_visitor.dart create mode 100644 test/src/lints/use_nearest_context/use_nearest_context_rule_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 20743373..6c8c46d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - feat!: migrate to analyzer_server_plugin - refactor: replace `avoid_unnecessary_type_casts` rule with dart analyzer's `unnecessary_cast` +- Added `use_nearest_context` rule. ## 0.3.3 diff --git a/lib/main.dart b/lib/main.dart index e6473d17..54efe979 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,6 +13,9 @@ import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_param import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; +import 'package:solid_lints/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart'; +import 'package:solid_lints/src/lints/use_nearest_context/fixes/replace_with_nearest_context_parameter_fix.dart'; +import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart'; /// The entry point for the Solid Lints analyser server plugin. /// @@ -50,6 +53,7 @@ class SolidLintsPlugin extends Plugin { AvoidUnusedParametersRule( analysisOptionsLoader: analysisLoader, ), + UseNearestContextRule(), ]; for (final lintRule in lintRules) { @@ -64,9 +68,20 @@ class SolidLintsPlugin extends Plugin { AvoidFinalWithGetterRule.code, AvoidFinalWithGetterFix.new, ); + registry.registerFixForRule( avoidUnnecessaryTypeAssertionsRule.diagnosticCode, AvoidUnnecessaryTypeAssertionsFix.new, ); + + registry.registerFixForRule( + UseNearestContextRule.code, + RenameNearestContextParameterFix.new, + ); + + registry.registerFixForRule( + UseNearestContextRule.code, + ReplaceWithNearestContextParameterFix.new, + ); } } diff --git a/lib/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart b/lib/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart new file mode 100644 index 00000000..8f10e84b --- /dev/null +++ b/lib/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart @@ -0,0 +1,58 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; +import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart'; +import 'package:solid_lints/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart'; + +/// A Quick fix for [UseNearestContextRule] rule +/// Suggests to rename the nearest BuildContext parameter +/// to the one that is being used. +class RenameNearestContextParameterFix extends ResolvedCorrectionProducer { + static const _renameParameterKind = FixKind( + 'solid_lints.fix.${UseNearestContextRule.lintName}.rename_parameter', + DartFixKindPriority.standard, + "Rename nearest BuildContext parameter", + ); + + /// Creates a new instance of [RenameNearestContextParameterFix]. + RenameNearestContextParameterFix({required super.context}); + + @override + FixKind get fixKind => _renameParameterKind; + + @override + CorrectionApplicability get applicability => + CorrectionApplicability.automatically; + + @override + Future compute(ChangeBuilder builder) async { + final identifierNode = node; + if (identifierNode is! SimpleIdentifier) return; + + // Do not offer renaming the parameter if this is an access on `this` + // or `super`. + final parent = identifierNode.parent; + if (parent is PropertyAccess) { + var target = parent.target; + while (target is ParenthesizedExpression) { + target = target.expression; + } + if (target is ThisExpression || target is SuperExpression) return; + } + + final closestBuildContext = findClosestBuildContext(identifierNode); + if (closestBuildContext == null) return; + + final parameterName = closestBuildContext.name; + if (parameterName == null) return; + + await builder.addDartFileEdit(file, (builder) { + builder.addSimpleReplacement( + parameterName.sourceRange, + identifierNode.name, + ); + }); + } +} diff --git a/lib/src/lints/use_nearest_context/fixes/replace_with_nearest_context_parameter_fix.dart b/lib/src/lints/use_nearest_context/fixes/replace_with_nearest_context_parameter_fix.dart new file mode 100644 index 00000000..8baaa491 --- /dev/null +++ b/lib/src/lints/use_nearest_context/fixes/replace_with_nearest_context_parameter_fix.dart @@ -0,0 +1,68 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; +import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart'; +import 'package:solid_lints/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart'; + +/// A Quick fix for [UseNearestContextRule] rule +/// Suggests to replace the outer BuildContext expression +/// with the nearest available parameter. +class ReplaceWithNearestContextParameterFix extends ResolvedCorrectionProducer { + static const _replaceExpressionKind = FixKind( + 'solid_lints.fix.${UseNearestContextRule.lintName}.replace_expression', + DartFixKindPriority.standard, + "Replace with nearest BuildContext parameter", + ); + + /// Creates a new instance of [ReplaceWithNearestContextParameterFix]. + ReplaceWithNearestContextParameterFix({required super.context}); + + @override + FixKind get fixKind => _replaceExpressionKind; + + @override + CorrectionApplicability get applicability => + CorrectionApplicability.automatically; + + @override + Future compute(ChangeBuilder builder) async { + final errorNode = node; + + final closestBuildContext = findClosestBuildContext(errorNode); + if (closestBuildContext == null) return; + + final parameterName = closestBuildContext.name?.lexeme; + if (parameterName == null) return; + + if (errorNode is ThisExpression) { + await builder.addDartFileEdit(file, (builder) { + builder.addSimpleReplacement( + errorNode.sourceRange, + parameterName, + ); + }); + return; + } + + if (errorNode is SimpleIdentifier) { + final parent = errorNode.parent; + if (parent is PropertyAccess) { + var target = parent.target; + while (target is ParenthesizedExpression) { + target = target.expression; + } + if (target is ThisExpression || target is SuperExpression) { + await builder.addDartFileEdit(file, (builder) { + builder.addSimpleReplacement( + parent.sourceRange, + parameterName, + ); + }); + return; + } + } + } + } +} diff --git a/lib/src/lints/use_nearest_context/use_nearest_context_rule.dart b/lib/src/lints/use_nearest_context/use_nearest_context_rule.dart new file mode 100644 index 00000000..56af784a --- /dev/null +++ b/lib/src/lints/use_nearest_context/use_nearest_context_rule.dart @@ -0,0 +1,82 @@ +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/use_nearest_context/visitors/use_nearest_context_visitor.dart'; + +/// A rule which checks that we use BuildContext from the nearest available +/// scope. +/// +/// ### Example: +/// #### BAD: +/// ```dart +/// class SomeWidget extends StatefulWidget { +/// ... +/// } +/// +/// class _SomeWidgetState extends State { +/// ... +/// void _showDialog() { +/// showModalBottomSheet( +/// context: context, +/// builder: (BuildContext _) { +/// final someProvider = context.watch(); // LINT, BuildContext is used not from the nearest available scope +/// +/// return const SizedBox.shrink(); +/// }, +/// ); +/// } +/// } +/// ``` +/// #### GOOD: +/// ```dart +/// class SomeWidget extends StatefulWidget { +/// ... +/// } +/// +/// class _SomeWidgetState extends State { +/// ... +/// void _showDialog() { +/// showModalBottomSheet( +/// context: context, +/// builder: (BuildContext context) +/// final someProvider = context.watch(); // OK +/// +/// return const SizedBox.shrink(); +/// }, +/// ); +/// } +/// } +/// ``` +/// +class UseNearestContextRule extends AnalysisRule { + /// This lint rule represents the error if BuildContext is used not from the + /// nearest available scope + static const lintName = 'use_nearest_context'; + + /// The code to report for a violation + static const LintCode code = LintCode( + lintName, + 'Use the nearest BuildContext parameter instead of the outer one.', + ); + + /// Creates a new instance of [UseNearestContextRule]. + UseNearestContextRule() + : super( + name: lintName, + description: code.problemMessage, + ); + + @override + LintCode get diagnosticCode => code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + final visitor = UseNearestContextVisitor(this); + registry.addSimpleIdentifier(this, visitor); + registry.addThisExpression(this, visitor); + } +} diff --git a/lib/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart b/lib/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart new file mode 100644 index 00000000..8e79d9d8 --- /dev/null +++ b/lib/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart @@ -0,0 +1,23 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; + +/// Finds the closest BuildContext parameter in the AST parent chain of [node]. +SimpleFormalParameter? findClosestBuildContext(AstNode node) { + AstNode? current = node.parent; + + while (current != null) { + if (current is FunctionExpression) { + final functionParams = current.parameters?.parameters ?? []; + for (final param in functionParams) { + final actualParam = + param is DefaultFormalParameter ? param.parameter : param; + if (actualParam is SimpleFormalParameter && + isBuildContext(actualParam.declaredFragment?.element.type)) { + return actualParam; + } + } + } + current = current.parent; + } + return null; +} diff --git a/lib/src/lints/use_nearest_context/visitors/use_nearest_context_visitor.dart b/lib/src/lints/use_nearest_context/visitors/use_nearest_context_visitor.dart new file mode 100644 index 00000000..e5886740 --- /dev/null +++ b/lib/src/lints/use_nearest_context/visitors/use_nearest_context_visitor.dart @@ -0,0 +1,37 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart'; +import 'package:solid_lints/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart'; +import 'package:solid_lints/src/utils/node_utils.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; + +/// A visitor for [UseNearestContextRule]. +class UseNearestContextVisitor extends SimpleAstVisitor { + final UseNearestContextRule _rule; + + /// Creates a new instance of [UseNearestContextVisitor]. + UseNearestContextVisitor(this._rule); + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + if (!isBuildContext(node.staticType)) return; + if (node.isPropertyOfOtherObject) return; + + final closestBuildContext = findClosestBuildContext(node); + if (closestBuildContext == null) return; + if (closestBuildContext.name?.lexeme == node.name) return; + if (node.isDeclaredInSameFunction(as: closestBuildContext)) return; + + _rule.reportAtNode(node); + } + + @override + void visitThisExpression(ThisExpression node) { + if (!isBuildContext(node.staticType)) return; + + final closestBuildContext = findClosestBuildContext(node); + if (closestBuildContext == null) return; + + _rule.reportAtNode(node); + } +} diff --git a/lib/src/utils/node_utils.dart b/lib/src/utils/node_utils.dart index 3cea0c01..2b008879 100644 --- a/lib/src/utils/node_utils.dart +++ b/lib/src/utils/node_utils.dart @@ -1,5 +1,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/element/element.dart'; + /// Check node is override method from its metadata bool isOverride(List metadata) => metadata.any( @@ -22,3 +24,40 @@ String humanReadableNodeType(AstNode? node) { return 'Node'; } + +/// Extension on [SimpleIdentifier] to provide scope and property utility +/// checks. +extension SimpleIdentifierExtension on SimpleIdentifier { + /// Returns `true` if this identifier is a property accessed on another + /// object (e.g. `state.context`), but not on `this` (e.g. `this.context`). + bool get isPropertyOfOtherObject { + final parent = this.parent; + if (parent is PrefixedIdentifier && this == parent.identifier) { + return true; + } + if (parent is PropertyAccess && this == parent.propertyName) { + var target = parent.target; + while (target is ParenthesizedExpression) { + target = target.expression; + } + return target is! ThisExpression && target is! SuperExpression; + } + return false; + } + + /// Returns `true` if this identifier refers to a variable declared inside + /// the body of the function that owns [as] (i.e. a local variable in the + /// same scope). + bool isDeclaredInSameFunction({required SimpleFormalParameter as}) { + final element = this.element; + if (element is! LocalVariableElement) return false; + + final nearestFunction = as.parent?.parent; + if (nearestFunction is! FunctionExpression) return false; + + final body = nearestFunction.body; + final declOffset = element.firstFragment.nameOffset; + if (declOffset == null) return false; + return declOffset >= body.offset && declOffset < body.end; + } +} diff --git a/lib/src/utils/types_utils.dart b/lib/src/utils/types_utils.dart index bdea71e5..64558270 100644 --- a/lib/src/utils/types_utils.dart +++ b/lib/src/utils/types_utils.dart @@ -129,8 +129,11 @@ bool isRowWidget(DartType? type) => type?.getDisplayString() == 'Row'; bool isPaddingWidget(DartType? type) => type?.getDisplayString() == 'Padding'; -bool isBuildContext(DartType? type) => - type?.getDisplayString() == 'BuildContext'; +bool isBuildContext(DartType? type) { + if (type == null) return false; + final displayString = type.getDisplayString(); + return displayString == 'BuildContext' || displayString == 'BuildContext?'; +} bool isGameWidget(DartType? type) => type?.getDisplayString() == 'GameWidget'; diff --git a/pubspec.yaml b/pubspec.yaml index b1a226bb..56c55842 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: solid_lints description: Lints for Dart and Flutter based on software industry standards and best practices. -version: 0.3.3 +version: 0.3.4 homepage: https://github.com/solid-software/solid_lints/ documentation: https://solid-software.github.io/solid_lints/docs/intro topics: [lints, linter, lint, analysis, analyzer] diff --git a/test/src/lints/use_nearest_context/use_nearest_context_rule_test.dart b/test/src/lints/use_nearest_context/use_nearest_context_rule_test.dart new file mode 100644 index 00000000..070b71e2 --- /dev/null +++ b/test/src/lints/use_nearest_context/use_nearest_context_rule_test.dart @@ -0,0 +1,332 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseNearestContextRuleTest); + }); +} + +@reflectiveTest +class UseNearestContextRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + @override + void setUp() { + rule = UseNearestContextRule(); + newPackage('flutter')..addFile('lib/material.dart', r''' +class BuildContext { + BuildContext get context => this; + Object? get size => null; +} +class Widget {} +void showModalBottomSheet({required BuildContext context, required Widget Function(BuildContext) builder}) {} +class Builder extends Widget { + final Function builder; + Builder({required this.builder}); +} +abstract class Element implements BuildContext { + State? findAncestorStateOfType(); + @override + BuildContext get context => this; + @override + Object? get size => null; +} +abstract class State { + BuildContext get context; +} +class Future { + static void microtask(void Function() callback) {} +} +'''); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + analysisOptionsContent(rules: [rule.name]), + ); + } + + Future test_reports_on_outer_context_usage() async { + await assertAutoDiagnostics(''' +import 'package:flutter/material.dart'; + +void showDialog(BuildContext context) { + final outerContext = context; + + showModalBottomSheet( + context: context, + builder: (BuildContext _) { + final s = ${expectLint('outerContext')}.size; + return Widget(); + }, + ); +} +'''); + } + + Future test_does_not_report_on_nearest_context_usage() async { + await assertNoDiagnostics(r''' +import 'package:flutter/material.dart'; + +void showDialog(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (BuildContext innerContext) { + final s = innerContext.size; + return Widget(); + }, + ); +} +'''); + } + + Future test_does_not_report_on_property_of_other_object() async { + await assertNoDiagnostics(r''' +import 'package:flutter/material.dart'; + +class FalsePositiveTest { + void build(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (BuildContext innerContext) { + final state = (innerContext as Element).findAncestorStateOfType()!; + final s = state.context.size; + return Widget(); + }, + ); + } +} +'''); + } + + Future test_reports_on_this_context_inside_builder() async { + await assertAutoDiagnostics(''' +import 'package:flutter/material.dart'; + +class FalsePositiveTest { + BuildContext get context => throw ''; + + void build(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (BuildContext innerContext) { + final s = this.${expectLint('context')}.size; + return Widget(); + }, + ); + } +} +'''); + } + + Future + test_does_not_report_on_local_variable_assigned_from_nearest_context() async { + await assertNoDiagnostics(r''' +import 'package:flutter/material.dart'; + +class FalsePositiveTest { + void build(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (BuildContext innerContext) { + final localContext = innerContext; + final s = localContext.size; + return Widget(); + }, + ); + } +} +'''); + } + + Future + test_reports_on_outer_builder_context_usage_in_nested_builder() async { + await assertAutoDiagnostics(''' +import 'package:flutter/material.dart'; + +void showDialog(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (BuildContext outerBuilderCtx) { + return Builder( + builder: (BuildContext innerBuilderCtx) { + final s = ${expectLint('outerBuilderCtx')}.size; + return Widget(); + }, + ); + }, + ); +} +'''); + } + + Future test_reports_on_untyped_builder_parameter_usage() async { + await assertAutoDiagnostics(''' +import 'package:flutter/material.dart'; + +void showDialog(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (_) { + final s = ${expectLint('context')}.size; + return Widget(); + }, + ); +} +'''); + } + + Future test_reports_on_outer_context_usage_in_async_callback() async { + await assertAutoDiagnostics(''' +import 'package:flutter/material.dart'; + +void showDialog(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (BuildContext innerContext) { + Future.microtask(() { + final s = ${expectLint('context')}.size; + }); + return Widget(); + }, + ); +} +'''); + } + + Future + test_does_not_report_on_nearest_context_usage_in_async_callback() async { + await assertNoDiagnostics(r''' +import 'package:flutter/material.dart'; + +void showDialog(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (BuildContext innerContext) { + Future.microtask(() { + final s = innerContext.size; + }); + return Widget(); + }, + ); +} +'''); + } + + Future test_reports_on_constructor_parameter_usage_in_builder() async { + await assertAutoDiagnostics(''' +import 'package:flutter/material.dart'; + +class QuickFixBrokenTest { + QuickFixBrokenTest(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (BuildContext _) { + final s = ${expectLint('context')}.size; + return Widget(); + }, + ); + } +} +'''); + } + + Future test_does_not_report_on_shadowed_parameter_name_usage() async { + await assertNoDiagnostics(r''' +import 'package:flutter/material.dart'; + +void showDialog(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + final s = context.size; + return Widget(); + }, + ); +} +'''); + } + + Future test_reports_on_nullable_outer_context_usage() async { + await assertAutoDiagnostics(''' +import 'package:flutter/material.dart'; + +void showDialog(BuildContext? context) { + showModalBottomSheet( + context: context!, + builder: (BuildContext _) { + final s = ${expectLint('context')}; + return Widget(); + }, + ); +} +'''); + } + + Future test_reports_on_this_context_inside_extension_method() async { + await assertAutoDiagnostics(''' +import 'package:flutter/material.dart'; + +extension BuildContextX on BuildContext { + void someMethod() { + showModalBottomSheet( + context: this, + builder: (BuildContext innerContext) { + final s = ${expectLint('this')}.size; + return Widget(); + }, + ); + } +} +'''); + } + + Future test_does_not_report_on_this_context_outside_builder() async { + await assertNoDiagnostics(r''' +import 'package:flutter/material.dart'; + +extension BuildContextX on BuildContext { + void someMethod() { + final s = this.size; + } +} +'''); + } + + Future test_does_not_report_on_nullable_nearest_context_usage() async { + await assertNoDiagnostics(r''' +import 'package:flutter/material.dart'; + +void showDialog(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (BuildContext? innerContext) { + final s = innerContext?.size; + return Widget(); + }, + ); +} +'''); + } + + Future + test_reports_on_outer_context_usage_with_nullable_nearest_context() async { + await assertAutoDiagnostics(''' +import 'package:flutter/material.dart'; + +void showDialog(BuildContext context) { + showModalBottomSheet( + context: context, + builder: (BuildContext? innerContext) { + final s = ${expectLint('context')}.size; + return Widget(); + }, + ); +} +'''); + } +} From 1940463ebc8d4174ab1eb5832997c4e8e0563fd4 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Tue, 23 Jun 2026 14:53:58 +0300 Subject: [PATCH 22/49] refactor: migrate cyclomatic_complexity (#294) * refactor: migrate cyclomatic_complexity (#250) * test: add coverage for nested closures in cyclomatic complexity rule and remove obsolete issue link * fix: update cyclomatic complexity rule to support constructors and do-statements * refactor: migrate CyclomaticComplexityRuleTest to AutoTestLintOffsets and add additional test cases * feat: include collection elements, logical patterns, and null-aware operators in cyclomatic complexity calculation * feat: include null check and null assert patterns in cyclomatic complexity analysis * refactor: migrate cyclomatic complexity visitor to SimpleAstVisitor and target individual declaration types * chore: refactor test files formatting * test: add cyclomatic complexity test case for nested for-in loops * refactor: use inclusive comparison for cyclomatic complexity threshold --------- Co-authored-by: Illia Aihistov --- lib/analysis_options_test.yaml | 1 - lib/main.dart | 6 + .../cyclomatic_complexity_rule.dart | 79 ++-- .../cyclomatic_complexity_parameters.dart | 7 + .../cyclomatic_complexity_flow_visitor.dart | 155 ++++-- .../cyclomatic_complexity_visitor.dart | 47 ++ lint_test/cyclomatic_complexity_test.dart | 57 --- .../cyclomatic_complexity_rule_test.dart | 442 ++++++++++++++++++ 8 files changed, 667 insertions(+), 127 deletions(-) create mode 100644 lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart delete mode 100644 lint_test/cyclomatic_complexity_test.dart create mode 100644 test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart diff --git a/lib/analysis_options_test.yaml b/lib/analysis_options_test.yaml index 0acf73b8..5db9e6b7 100644 --- a/lib/analysis_options_test.yaml +++ b/lib/analysis_options_test.yaml @@ -12,7 +12,6 @@ custom_lint: # Since we're not using the source-lines-of-code rule, `main()` function in test can # have high cyclomatic complexity. # For rationale against splitting up `main()` in tests, see `source-lines-of-code` comments. -# Also, there is a bug in metric calculation: https://github.com/dart-code-checker/dart-code-metrics/issues/663 - cyclomatic_complexity: false # Late keyword is allowed in tests in order to enable the use of custom mocks and diff --git a/lib/main.dart b/lib/main.dart index 54efe979..44e0b44d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,8 @@ import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_wi import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart'; import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; @@ -53,6 +55,10 @@ class SolidLintsPlugin extends Plugin { AvoidUnusedParametersRule( analysisOptionsLoader: analysisLoader, ), + CyclomaticComplexityRule( + analysisOptionsLoader: analysisLoader, + parametersParser: CyclomaticComplexityParameters.fromJson, + ), UseNearestContextRule(), ]; diff --git a/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart b/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart index 75c36d76..c7024791 100644 --- a/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart +++ b/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart @@ -1,8 +1,8 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; -import 'package:solid_lints/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Limit for the number of linearly independent paths through a program's @@ -17,54 +17,51 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// triggering a warning. /// /// ```yaml -/// custom_lint: -/// rules: -/// - cyclomatic_complexity: -/// max_complexity: 10 +/// plugins: +/// solid_lints: +/// diagnostics: +/// cyclomatic_complexity: +/// max_complexity: 10 /// ``` class CyclomaticComplexityRule extends SolidLintRule { - /// This lint rule represents the error if complexity - /// reaches maximum value. + /// Name of the lint. static const lintName = 'cyclomatic_complexity'; - CyclomaticComplexityRule._(super.rule); + static const _code = LintCode( + lintName, + 'The maximum allowed complexity of a function is {0}. Please decrease it.', + ); - /// Creates a new instance of [CyclomaticComplexityRule] - /// based on the lint configuration. - factory CyclomaticComplexityRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - paramsParser: CyclomaticComplexityParameters.fromJson, - problemMessage: (value) => - 'The maximum allowed complexity of a function is ' - '${value.maxComplexity}. Please decrease it.', - ); + @override + DiagnosticCode get diagnosticCode => _code; - return CyclomaticComplexityRule._(rule); - } + /// Creates a new instance of [CyclomaticComplexityRule]. + CyclomaticComplexityRule({ + required super.analysisOptionsLoader, + required super.parametersParser, + }) : super.withParameters( + name: lintName, + description: + 'Limit for the number of linearly independent paths ' + "through a program's source code.", + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addBlockFunctionBody((node) { - context.registry.addDeclaration((declarationNode) { - final isIgnored = - config.parameters.exclude.shouldIgnore(declarationNode); - if (isIgnored) return; + super.registerNodeProcessors(registry, context); + + final parameters = + getParametersForContext(context) ?? + CyclomaticComplexityParameters.empty(); - final visitor = CyclomaticComplexityFlowVisitor(); - node.visitChildren(visitor); + final visitor = CyclomaticComplexityVisitor(this, parameters); - if (visitor.complexityEntities.length + 1 > - config.parameters.maxComplexity) { - reporter.atNode(node, code); - } - }); - }); + registry.addFunctionDeclaration(this, visitor); + registry.addConstructorDeclaration(this, visitor); + registry.addMethodDeclaration(this, visitor); } } diff --git a/lib/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart b/lib/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart index 93d3b2cd..ff5ddb59 100644 --- a/lib/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart +++ b/lib/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart @@ -17,6 +17,13 @@ class CyclomaticComplexityParameters { required this.exclude, }); + /// Empty [CyclomaticComplexityParameters] model. + factory CyclomaticComplexityParameters.empty() => + CyclomaticComplexityParameters( + maxComplexity: _defaultMaxComplexity, + exclude: ExcludedIdentifiersListParameter(exclude: []), + ); + /// Method for creating from json data factory CyclomaticComplexityParameters.fromJson(Map json) => CyclomaticComplexityParameters( diff --git a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart index c101ced4..20114def 100644 --- a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart +++ b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart @@ -31,14 +31,6 @@ import 'package:analyzer/dart/ast/visitor.dart'; /// The AST visitor that will collect cyclomatic complexity of visit nodes in an /// AST structure. class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { - static const _complexityTokenTypes = [ - TokenType.AMPERSAND_AMPERSAND, - TokenType.BAR_BAR, - TokenType.QUESTION_PERIOD, - TokenType.QUESTION_QUESTION, - TokenType.QUESTION_QUESTION_EQ, - ]; - final _complexityEntities = {}; /// Returns an array of entities that increase cyclomatic complexity. @@ -52,13 +44,46 @@ class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { } @override - void visitBlockFunctionBody(BlockFunctionBody node) { - _visitBlock( - node.block.leftBracket.next, - node.block.rightBracket, - ); + void visitBinaryExpression(BinaryExpression node) { + final type = node.operator.type; + if (type == TokenType.AMPERSAND_AMPERSAND || + type == TokenType.BAR_BAR || + type == TokenType.QUESTION_QUESTION) { + _increaseComplexity(node); + } + super.visitBinaryExpression(node); + } + + @override + void visitAssignmentExpression(AssignmentExpression node) { + if (node.operator.type == TokenType.QUESTION_QUESTION_EQ) { + _increaseComplexity(node); + } + super.visitAssignmentExpression(node); + } + + @override + void visitPropertyAccess(PropertyAccess node) { + if (node.operator.type == TokenType.QUESTION_PERIOD) { + _increaseComplexity(node); + } + super.visitPropertyAccess(node); + } - super.visitBlockFunctionBody(node); + @override + void visitMethodInvocation(MethodInvocation node) { + if (node.operator?.type == TokenType.QUESTION_PERIOD) { + _increaseComplexity(node); + } + super.visitMethodInvocation(node); + } + + @override + void visitIndexExpression(IndexExpression node) { + if (node.question != null) { + _increaseComplexity(node); + } + super.visitIndexExpression(node); } @override @@ -76,13 +101,10 @@ class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { } @override - void visitExpressionFunctionBody(ExpressionFunctionBody node) { - _visitBlock( - node.expression.beginToken.previous, - node.expression.endToken.next, - ); + void visitDoStatement(DoStatement node) { + _increaseComplexity(node); - super.visitExpressionFunctionBody(node); + super.visitDoStatement(node); } @override @@ -113,6 +135,79 @@ class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { super.visitSwitchDefault(node); } + @override + void visitSwitchExpressionCase(SwitchExpressionCase node) { + _increaseComplexity(node); + + super.visitSwitchExpressionCase(node); + } + + @override + void visitSwitchPatternCase(SwitchPatternCase node) { + _increaseComplexity(node); + + super.visitSwitchPatternCase(node); + } + + @override + void visitWhenClause(WhenClause node) { + _increaseComplexity(node); + + super.visitWhenClause(node); + } + + @override + void visitIfElement(IfElement node) { + _increaseComplexity(node); + super.visitIfElement(node); + } + + @override + void visitForElement(ForElement node) { + _increaseComplexity(node); + super.visitForElement(node); + } + + @override + void visitLogicalAndPattern(LogicalAndPattern node) { + _increaseComplexity(node); + super.visitLogicalAndPattern(node); + } + + @override + void visitLogicalOrPattern(LogicalOrPattern node) { + _increaseComplexity(node); + super.visitLogicalOrPattern(node); + } + + @override + void visitNullCheckPattern(NullCheckPattern node) { + _increaseComplexity(node); + super.visitNullCheckPattern(node); + } + + @override + void visitNullAssertPattern(NullAssertPattern node) { + _increaseComplexity(node); + super.visitNullAssertPattern(node); + } + + @override + void visitCascadeExpression(CascadeExpression node) { + if (node.isNullAware) { + _increaseComplexity(node); + } + super.visitCascadeExpression(node); + } + + @override + void visitSpreadElement(SpreadElement node) { + if (node.isNullAware) { + _increaseComplexity(node); + } + super.visitSpreadElement(node); + } + @override void visitWhileStatement(WhileStatement node) { _increaseComplexity(node); @@ -127,15 +222,19 @@ class CyclomaticComplexityFlowVisitor extends RecursiveAstVisitor { super.visitYieldStatement(node); } - void _visitBlock(Token? firstToken, Token? lastToken) { - var token = firstToken; - while (token != lastToken && token != null) { - if (token.matchesAny(_complexityTokenTypes)) { - _increaseComplexity(token); - } + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + // Stop recursion into nested function declarations. + } - token = token.next; - } + @override + void visitMethodDeclaration(MethodDeclaration node) { + // Stop recursion into nested method declarations. + } + + @override + void visitFunctionExpression(FunctionExpression node) { + // Stop recursion into nested function expressions (closures). } void _increaseComplexity(SyntacticEntity entity) { diff --git a/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart new file mode 100644 index 00000000..1d470061 --- /dev/null +++ b/lib/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_visitor.dart @@ -0,0 +1,47 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/visitors/cyclomatic_complexity_flow_visitor.dart'; + +/// Visitor that runs [CyclomaticComplexityFlowVisitor] on function, +/// constructor, and method bodies to count their cyclomatic complexity. +class CyclomaticComplexityVisitor extends SimpleAstVisitor { + final CyclomaticComplexityRule _rule; + final CyclomaticComplexityParameters _parameters; + + /// Creates a new instance of [CyclomaticComplexityVisitor]. + CyclomaticComplexityVisitor(this._rule, this._parameters); + + void _checkBody(Declaration declaration, FunctionBody? body) { + if (body == null) return; + + final isIgnored = _parameters.exclude.shouldIgnore(declaration); + if (isIgnored) return; + + final visitor = CyclomaticComplexityFlowVisitor(); + body.accept(visitor); + + if (visitor.complexityEntities.length >= _parameters.maxComplexity) { + _rule.reportAtNode( + body, + arguments: [_parameters.maxComplexity.toString()], + ); + } + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + _checkBody(node, node.functionExpression.body); + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + _checkBody(node, node.body); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + _checkBody(node, node.body); + } +} diff --git a/lint_test/cyclomatic_complexity_test.dart b/lint_test/cyclomatic_complexity_test.dart deleted file mode 100644 index 116e1fe0..00000000 --- a/lint_test/cyclomatic_complexity_test.dart +++ /dev/null @@ -1,57 +0,0 @@ -// ignore_for_file: literal_only_boolean_expressions, prefer_early_return -// ignore_for_file: no_empty_block, prefer_match_file_name - -/// Check complexity fail -/// -/// `cyclomatic_complexity_metric: max_complexity` -/// expect_lint: cyclomatic_complexity -void cyclomaticComplexity() { - if (true) { - if (true) { - if (true) { - if (true) {} - } - } - } -} - -class A { - /// expect_lint: cyclomatic_complexity - void cyclomaticComplexity() { - if (true) { - if (true) { - if (true) { - if (true) {} - } - } - } - } - - void simple() { - if (true) {} - } -} - -// no lint -void excludeMethod() { - if (true) { - if (true) { - if (true) { - if (true) {} - } - } - } -} - -class Exclude { - // no lint - void excludeMethod() { - if (true) { - if (true) { - if (true) { - if (true) {} - } - } - } - } -} diff --git a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart new file mode 100644 index 00000000..86236e45 --- /dev/null +++ b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart @@ -0,0 +1,442 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; +import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(CyclomaticComplexityRuleTest); + }); +} + +@reflectiveTest +class CyclomaticComplexityRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + static const _mockAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + cyclomatic_complexity: + max_complexity: 4 + exclude: + - class_name: Exclude + method_name: excludeMethod + - method_name: excludeMethod + '''; + + @override + void setUp() { + rule = CyclomaticComplexityRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + parametersParser: CyclomaticComplexityParameters.fromJson, + ); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); + } + + @override + String get analysisRule => CyclomaticComplexityRule.lintName; + + Future test_reports_when_complexity_exceeds_threshold() async { + await assertAutoDiagnostics(''' +void cyclomaticComplexity() ${expectLint(r'''{ + if (true) { + if (true) { + if (true) { + if (true) {} + } + } + } +}''')} +'''); + } + + Future + test_does_not_report_when_complexity_is_within_threshold() async { + await assertNoDiagnostics(r''' +void simple() { + if (true) {} +} +'''); + } + + Future test_does_not_report_on_excluded_method_in_class() async { + await assertNoDiagnostics(r''' +class Exclude { + void excludeMethod() { + if (true) { + if (true) { + if (true) { + if (true) {} + } + } + } + } +} +'''); + } + + Future test_does_not_report_on_excluded_top_level_function() async { + await assertNoDiagnostics(r''' +void excludeMethod() { + if (true) { + if (true) { + if (true) { + if (true) {} + } + } + } +} +'''); + } + + Future test_does_not_report_on_nested_functions() async { + await assertNoDiagnostics(r''' +void parentFunction() { + if (true) {} + + void nestedFunction() { + if (true) { + if (true) { + if (true) {} + } + } + } +} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_with_switch_expression() async { + await assertAutoDiagnostics(''' +String test(int val) ${expectLint(r'''{ + return switch (val) { + 1 => 'one', + 2 => 'two', + 3 => 'three', + _ => 'other', + }; +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_guard_clause() async { + await assertAutoDiagnostics(''' +String test(int val) ${expectLint(r'''{ + return switch (val) { + 1 when val > 0 => 'one', + 2 => 'two', + _ => 'other', + }; +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_with_switch_statement_and_patterns() async { + await assertAutoDiagnostics(''' +void test(Object val) ${expectLint(r'''{ + switch (val) { + case int x: + print('int'); + case String s: + print('string'); + case double d: + print('double'); + default: + print('other'); + } +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_logical_operators() async { + await assertAutoDiagnostics(''' +void test(bool a, bool b, bool c, bool d) ${expectLint(r'''{ + if (a && b && c && d) {} +}''')} +'''); + } + + Future test_does_not_count_complexity_in_closures() async { + await assertNoDiagnostics(r''' +void main() { + Calculator? calc; + group('a', () { + group('b', () { + group('c', () { + test('adds one to input values', () { + calc = Calculator(); + expect(calc?.addOne(2), 3); + expect(calc?.addOne(-7), -6); + expect(calc?.addOne(0), 1); + }); + }); + }); + }); +} +void group(String name, void Function() body) {} +void test(String name, void Function() body) {} +void expect(Object? actual, Object? matcher) {} +class Calculator { + int addOne(int value) => value + 1; +} +'''); + } + + Future + test_reports_when_constructor_complexity_exceeds_threshold() async { + await assertAutoDiagnostics(''' +class Complex { + Complex(int val) ${expectLint(r'''{ + if (val > 0) { + if (val > 1) { + if (val > 2) { + if (val > 3) {} + } + } + } + }''')} +} +'''); + } + + Future test_does_not_report_on_simple_constructor() async { + await assertNoDiagnostics(r''' +class Simple { + Simple(int val) { + if (val > 0) {} + } +} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_with_do_statement() async { + await assertAutoDiagnostics(''' +void testDoWhile() ${expectLint(r'''{ + int x = 0; + do { + if (x == 1) { + if (x == 2) { + if (x == 3) {} + } + } + } while (x < 10); +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_collection_if() async { + await assertAutoDiagnostics(''' +void test(bool a, bool b, bool c, bool d) ${expectLint(r'''{ + var result = [ + if (a) 1, + if (b) 2, + if (c) 3, + if (d) 4, + ]; +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_collection_for() async { + await assertAutoDiagnostics(''' +void test(List l1, List l2, List l3, List l4) ${expectLint(r'''{ + var result = [ + for (var x in l1) x, + for (var x in l2) x, + for (var x in l3) x, + for (var x in l4) x, + ]; +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_for_in_loops() async { + await assertAutoDiagnostics(''' +void test(List l1, List l2, List l3, List l4) ${expectLint(r'''{ + for (var x in l1) { + for (var y in l2) { + for (var z in l3) { + for (var w in l4) {} + } + } + } +}''')} +'''); + } + + + Future + test_reports_when_complexity_exceeds_threshold_due_to_logical_and_pattern() async { + await assertAutoDiagnostics(''' +void test(Object val) ${expectLint(r'''{ + if (val case int x && > 0 && < 10 && != 5 && != 6) {} +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_logical_or_pattern() async { + await assertAutoDiagnostics(''' +void test(Object val) ${expectLint(r'''{ + if (val case int x || int x || int x || int x || int x) {} +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_null_aware_cascade() async { + await assertAutoDiagnostics(''' +void test(dynamic a, dynamic b, dynamic c, dynamic d) ${expectLint(r'''{ + a?..f(); + b?..f(); + c?..f(); + d?..f(); +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_null_aware_spread() async { + await assertAutoDiagnostics(''' +void test(List? a, List? b, List? c, List? d) ${expectLint(r'''{ + var r = [ + ...?a, + ...?b, + ...?c, + ...?d, + ]; +}''')} +'''); + } + + Future test_does_not_report_on_normal_cascades_and_spreads() async { + await assertNoDiagnostics(r''' +void test(dynamic x, List y) { + x..a()..b(); + [...y]; +} +'''); + } + + Future + test_does_not_report_when_collection_elements_within_threshold() async { + await assertNoDiagnostics(r''' +void test(bool a, List list) { + var result = [ + if (a) 1, + for (var x in list) x, + ]; +} +'''); + } + + Future test_does_not_double_count_cascade_sections() async { + await assertNoDiagnostics(r''' +void test(dynamic x) { + x?..a()..b()..c()..d()..e(); +} +'''); + } + + Future test_does_not_report_on_simple_pattern_matching() async { + await assertNoDiagnostics(r''' +void test(Object val) { + if (val case int x && > 0) {} +} +'''); + } + + Future + test_does_not_report_on_pattern_matching_or_within_threshold() async { + await assertNoDiagnostics(r''' +void test(Object val) { + if (val case int x || int x) {} +} +'''); + } + + Future test_does_not_count_collection_elements_in_closures() async { + await assertNoDiagnostics(r''' +void parent() { + final closure = () { + final list = [ + if (true) 1, + if (true) 2, + if (true) 3, + if (true) 4, + ]; + }; +} +'''); + } + + Future + test_does_not_count_null_aware_cascades_and_spreads_in_closures() async { + await assertNoDiagnostics(r''' +void parent() { + final closure = (dynamic x, List? y) { + x?..a()..b()..c()..d(); + [...?y]; + }; +} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_null_aware_function_calls() async { + await assertAutoDiagnostics(''' +void test(Function? a, Function? b, Function? c, Function? d) ${expectLint(r'''{ + a?.call(); + b?.call(); + c?.call(); + d?.call(); +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_null_check_patterns() async { + await assertAutoDiagnostics(''' +void test(Object val) ${expectLint(r'''{ + if (val case [int? a?, int? b?, int? c?]) {} +}''')} +'''); + } + + Future + test_reports_when_complexity_exceeds_threshold_due_to_null_assert_patterns() async { + await assertAutoDiagnostics(''' +void test(Object val) ${expectLint(r'''{ + if (val case [int? a!, int? b!, int? c!]) {} +}''')} +'''); + } + + Future + test_does_not_report_on_null_check_and_assert_patterns_within_threshold() async { + await assertNoDiagnostics(r''' +void test(Object val) { + if (val case [int? a?, int? b!]) {} +} +'''); + } +} From 9c5842ae3417fb957302d61c80afe984e8cead8a Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Tue, 23 Jun 2026 15:52:57 +0300 Subject: [PATCH 23/49] refactor: migrate prefer_first (#286) * refactor: migrate prefer_first and tests * refactor: migrate prefer_first fix * refactor: prefer firstOrNull * fix: only apply one fix to one node at a time * fix: handle null awareness * refactor: use AutoTestLintOffsets --- lib/main.dart | 12 +- .../prefer_first/fixes/prefer_first_fix.dart | 97 +++++++++-------- .../lints/prefer_first/prefer_first_rule.dart | 47 +++----- .../visitors/prefer_first_visitor.dart | 26 ++--- lint_test/prefer_first_test.dart | 21 ---- .../prefer_first/prefer_first_rule_test.dart | 103 ++++++++++++++++++ 6 files changed, 191 insertions(+), 115 deletions(-) delete mode 100644 lint_test/prefer_first_test.dart create mode 100644 test/lints/prefer_first/prefer_first_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index 44e0b44d..deb89a61 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,9 +14,10 @@ import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexit import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; +import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; +import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; import 'package:solid_lints/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart'; -import 'package:solid_lints/src/lints/use_nearest_context/fixes/replace_with_nearest_context_parameter_fix.dart'; import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart'; /// The entry point for the Solid Lints analyser server plugin. @@ -40,6 +41,7 @@ class SolidLintsPlugin extends Plugin { final avoidUnnecessaryTypeAssertionsRule = AvoidUnnecessaryTypeAssertionsRule(); final doubleLiteralFormatRule = DoubleLiteralFormatRule(); + final preferFirstRule = PreferFirstRule(); final lintRules = [ AvoidFinalWithGetterRule(), @@ -60,6 +62,10 @@ class SolidLintsPlugin extends Plugin { parametersParser: CyclomaticComplexityParameters.fromJson, ), UseNearestContextRule(), + preferFirstRule, + // TODO: Add more lint rules and use analysisLoader + // for rules that need parameters + // For example: `CyclomaticComplexityRule(analysisLoader)` ]; for (final lintRule in lintRules) { @@ -86,8 +92,8 @@ class SolidLintsPlugin extends Plugin { ); registry.registerFixForRule( - UseNearestContextRule.code, - ReplaceWithNearestContextParameterFix.new, + preferFirstRule.diagnosticCode, + PreferFirstFix.new, ); } } diff --git a/lib/src/lints/prefer_first/fixes/prefer_first_fix.dart b/lib/src/lints/prefer_first/fixes/prefer_first_fix.dart index d677ffde..0dad920b 100644 --- a/lib/src/lints/prefer_first/fixes/prefer_first_fix.dart +++ b/lib/src/lints/prefer_first/fixes/prefer_first_fix.dart @@ -1,67 +1,70 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/diagnostic/diagnostic.dart'; -import 'package:analyzer/source/source_range.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; +import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; /// A Quick fix for `prefer_first` rule /// Suggests to replace iterable access expressions -class PreferFirstFix extends DartFix { +class PreferFirstFix extends ParsedCorrectionProducer { static const _replaceComment = "Replace with 'first'."; + /// Creates a new instance of [PreferFirstFix] + PreferFirstFix({required super.context}); + @override - void run( - CustomLintResolver resolver, - ChangeReporter reporter, - CustomLintContext context, - Diagnostic analysisError, - List others, - ) { - context.registry.addMethodInvocation((node) { - if (analysisError.sourceRange.intersects(node.sourceRange)) { - final correction = _createCorrection(node); + FixKind get fixKind => const FixKind( + 'solid_lints.fix.${PreferFirstRule.lintName}', + DartFixKindPriority.standard, + _replaceComment, + ); - _addReplacement(reporter, node, correction); - } - }); + @override + FixKind get multiFixKind => const FixKind( + 'solid_lints.fix.multi.${PreferFirstRule.lintName}', + DartFixKindPriority.standard, + '$_replaceComment across files', + ); - context.registry.addIndexExpression((node) { - if (analysisError.sourceRange.intersects(node.sourceRange)) { - final correction = _createCorrection(node); + @override + CorrectionApplicability get applicability => + CorrectionApplicability.automatically; - _addReplacement(reporter, node, correction); - } - }); + @override + Future compute(ChangeBuilder builder) async { + final targetNode = node.thisOrAncestorMatching( + (n) => n is MethodInvocation || n is IndexExpression, + ); + if (targetNode is! Expression) return; + + final correction = _createCorrection(targetNode); + await _addReplacement(builder, targetNode, correction); } String _createCorrection(Expression expression) { - if (expression is MethodInvocation) { - return expression.isCascaded - ? '..first' - : '${expression.target ?? ''}.first'; - } else if (expression is IndexExpression) { - return expression.isCascaded - ? '..first' - : '${expression.target ?? ''}.first'; - } else { - return '.first'; + switch (expression) { + case MethodInvocation(isCascaded: true, :final isNullAware): + case IndexExpression(isCascaded: true, :final isNullAware): + return isNullAware ? '?.first' : '..first'; + + case MethodInvocation(:final target?, :final isNullAware): + case IndexExpression(:final target?, :final isNullAware): + return isNullAware ? '$target?.first' : '$target.first'; + + default: + return '.first'; } } - void _addReplacement( - ChangeReporter reporter, - Expression node, + Future _addReplacement( + ChangeBuilder builder, + AstNode node, String correction, - ) { - final changeBuilder = reporter.createChangeBuilder( - message: _replaceComment, - priority: 1, + ) async { + await builder.addDartFileEdit( + file, + (builder) => builder.addSimpleReplacement(node.sourceRange, correction), ); - - changeBuilder.addDartFileEdit((builder) { - builder.addSimpleReplacement( - SourceRange(node.offset, node.length), - correction, - ); - }); } } diff --git a/lib/src/lints/prefer_first/prefer_first_rule.dart b/lib/src/lints/prefer_first/prefer_first_rule.dart index 0af19607..b644cffe 100644 --- a/lib/src/lints/prefer_first/prefer_first_rule.dart +++ b/lib/src/lints/prefer_first/prefer_first_rule.dart @@ -1,8 +1,7 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/prefer_first/visitors/prefer_first_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Warns about usage of iterable[0] or iterable.elementAt(0) instead of @@ -31,37 +30,23 @@ class PreferFirstRule extends SolidLintRule { /// parameters reaches the maximum value. static const lintName = 'prefer_first'; - PreferFirstRule._(super.config); + static const _code = LintCode( + lintName, + "Use first instead of accessing the element at zero index.", + ); - /// Creates a new instance of [PreferFirstRule] - /// based on the lint configuration. - factory PreferFirstRule.createRule(CustomLintConfigs configs) { - final config = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (value) => - 'Use first instead of accessing the element at zero index.', - ); + @override + LintCode get diagnosticCode => _code; - return PreferFirstRule._(config); - } + /// Creates a new instance of [PreferFirstRule] + PreferFirstRule() : super(name: lintName, description: _code.problemMessage); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addCompilationUnit((node) { - final visitor = PreferFirstVisitor(); - node.accept(visitor); - - for (final element in visitor.expressions) { - reporter.atNode(element, code); - } - }); + final visitor = PreferFirstVisitor(this); + registry.addCompilationUnit(this, visitor); } - - @override - List getFixes() => [PreferFirstFix()]; } diff --git a/lib/src/lints/prefer_first/visitors/prefer_first_visitor.dart b/lib/src/lints/prefer_first/visitors/prefer_first_visitor.dart index 984d4515..222734c6 100644 --- a/lib/src/lints/prefer_first/visitors/prefer_first_visitor.dart +++ b/lib/src/lints/prefer_first/visitors/prefer_first_visitor.dart @@ -1,14 +1,15 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; import 'package:solid_lints/src/utils/types_utils.dart'; /// The AST visitor that will collect all Iterable access expressions /// which can be replaced with .first class PreferFirstVisitor extends RecursiveAstVisitor { - final _expressions = []; + final PreferFirstRule _rule; - /// List of all Iterable access expressions - Iterable get expressions => _expressions; + /// Creates a new instance of [PreferFirstVisitor] + PreferFirstVisitor(this._rule); @override void visitMethodInvocation(MethodInvocation node) { @@ -16,12 +17,11 @@ class PreferFirstVisitor extends RecursiveAstVisitor { final isIterable = isIterableOrSubclass(node.realTarget?.staticType); final isElementAt = node.methodName.name == 'elementAt'; - if (isIterable && isElementAt) { - final arg = node.argumentList.arguments.first; + if (!isIterable || !isElementAt) return; - if (arg is IntegerLiteral && arg.value == 0) { - _expressions.add(node); - } + final arg = node.argumentList.arguments.firstOrNull; + if (arg case IntegerLiteral(value: 0)) { + _rule.reportAtNode(node); } } @@ -29,12 +29,12 @@ class PreferFirstVisitor extends RecursiveAstVisitor { void visitIndexExpression(IndexExpression node) { super.visitIndexExpression(node); - if (isListOrSubclass(node.realTarget.staticType)) { - final index = node.index; + if (!isListOrSubclass(node.realTarget.staticType)) return; - if (index is IntegerLiteral && index.value == 0) { - _expressions.add(node); - } + final index = node.index; + + if (index case IntegerLiteral(value: 0)) { + _rule.reportAtNode(node); } } } diff --git a/lint_test/prefer_first_test.dart b/lint_test/prefer_first_test.dart deleted file mode 100644 index 2527d828..00000000 --- a/lint_test/prefer_first_test.dart +++ /dev/null @@ -1,21 +0,0 @@ -/// Check the `prefer_first` rule -void fun() { - const zero = 0; - final list = [0, 1, 2, 3]; - final set = {0, 1, 2, 3}; - final map = {0: 0, 1: 1, 2: 2, 3: 3}; - - // expect_lint: prefer_first - list[0]; - list[zero]; - // expect_lint: prefer_first - list.elementAt(0); - list.elementAt(zero); - // expect_lint: prefer_first - set.elementAt(0); - - // expect_lint: prefer_first - map.keys.elementAt(0); - // expect_lint: prefer_first - map.values.elementAt(0); -} diff --git a/test/lints/prefer_first/prefer_first_rule_test.dart b/test/lints/prefer_first/prefer_first_rule_test.dart new file mode 100644 index 00000000..50bdf212 --- /dev/null +++ b/test/lints/prefer_first/prefer_first_rule_test.dart @@ -0,0 +1,103 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferFirstRuleTest); + }); +} + +@reflectiveTest +class PreferFirstRuleTest extends AnalysisRuleTest with AutoTestLintOffsets { + @override + void setUp() { + rule = PreferFirstRule(); + super.setUp(); + } + + void test_reports_on_list_index_access_with_zero_literal() async { + await assertAutoDiagnostics(''' +final list1 = [0, 1, 2, 3]; +var a = ${expectLint('list1[0]')}; + +void main () { + final list2 = [1, 0, 2, 3]; + ${expectLint('list1[0]')}; + ${expectLint('list2[0]')}; +} +'''); + } + + void + test_does_not_report_on_list_index_access_with_variable_or_constant() async { + await assertNoDiagnostics(r''' +const zero = 0; +final zeroVar = 0; + +final list = [0, 1, 2, 3]; + +var a = list[zero]; +var b = list[1 - 1]; +var c = list[zeroVar]; +'''); + } + + void test_reports_on_list_subclasses() async { + await assertAutoDiagnostics(''' +abstract class MyList implements List {} + +T getFirst(MyList list) { + return ${expectLint('list[0]')}; +} +'''); + } + + void test_reports_on_element_at_access_with_zero_literal() async { + await assertAutoDiagnostics(''' +final list1 = [0, 1, 2, 3]; +var a = ${expectLint('list1.elementAt(0)')}; + +void main () { + final list2 = [1, 0, 2, 3]; + ${expectLint('list1.elementAt(0)')}; + ${expectLint('list2.elementAt(0)')}; +} +'''); + } + + void + test_does_not_report_on_element_at_access_with_variable_or_constant() async { + await assertNoDiagnostics(r''' +const zero = 0; +final zeroVar = 0; + +final list = [0, 1, 2, 3]; + +var a = list.elementAt(zero); +var b = list.elementAt(1 - 1); +var c = list.elementAt(zeroVar); +'''); + } + + void test_reports_on_iterable_subclasses() async { + await assertAutoDiagnostics(''' +abstract class MyIterable implements Iterable {} + +T getFirst(MyIterable iterable) { + return ${expectLint('iterable.elementAt(0)')}; +} + +void main () { + final set = {0, 1, 2, 3}; + final map = {0: 0, 1: 1, 2: 2, 3: 3}; + + ${expectLint('set.elementAt(0)')}; + ${expectLint('map.keys.elementAt(0)')}; + ${expectLint('map.values.elementAt(0)')}; +} +'''); + } +} From c6cd0b2e93f5c391f4a335f8b4f2d3d8310f8ab8 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 24 Jun 2026 15:59:04 +0300 Subject: [PATCH 24/49] refactor: migrate function_lines_of_code rule (#296) * refactor: migrate function_lines_of_code rule (#251) * refactor: update function expression visitor logic * refactor: migrate function_lines_of_code tests to AutoTestLintOffsets * refactor: optimize currentUnit access in function lines of code visitor * feat: update description and add test case for function lines of code excluding comments * refactor: simplify lines of code visitor logic * refactor: migrate function_lines_of_code rule tests to a table-driven approach * refactor: introduce TableDrivenRuleTest base class to consolidate table-driven test logic * refactor: simplify test code generation by introducing string extensions and moving lint markers into generation logic * refactor: move parameter parser initialization into FunctionLinesOfCodeRule constructor * refactor: move cyclomatic complexity parameters initialization inside the rule constructor --------- Co-authored-by: Illia Aihistov --- lib/main.dart | 6 +- .../cyclomatic_complexity_rule.dart | 2 +- .../function_lines_of_code_rule.dart | 115 ++-- .../function_lines_of_code_parameters.dart | 8 + .../function_lines_of_code_rule_visitor.dart | 73 +++ .../analysis_options.yaml | 14 - .../function_lines_of_code_test.dart | 535 ------------------ .../avoid_returning_widgets_rule_test.dart | 2 - .../cyclomatic_complexity_rule_test.dart | 2 - .../function_lines_of_code_rule_test.dart | 143 +++++ .../models/test_case.dart | 38 ++ test/src/utils/code_generators.dart | 8 + test/src/utils/table_driven_rule_test.dart | 49 ++ 13 files changed, 362 insertions(+), 633 deletions(-) create mode 100644 lib/src/lints/function_lines_of_code/visitors/function_lines_of_code_rule_visitor.dart delete mode 100644 lint_test/function_lines_of_code_test/analysis_options.yaml delete mode 100644 lint_test/function_lines_of_code_test/function_lines_of_code_test.dart create mode 100644 test/src/lints/function_lines_of_code/function_lines_of_code_rule_test.dart create mode 100644 test/src/lints/function_lines_of_code/models/test_case.dart create mode 100644 test/src/utils/code_generators.dart create mode 100644 test/src/utils/table_driven_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index deb89a61..d9d338a8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,9 +11,9 @@ import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_un import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart'; import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart'; import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; -import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; +import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart'; import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; @@ -54,12 +54,14 @@ class SolidLintsPlugin extends Plugin { AvoidReturningWidgetsRule( analysisOptionsLoader: analysisLoader, ), + FunctionLinesOfCodeRule( + analysisOptionsLoader: analysisLoader, + ), AvoidUnusedParametersRule( analysisOptionsLoader: analysisLoader, ), CyclomaticComplexityRule( analysisOptionsLoader: analysisLoader, - parametersParser: CyclomaticComplexityParameters.fromJson, ), UseNearestContextRule(), preferFirstRule, diff --git a/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart b/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart index c7024791..79a6b671 100644 --- a/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart +++ b/lib/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart @@ -39,12 +39,12 @@ class CyclomaticComplexityRule /// Creates a new instance of [CyclomaticComplexityRule]. CyclomaticComplexityRule({ required super.analysisOptionsLoader, - required super.parametersParser, }) : super.withParameters( name: lintName, description: 'Limit for the number of linearly independent paths ' "through a program's source code.", + parametersParser: CyclomaticComplexityParameters.fromJson, ); @override diff --git a/lib/src/lints/function_lines_of_code/function_lines_of_code_rule.dart b/lib/src/lints/function_lines_of_code/function_lines_of_code_rule.dart index a8e7c89e..49dc0d42 100644 --- a/lib/src/lints/function_lines_of_code/function_lines_of_code_rule.dart +++ b/lib/src/lints/function_lines_of_code/function_lines_of_code_rule.dart @@ -1,9 +1,8 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart'; -import 'package:solid_lints/src/lints/function_lines_of_code/visitors/function_lines_of_code_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; +import 'package:solid_lints/src/lints/function_lines_of_code/visitors/function_lines_of_code_rule_visitor.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// An approximate metric of meaningful lines of source code inside a function, @@ -12,90 +11,52 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// ### Example config: /// /// ```yaml -/// custom_lint: -/// rules: -/// - function_lines_of_code: -/// max_lines: 100 -/// excludeNames: -/// - "Build" +/// plugins: +/// solid_lints: +/// diagnostics: +/// function_lines_of_code: +/// max_lines: 100 +/// exclude: +/// - "Build" /// ``` class FunctionLinesOfCodeRule extends SolidLintRule { - /// This lint rule represents the error if number of - /// parameters reaches the maximum value. + /// This lint rule name. static const lintName = 'function_lines_of_code'; - FunctionLinesOfCodeRule._(super.config); - - /// Creates a new instance of [FunctionLinesOfCodeRule] - /// based on the lint configuration. - factory FunctionLinesOfCodeRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - paramsParser: FunctionLinesOfCodeParameters.fromJson, - problemMessage: (value) => - 'The maximum allowed number of lines is ${value.maxLines}. ' - 'Try splitting this function into smaller parts.', - ); - - return FunctionLinesOfCodeRule._(rule); - } + static const _code = LintCode( + lintName, + 'The maximum allowed number of lines is {0}. ' + 'Try splitting this function into smaller parts.', + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, - ) { - void checkNode(AstNode node) => _checkNode(resolver, reporter, node); + DiagnosticCode get diagnosticCode => _code; - void checkDeclarationNode(Declaration node) { - final isIgnored = config.parameters.exclude.shouldIgnore(node); - if (isIgnored) { - return; - } - checkNode(node); - } - - // Check for an anonymous function - void checkFunctionExpressionNode(FunctionExpression node) { - // If a FunctionExpression is an immediate child of a FunctionDeclaration - // this means it's a named function, which are already check as part of - // addFunctionDeclaration call. - if (node.parent is FunctionDeclaration) { - return; - } - checkNode(node); - } - - context.registry.addFunctionDeclaration(checkDeclarationNode); - context.registry.addMethodDeclaration(checkDeclarationNode); - context.registry.addFunctionExpression(checkFunctionExpressionNode); - } + /// Creates a new instance of [FunctionLinesOfCodeRule] + FunctionLinesOfCodeRule({ + required super.analysisOptionsLoader, + }) : super.withParameters( + name: lintName, + description: + 'An approximate metric of meaningful lines of source code ' + 'inside a function, excluding blank lines and comments.', + parametersParser: FunctionLinesOfCodeParameters.fromJson, + ); - void _checkNode( - CustomLintResolver resolver, - DiagnosticReporter reporter, - AstNode node, + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - final visitor = FunctionLinesOfCodeVisitor(resolver.lineInfo); - node.visitChildren(visitor); + super.registerNodeProcessors(registry, context); - if (visitor.linesWithCode.length > config.parameters.maxLines) { - if (node is! AnnotatedNode) { - reporter.atNode(node, code); - return; - } + final parameters = + getParametersForContext(context) ?? + FunctionLinesOfCodeParameters.empty(); - final startOffset = node.firstTokenAfterCommentAndMetadata.offset; - final lengthDifference = startOffset - node.offset; + final visitor = FunctionLinesOfCodeRuleVisitor(this, context, parameters); - reporter.atOffset( - offset: startOffset, - length: node.length - lengthDifference, - diagnosticCode: code, - ); - } + registry.addCompilationUnit(this, visitor); } } diff --git a/lib/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart b/lib/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart index 7df6da2a..3e748c49 100644 --- a/lib/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart +++ b/lib/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart @@ -18,6 +18,14 @@ class FunctionLinesOfCodeParameters { required this.exclude, }); + /// Empty [FunctionLinesOfCodeParameters] model with default max lines. + factory FunctionLinesOfCodeParameters.empty() { + return FunctionLinesOfCodeParameters( + maxLines: _defaultMaxLines, + exclude: ExcludedIdentifiersListParameter(exclude: []), + ); + } + /// Method for creating from json data factory FunctionLinesOfCodeParameters.fromJson(Map json) => FunctionLinesOfCodeParameters( diff --git a/lib/src/lints/function_lines_of_code/visitors/function_lines_of_code_rule_visitor.dart b/lib/src/lints/function_lines_of_code/visitors/function_lines_of_code_rule_visitor.dart new file mode 100644 index 00000000..28e9eda8 --- /dev/null +++ b/lib/src/lints/function_lines_of_code/visitors/function_lines_of_code_rule_visitor.dart @@ -0,0 +1,73 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart'; +import 'package:solid_lints/src/lints/function_lines_of_code/models/function_lines_of_code_parameters.dart'; +import 'package:solid_lints/src/lints/function_lines_of_code/visitors/function_lines_of_code_visitor.dart'; + +/// A visitor that reports on functions/methods exceeding the max line limit. +class FunctionLinesOfCodeRuleVisitor extends RecursiveAstVisitor { + final FunctionLinesOfCodeRule _rule; + final RuleContext _context; + final FunctionLinesOfCodeParameters _parameters; + + /// Creates a new instance of [FunctionLinesOfCodeRuleVisitor]. + FunctionLinesOfCodeRuleVisitor(this._rule, this._context, this._parameters); + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + final isIgnored = _parameters.exclude.shouldIgnore(node); + if (!isIgnored) { + _checkNode(node); + } + super.visitFunctionDeclaration(node); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + final isIgnored = _parameters.exclude.shouldIgnore(node); + if (!isIgnored) { + _checkNode(node); + } + super.visitMethodDeclaration(node); + } + + @override + void visitFunctionExpression(FunctionExpression node) { + if (node.parent is! FunctionDeclaration) { + _checkNode(node); + } + super.visitFunctionExpression(node); + } + + void _checkNode(AstNode node) { + final currentUnit = _context.currentUnit; + if (currentUnit == null) return; + + final lineInfo = currentUnit.unit.lineInfo; + final visitor = FunctionLinesOfCodeVisitor(lineInfo); + node.visitChildren(visitor); + if (visitor.linesWithCode.length <= _parameters.maxLines) return; + + final reporter = currentUnit.diagnosticReporter; + if (node is! AnnotatedNode) { + reporter.atNode( + node, + _rule.diagnosticCode, + arguments: [_parameters.maxLines], + ); + + return; + } + + final startOffset = node.firstTokenAfterCommentAndMetadata.offset; + final lengthDifference = startOffset - node.offset; + + reporter.atOffset( + offset: startOffset, + length: node.length - lengthDifference, + diagnosticCode: _rule.diagnosticCode, + arguments: [_parameters.maxLines], + ); + } +} diff --git a/lint_test/function_lines_of_code_test/analysis_options.yaml b/lint_test/function_lines_of_code_test/analysis_options.yaml deleted file mode 100644 index 459a4d79..00000000 --- a/lint_test/function_lines_of_code_test/analysis_options.yaml +++ /dev/null @@ -1,14 +0,0 @@ -analyzer: - plugins: - - ../custom_lint - -custom_lint: - rules: - - function_lines_of_code: - max_lines: 5 - exclude: - - class_name: ClassWithLongMethods - method_name: longMethodExcluded - - method_name: longFunctionExcluded - - longFunctionExcludedByDeclarationName - - longMethodExcludedByDeclarationName diff --git a/lint_test/function_lines_of_code_test/function_lines_of_code_test.dart b/lint_test/function_lines_of_code_test/function_lines_of_code_test.dart deleted file mode 100644 index 032f5cd6..00000000 --- a/lint_test/function_lines_of_code_test/function_lines_of_code_test.dart +++ /dev/null @@ -1,535 +0,0 @@ -// ignore_for_file: prefer_match_file_name - -class ClassWithLongMethods { - // expect_lint: function_lines_of_code - int longMethod() { - var i = 0; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - - return i; - } - - // Excluded by method_name - int longMethodExcluded() { - var i = 0; - i++; - i++; - i++; - i++; - - return i; - } - -// Excluded by declaration_name - int longMethodExcludedByDeclarationName() { - var i = 0; - i++; - i++; - i++; - i++; - - return i; - } - - int notLongMethod() { - var i = 0; - i++; - i++; - i++; - - return i; - } -} - -int notLongFunction() { - var i = 0; - i++; - i++; - i++; - - return i; -} - -// expect_lint: function_lines_of_code -int longFunction() { - var i = 0; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - i++; - - return i; -} - -// Excluded by method_name -int longFunctionExcluded() { - var i = 0; - i++; - i++; - i++; - i++; - - return i; -} - -// Excluded by declaration_name -int longFunctionExcludedByDeclarationName() { - var i = 0; - i++; - i++; - i++; - i++; - - return i; -} - -// expect_lint: function_lines_of_code -final longAnonymousFunction = () { - var i = 0; - i++; - i++; - i++; - i++; - - return i; -}; - -final notLongAnonymousFunction = () { - var i = 0; - i++; - i++; - i++; - - return i; -}; diff --git a/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart b/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart index f780c3df..b5286057 100644 --- a/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart +++ b/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart @@ -2,7 +2,6 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:analyzer_testing/utilities/utilities.dart'; import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; -import 'package:solid_lints/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; void main() { @@ -103,7 +102,6 @@ class BaseWidget extends StatelessWidget { analysisOptionsLoader: AnalysisOptionsLoader( resourceProvider: resourceProvider, ), - parametersParser: AvoidReturningWidgetsParameters.fromJson, ); newPackage('flutter') ..addFile('lib/widgets.dart', _mockFlutterWidgetsContent); diff --git a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart index 86236e45..4226997d 100644 --- a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart +++ b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart @@ -2,7 +2,6 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:analyzer_testing/utilities/utilities.dart'; import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; -import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; import '../../../lints/auto_test_lint_offsets.dart'; @@ -34,7 +33,6 @@ plugins: analysisOptionsLoader: AnalysisOptionsLoader( resourceProvider: resourceProvider, ), - parametersParser: CyclomaticComplexityParameters.fromJson, ); super.setUp(); diff --git a/test/src/lints/function_lines_of_code/function_lines_of_code_rule_test.dart b/test/src/lints/function_lines_of_code/function_lines_of_code_rule_test.dart new file mode 100644 index 00000000..163f0988 --- /dev/null +++ b/test/src/lints/function_lines_of_code/function_lines_of_code_rule_test.dart @@ -0,0 +1,143 @@ +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../utils/code_generators.dart'; +import '../../utils/table_driven_rule_test.dart'; +import 'models/test_case.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(FunctionLinesOfCodeRuleTest); + }); +} + +@reflectiveTest +class FunctionLinesOfCodeRuleTest extends TableDrivenRuleTest { + static const _excludedClassName = 'ExcludedClass'; + static const _nonExcludedClassName = 'NonExcludedClass'; + static const _excludedMethodName = 'excludedMethod'; + static const _excludedMethodByDeclaration = 'excludedMethodByDeclaration'; + static const _excludedFunctionName = 'excludedFunction'; + static const _excludedFunctionByDeclaration = 'excludedFunctionByDeclaration'; + static const _excludedByString = 'excludedByString'; + + static const _mockAnalysisOptionsContent = + ''' +plugins: + solid_lints: + diagnostics: + function_lines_of_code: + max_lines: 4 + exclude: + - class_name: $_excludedClassName + method_name: $_excludedMethodName + - method_name: $_excludedMethodByDeclaration + - method_name: $_excludedFunctionName + - method_name: $_excludedFunctionByDeclaration + - $_excludedByString + '''; + + /// All test cases for the function_lines_of_code rule. + static const testTable = { + // --- Threshold: fail when code lines > max (4) --- + TestCase(codeLines: 5): ExpectedResult.fail, + TestCase(codeLines: 5, comments: true): ExpectedResult.fail, + + // --- Threshold: pass when code lines ≤ max (4) --- + TestCase(codeLines: 4): ExpectedResult.pass, + TestCase(codeLines: 3): ExpectedResult.pass, + TestCase(codeLines: 4, comments: true): ExpectedResult.pass, + + // --- Exclude config --- + TestCase( + codeLines: 5, + className: _excludedClassName, + methodName: _excludedMethodName, + ): ExpectedResult.pass, + TestCase( + codeLines: 5, + className: _excludedClassName, + methodName: _excludedMethodByDeclaration, + ): ExpectedResult.pass, + TestCase(codeLines: 5, methodName: _excludedFunctionName): + ExpectedResult.pass, + TestCase(codeLines: 5, methodName: _excludedFunctionByDeclaration): + ExpectedResult.pass, + TestCase( + codeLines: 5, + className: _nonExcludedClassName, + methodName: _excludedByString, + ): ExpectedResult.pass, + + // --- Anonymous functions --- + TestCase(codeLines: 5, anonymous: true): ExpectedResult.fail, + }; + + @override + void setUp() { + rule = FunctionLinesOfCodeRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); + } + + /// Generates test source code for the given [testCase] based on [expected]. + @override + String generateCode(TestCase testCase, ExpectedResult expected) { + final indent = testCase.className != null ? ' ' : ' '; + + final bodyBuffer = StringBuffer(); + if (testCase.comments) { + bodyBuffer.writeln('$indent// This is a single-line comment.'); + } + + bodyBuffer.writeln('${indent}var i = 0;'); + + final extra = testCase.codeLines - 2; + if (extra > 0) { + bodyBuffer.writeln('${indent}i++;'.repeatLines(extra)); + } + + if (testCase.comments) { + bodyBuffer.writeln('$indent/*'); + bodyBuffer.writeln('$indent * This is a multi-line comment.'); + bodyBuffer.writeln('$indent */'); + } + + bodyBuffer.write('${indent}return i;\n'); + final body = bodyBuffer.toString(); + + String wrap(String target) { + return expected == ExpectedResult.fail ? expectLint(target) : target; + } + + if (testCase.anonymous) { + final fn = '() {\n$body}'; + return 'final longAnonymousFunction = ${wrap(fn)};\n'; + } + + final name = testCase.methodName ?? 'function'; + + if (testCase.className != null) { + final method = ' int $name() {\n$body }'; + return '\nclass ${testCase.className} {\n${wrap(method)}\n}\n'; + } + + final fn = 'int $name() {\n$body}'; + return '${wrap(fn)}\n'; + } + + Future test_function_lines_of_code_cases() async { + await runTableTests(testTable); + } +} diff --git a/test/src/lints/function_lines_of_code/models/test_case.dart b/test/src/lints/function_lines_of_code/models/test_case.dart new file mode 100644 index 00000000..49b36416 --- /dev/null +++ b/test/src/lints/function_lines_of_code/models/test_case.dart @@ -0,0 +1,38 @@ +/// Parameters for a single function_lines_of_code test case. +class TestCase { + /// Number of code lines inside the function body. + /// + /// `var i = 0;` and `return i;` are always present, so [codeLines] of 4 + /// means two extra `i++;` statements. + final int codeLines; + + /// Whether to include single-line and multi-line comments (not counted). + final bool comments; + + /// If set, wraps the function inside `class [className] { ... }`. + final String? className; + + /// Overrides the default function name (`function`). + final String? methodName; + + /// If true, generates an anonymous function literal instead. + final bool anonymous; + + const TestCase({ + required this.codeLines, + this.comments = false, + this.className, + this.methodName, + this.anonymous = false, + }); + + @override + String toString() { + final parts = ['codeLines: $codeLines']; + if (comments) parts.add('comments: true'); + if (className != null) parts.add('className: $className'); + if (methodName != null) parts.add('methodName: $methodName'); + if (anonymous) parts.add('anonymous: true'); + return '(${parts.join(', ')})'; + } +} diff --git a/test/src/utils/code_generators.dart b/test/src/utils/code_generators.dart new file mode 100644 index 00000000..5947a168 --- /dev/null +++ b/test/src/utils/code_generators.dart @@ -0,0 +1,8 @@ +/// Extension utilities for generating test code. +extension RepeatLinesExtension on String { + /// Repeats this string [times] times, joining with newlines. + /// + /// Useful for generating test Dart code with a specific number of lines. + String repeatLines(int times) => + List.generate(times, (_) => this).join('\n'); +} diff --git a/test/src/utils/table_driven_rule_test.dart b/test/src/utils/table_driven_rule_test.dart new file mode 100644 index 00000000..f27fdaad --- /dev/null +++ b/test/src/utils/table_driven_rule_test.dart @@ -0,0 +1,49 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:test/test.dart' hide setUp; + +import '../../lints/auto_test_lint_offsets.dart'; + +/// Result expected from a table-driven test case. +enum ExpectedResult { + /// The test case should pass without diagnostics. + pass, + + /// The test case should fail with a lint diagnostic. + fail, +} + +/// Base class for table-driven lint rule tests. +abstract class TableDrivenRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + /// Disposes and recreates the analysis context. + Future resetAnalyzerContext() async { + // ignore: invalid_use_of_visible_for_testing_member + await super.tearDown(); + setUp(); + } + + /// Generates the test source code for a given [testCase] based on [expected]. + String generateCode(T testCase, ExpectedResult expected); + + /// Executes all test cases defined in the [testTable] map. + Future runTableTests(Map testTable) async { + for (final MapEntry(key: testCase, value: expected) in testTable.entries) { + final source = generateCode(testCase, expected); + + try { + switch (expected) { + case ExpectedResult.pass: + await assertNoDiagnostics(source); + case ExpectedResult.fail: + await assertAutoDiagnostics(source); + } + } on TestFailure catch (e) { + fail('Case $testCase: $e'); + } + + // Reset the analysis context between test cases to bypass the analyzer's + // internal caching and ensure the newly generated code is re-analyzed. + await resetAnalyzerContext(); + } + } +} From 56125c8f53cd0ce4259f79243d5d5bb658a4e35e Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 24 Jun 2026 19:19:01 +0300 Subject: [PATCH 25/49] refactor: rename table_driven_rule_test to table_driven_rule_test_base (#301) Co-authored-by: Illia Aihistov --- .../function_lines_of_code_rule_test.dart | 2 +- ...e_driven_rule_test.dart => table_driven_rule_test_base.dart} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename test/src/utils/{table_driven_rule_test.dart => table_driven_rule_test_base.dart} (100%) diff --git a/test/src/lints/function_lines_of_code/function_lines_of_code_rule_test.dart b/test/src/lints/function_lines_of_code/function_lines_of_code_rule_test.dart index 163f0988..0b5fe1a7 100644 --- a/test/src/lints/function_lines_of_code/function_lines_of_code_rule_test.dart +++ b/test/src/lints/function_lines_of_code/function_lines_of_code_rule_test.dart @@ -4,7 +4,7 @@ import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_c import 'package:test_reflective_loader/test_reflective_loader.dart'; import '../../utils/code_generators.dart'; -import '../../utils/table_driven_rule_test.dart'; +import '../../utils/table_driven_rule_test_base.dart'; import 'models/test_case.dart'; void main() { diff --git a/test/src/utils/table_driven_rule_test.dart b/test/src/utils/table_driven_rule_test_base.dart similarity index 100% rename from test/src/utils/table_driven_rule_test.dart rename to test/src/utils/table_driven_rule_test_base.dart From f32d2ae97969fcffdfe42357b56428204cef4d13 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 25 Jun 2026 16:50:21 +0300 Subject: [PATCH 26/49] refactor: migrate no_empty_block (#299) * refactor: migrate no_empty_block * fix: allow multiple preceding comments to satisfy the todo check in no_empty_block rule * docs: add configuration examples to no_empty_block rule documentation * test: rename excluded method in no_empty_block_rule_test --------- Co-authored-by: Illia Aihistov --- lib/main.dart | 4 + .../models/no_empty_block_parameters.dart | 8 + .../no_empty_block/no_empty_block_rule.dart | 75 ++++--- .../visitors/no_empty_block_visitor.dart | 41 ++-- lint_test/no_empty_block_test.dart | 82 -------- .../no_empty_block_rule_test.dart | 198 ++++++++++++++++++ 6 files changed, 283 insertions(+), 125 deletions(-) delete mode 100644 lint_test/no_empty_block_test.dart create mode 100644 test/src/lints/no_empty_block/no_empty_block_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index d9d338a8..7974182d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,7 @@ import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexit import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart'; +import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; @@ -63,6 +64,9 @@ class SolidLintsPlugin extends Plugin { CyclomaticComplexityRule( analysisOptionsLoader: analysisLoader, ), + NoEmptyBlockRule( + analysisOptionsLoader: analysisLoader, + ), UseNearestContextRule(), preferFirstRule, // TODO: Add more lint rules and use analysisLoader diff --git a/lib/src/lints/no_empty_block/models/no_empty_block_parameters.dart b/lib/src/lints/no_empty_block/models/no_empty_block_parameters.dart index d8e143f3..3a39ad3c 100644 --- a/lib/src/lints/no_empty_block/models/no_empty_block_parameters.dart +++ b/lib/src/lints/no_empty_block/models/no_empty_block_parameters.dart @@ -17,6 +17,14 @@ class NoEmptyBlockParameters { required this.allowWithComments, }); + /// Empty [NoEmptyBlockParameters] model, excludes nothing. + factory NoEmptyBlockParameters.empty() { + return NoEmptyBlockParameters( + exclude: ExcludedIdentifiersListParameter(exclude: []), + allowWithComments: false, + ); + } + /// Method for creating from json data factory NoEmptyBlockParameters.fromJson(Map json) { return NoEmptyBlockParameters( diff --git a/lib/src/lints/no_empty_block/no_empty_block_rule.dart b/lib/src/lints/no_empty_block/no_empty_block_rule.dart index faa55d6c..db3e670f 100644 --- a/lib/src/lints/no_empty_block/no_empty_block_rule.dart +++ b/lib/src/lints/no_empty_block/no_empty_block_rule.dart @@ -1,8 +1,8 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/no_empty_block/models/no_empty_block_parameters.dart'; import 'package:solid_lints/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; // Inspired by TSLint (https://palantir.github.io/tslint/rules/no-empty/) @@ -13,6 +13,20 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// /// An empty code block often indicates missing code. /// +/// ### Example config: +/// +/// ```yaml +/// plugins: +/// solid_lints: +/// diagnostics: +/// no_empty_block: +/// allow_with_comments: true +/// exclude: +/// - method_name: build +/// - class_name: MyClass +/// method_name: build +/// ``` +/// /// ### Example /// /// #### BAD: @@ -55,40 +69,39 @@ class NoEmptyBlockRule extends SolidLintRule { /// the error whether left empty block. static const String lintName = 'no_empty_block'; - NoEmptyBlockRule._(super.config); + static const _code = LintCode( + lintName, + 'Block is empty. Empty blocks are often indicators of missing code.', + ); - /// Creates a new instance of [NoEmptyBlockRule] - /// based on the lint configuration. - factory NoEmptyBlockRule.createRule(CustomLintConfigs configs) { - final config = RuleConfig( - configs: configs, - name: lintName, - paramsParser: NoEmptyBlockParameters.fromJson, - problemMessage: (_) => - 'Block is empty. Empty blocks are often indicators of missing code.', - ); + @override + DiagnosticCode get diagnosticCode => _code; - return NoEmptyBlockRule._(config); - } + /// Creates a new instance of [NoEmptyBlockRule] + NoEmptyBlockRule({ + required super.analysisOptionsLoader, + }) : super.withParameters( + name: lintName, + description: _code.problemMessage, + parametersParser: NoEmptyBlockParameters.fromJson, + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addDeclaration((node) { - final isIgnored = config.parameters.exclude.shouldIgnore(node); - if (isIgnored) return; + super.registerNodeProcessors(registry, context); - final visitor = NoEmptyBlockVisitor( - allowWithComments: config.parameters.allowWithComments, - ); - node.accept(visitor); + final parameters = + getParametersForContext(context) ?? NoEmptyBlockParameters.empty(); + + final visitor = NoEmptyBlockVisitor( + rule: this, + allowWithComments: parameters.allowWithComments, + exclude: parameters.exclude, + ); - for (final emptyBlock in visitor.emptyBlocks) { - reporter.atNode(emptyBlock, code); - } - }); + registry.addCompilationUnit(this, visitor); } } diff --git a/lib/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart b/lib/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart index 3f1e50d3..f58ddd00 100644 --- a/lib/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart +++ b/lib/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart @@ -21,26 +21,29 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import 'package:analyzer/analysis_rule/analysis_rule.dart'; import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart'; const _todoComment = 'TODO'; /// The AST visitor that will find all empty blocks, excluding catch blocks /// and blocks containing [_todoComment] class NoEmptyBlockVisitor extends RecursiveAstVisitor { + final AnalysisRule _rule; final bool _allowWithComments; - - final _emptyBlocks = []; + final ExcludedIdentifiersListParameter _exclude; /// Constructor for [NoEmptyBlockVisitor] - /// [_allowWithComments] indicates whether to allow empty blocks that contain - /// any comments - NoEmptyBlockVisitor({required bool allowWithComments}) - : _allowWithComments = allowWithComments; - - /// All empty blocks - Iterable get emptyBlocks => _emptyBlocks; + NoEmptyBlockVisitor({ + required AnalysisRule rule, + required bool allowWithComments, + required ExcludedIdentifiersListParameter exclude, + }) : _rule = rule, + _allowWithComments = allowWithComments, + _exclude = exclude; @override void visitBlock(Block node) { @@ -51,11 +54,25 @@ class NoEmptyBlockVisitor extends RecursiveAstVisitor { if (_allowWithComments && _isPrecedingCommentAny(node)) return; if (_isPrecedingCommentToDo(node)) return; - _emptyBlocks.add(node); + final enclosingDeclaration = node.thisOrAncestorOfType(); + if (enclosingDeclaration != null && + _exclude.shouldIgnore(enclosingDeclaration)) { + return; + } + + _rule.reportAtNode(node); } - static bool _isPrecedingCommentToDo(Block node) => - node.endToken.precedingComments?.lexeme.contains(_todoComment) ?? false; + static bool _isPrecedingCommentToDo(Block node) { + Token? comment = node.endToken.precedingComments; + while (comment != null) { + if (comment.lexeme.contains(_todoComment)) { + return true; + } + comment = comment.next; + } + return false; + } static bool _isPrecedingCommentAny(Block node) => node.endToken.precedingComments != null; diff --git a/lint_test/no_empty_block_test.dart b/lint_test/no_empty_block_test.dart deleted file mode 100644 index 793dd031..00000000 --- a/lint_test/no_empty_block_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -// ignore_for_file: prefer_const_declarations, prefer_match_file_name, prefer_early_return -// ignore_for_file: unused_local_variable -// ignore_for_file: cyclomatic_complexity -// ignore_for_file: avoid_unused_parameters - -/// Check the `no_empty_block` rule - -// expect_lint: no_empty_block -void fun() {} - -void anotherFun() { - if (true) { - if (true) { - // expect_lint: no_empty_block - if (true) {} - } - } -} - -// to-do comments are allowed -void toDoStuff() { - // TODO: Implement doStuff function -} - -void catchStuff() { - // expect_lint: no_empty_block - try {} catch (e) {} - - try { - print('do stuff'); - // empty catch block is allowed - } catch (e) {} -} - -void nestedFun(void Function() fun) { - return; -} - -void doStuff() { - // expect_lint: no_empty_block - nestedFun(() {}); -} - -class A { - // expect_lint: no_empty_block - void method() {} - - void toDoMethod() { - // TODO: implement toDoMethod - } -} - -// no lint -void excludeMethod() {} - -class Exclude { - // no lint - void excludeMethod() {} -} - -// no lint -void emptyMethodWithComments() { - // comment explaining why this block is empty -} - -void anotherExample() { - // no lint - nestedFun(() { - // explain why this block is empty - }); -} - -void nestedIfElse() { - if (true) { - if (true) { - // no lint - if (true) { - // explain why this block is empty - } - } - } -} diff --git a/test/src/lints/no_empty_block/no_empty_block_rule_test.dart b/test/src/lints/no_empty_block/no_empty_block_rule_test.dart new file mode 100644 index 00000000..f71a29e1 --- /dev/null +++ b/test/src/lints/no_empty_block/no_empty_block_rule_test.dart @@ -0,0 +1,198 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(NoEmptyBlockRuleTest); + }); +} + +@reflectiveTest +class NoEmptyBlockRuleTest extends AnalysisRuleTest with AutoTestLintOffsets { + @override + void setUp() { + rule = NoEmptyBlockRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + super.setUp(); + } + + @override + String get analysisRule => NoEmptyBlockRule.lintName; + + Future test_reports_on_empty_function() async { + await assertAutoDiagnostics(''' +void fun() ${expectLint('{}')} +'''); + } + + Future test_reports_on_empty_if_block() async { + await assertAutoDiagnostics(''' +void fun() { + if (true) ${expectLint('{}')} +} +'''); + } + + Future test_does_not_report_on_catch_clause() async { + await assertNoDiagnostics(r''' +void fun() { + try { + print('hello'); + } catch (e) {} +} +'''); + } + + Future test_does_not_report_on_todo_comment() async { + await assertNoDiagnostics(r''' +// ignore_for_file: todo +void fun() { + // TODO: implement +} +'''); + } + + Future test_does_not_report_on_todo_comment_with_other_comments() async { + await assertNoDiagnostics(r''' +// ignore_for_file: todo +void fun() { + // some other comment + // TODO: implement +} +'''); + } + + Future test_does_not_report_on_any_comment_if_allowed() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + no_empty_block: + allow_with_comments: true +''', + ); + + await assertNoDiagnostics(r''' +void fun() { + // some explanation comment +} +'''); + } + + Future test_does_not_report_on_excluded() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + no_empty_block: + exclude: + - method_name: excludeMethod + - class_name: Exclude + method_name: excludeClassMethod +''', + ); + + await assertNoDiagnostics(r''' +void excludeMethod() {} + +class Exclude { + void excludeClassMethod() {} +} +'''); + } + + Future test_reports_on_empty_try_block() async { + await assertAutoDiagnostics(''' +void fun() { + try ${expectLint('{}')} catch (e) {} +} +'''); + } + + Future test_reports_on_empty_closure() async { + await assertAutoDiagnostics(''' +void nestedFun(void Function() fun) { + print('not empty'); +} + +void doStuff() { + nestedFun(() ${expectLint('{}')}); +} +'''); + } + + Future test_reports_on_empty_class_method() async { + await assertAutoDiagnostics(''' +class A { + void method() ${expectLint('{}')} +} +'''); + } + + Future test_does_not_report_on_class_method_with_todo() async { + await assertNoDiagnostics(r''' +// ignore_for_file: todo +class A { + void toDoMethod() { + // TODO: implement toDoMethod + } +} +'''); + } + + Future test_reports_on_empty_finally_block() async { + await assertAutoDiagnostics(''' +void fun() { + try { + print('try'); + } finally ${expectLint('{}')} +} +'''); + } + + Future test_reports_on_empty_constructor_body() async { + await assertAutoDiagnostics(''' +class A { + A() ${expectLint('{}')} +} +'''); + } + + Future test_reports_on_empty_for_loop_block() async { + await assertAutoDiagnostics(''' +void fun() { + for (var i = 0; i < 10; i++) ${expectLint('{}')} +} +'''); + } + + Future test_reports_on_empty_while_loop_block() async { + await assertAutoDiagnostics(''' +void fun() { + while (true) ${expectLint('{}')} +} +'''); + } + + Future test_reports_on_empty_else_block() async { + await assertAutoDiagnostics(''' +void fun(bool condition) { + if (condition) { + print('if'); + } else ${expectLint('{}')} +} +'''); + } +} From db01743aea64bb9f51ff403383e7f97b19589acb Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Thu, 25 Jun 2026 23:42:16 +0300 Subject: [PATCH 27/49] refactor: migrate prefer last (#287) * refactor: migrate prefer_last rule & visitor * refactor: migrate prefer_last test * refactor: migrate prefer_last fix * fix: handle null awareness * fix: prefer firstOrNull * feat: handle cascade & null aware expressions * refactor: use AutoTestLintOffsets * refactor: use return with switch expression --- lib/main.dart | 9 ++ .../prefer_last/fixes/prefer_last_fix.dart | 97 ++++++------- .../lints/prefer_last/prefer_last_rule.dart | 45 ++---- .../visitors/prefer_last_visitor.dart | 102 +++++++++----- lint_test/prefer_last_test.dart | 25 ---- test/prefer_last/prefer_last_rule_test.dart | 129 ++++++++++++++++++ 6 files changed, 273 insertions(+), 134 deletions(-) delete mode 100644 lint_test/prefer_last_test.dart create mode 100644 test/prefer_last/prefer_last_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index 7974182d..b62c9da2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,8 @@ import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_c import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; +import 'package:solid_lints/src/lints/prefer_last/fixes/prefer_last_fix.dart'; +import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; import 'package:solid_lints/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart'; import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart'; @@ -43,6 +45,7 @@ class SolidLintsPlugin extends Plugin { AvoidUnnecessaryTypeAssertionsRule(); final doubleLiteralFormatRule = DoubleLiteralFormatRule(); final preferFirstRule = PreferFirstRule(); + final preferLastRule = PreferLastRule(); final lintRules = [ AvoidFinalWithGetterRule(), @@ -69,6 +72,7 @@ class SolidLintsPlugin extends Plugin { ), UseNearestContextRule(), preferFirstRule, + preferLastRule, // TODO: Add more lint rules and use analysisLoader // for rules that need parameters // For example: `CyclomaticComplexityRule(analysisLoader)` @@ -101,5 +105,10 @@ class SolidLintsPlugin extends Plugin { preferFirstRule.diagnosticCode, PreferFirstFix.new, ); + + registry.registerFixForRule( + preferLastRule.diagnosticCode, + PreferLastFix.new, + ); } } diff --git a/lib/src/lints/prefer_last/fixes/prefer_last_fix.dart b/lib/src/lints/prefer_last/fixes/prefer_last_fix.dart index 95148903..105d6cc0 100644 --- a/lib/src/lints/prefer_last/fixes/prefer_last_fix.dart +++ b/lib/src/lints/prefer_last/fixes/prefer_last_fix.dart @@ -1,67 +1,70 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/diagnostic/diagnostic.dart'; -import 'package:analyzer/source/source_range.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; +import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; /// A Quick fix for `prefer_last` rule /// Suggests to replace iterable access expressions -class PreferLastFix extends DartFix { +class PreferLastFix extends ParsedCorrectionProducer { static const _replaceComment = "Replace with 'last'."; + /// Creates a new instance of [PreferLastFix] + PreferLastFix({required super.context}); + @override - void run( - CustomLintResolver resolver, - ChangeReporter reporter, - CustomLintContext context, - Diagnostic analysisError, - List others, - ) { - context.registry.addMethodInvocation((node) { - if (analysisError.sourceRange.intersects(node.sourceRange)) { - final correction = _createCorrection(node); + FixKind get fixKind => const FixKind( + 'solid_lints.fix.${PreferLastRule.lintName}', + DartFixKindPriority.standard, + _replaceComment, + ); - _addReplacement(reporter, node, correction); - } - }); + @override + FixKind get multiFixKind => const FixKind( + 'solid_lints.fix.multi.${PreferLastRule.lintName}', + DartFixKindPriority.standard, + '$_replaceComment across files', + ); - context.registry.addIndexExpression((node) { - if (analysisError.sourceRange.intersects(node.sourceRange)) { - final correction = _createCorrection(node); + @override + CorrectionApplicability get applicability => + CorrectionApplicability.automatically; - _addReplacement(reporter, node, correction); - } - }); + @override + Future compute(ChangeBuilder builder) async { + final targetNode = node.thisOrAncestorMatching( + (n) => n is MethodInvocation || n is IndexExpression, + ); + if (targetNode is! Expression) return; + + final correction = _createCorrection(targetNode); + await _addReplacement(builder, targetNode, correction); } String _createCorrection(Expression expression) { - if (expression is MethodInvocation) { - return expression.isCascaded - ? '..last' - : '${expression.target ?? ''}.last'; - } else if (expression is IndexExpression) { - return expression.isCascaded - ? '..last' - : '${expression.target ?? ''}.last'; - } else { - return '.last'; + switch (expression) { + case MethodInvocation(isCascaded: true, :final isNullAware): + case IndexExpression(isCascaded: true, :final isNullAware): + return isNullAware ? '?.last' : '..last'; + + case MethodInvocation(:final target?, :final isNullAware): + case IndexExpression(:final target?, :final isNullAware): + return isNullAware ? '$target?.last' : '$target.last'; + + default: + return '.last'; } } - void _addReplacement( - ChangeReporter reporter, - Expression node, + Future _addReplacement( + ChangeBuilder builder, + AstNode node, String correction, - ) { - final changeBuilder = reporter.createChangeBuilder( - message: _replaceComment, - priority: 1, + ) async { + await builder.addDartFileEdit( + file, + (builder) => builder.addSimpleReplacement(node.sourceRange, correction), ); - - changeBuilder.addDartFileEdit((builder) { - builder.addSimpleReplacement( - SourceRange(node.offset, node.length), - correction, - ); - }); } } diff --git a/lib/src/lints/prefer_last/prefer_last_rule.dart b/lib/src/lints/prefer_last/prefer_last_rule.dart index 7dca9d21..ff75f5ba 100644 --- a/lib/src/lints/prefer_last/prefer_last_rule.dart +++ b/lib/src/lints/prefer_last/prefer_last_rule.dart @@ -1,8 +1,7 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/lints/prefer_last/fixes/prefer_last_fix.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/prefer_last/visitors/prefer_last_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Warns about usage of `iterable[length - 1]` or @@ -31,37 +30,23 @@ class PreferLastRule extends SolidLintRule { /// access can be simplified. static const lintName = 'prefer_last'; - PreferLastRule._(super.config); + static const _code = LintCode( + lintName, + "Use last instead of accessing the last element by index.", + ); /// Creates a new instance of [PreferLastRule] - /// based on the lint configuration. - factory PreferLastRule.createRule(CustomLintConfigs configs) { - final config = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (value) => - 'Use last instead of accessing the last element by index.', - ); + PreferLastRule() : super(name: lintName, description: _code.problemMessage); - return PreferLastRule._(config); - } + @override + LintCode get diagnosticCode => _code; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addCompilationUnit((node) { - final visitor = PreferLastVisitor(); - node.accept(visitor); - - for (final element in visitor.expressions) { - reporter.atNode(element, code); - } - }); + final visitor = PreferLastVisitor(this); + registry.addCompilationUnit(this, visitor); } - - @override - List getFixes() => [PreferLastFix()]; } diff --git a/lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart b/lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart index e8b12bb2..b4ee4c43 100644 --- a/lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart +++ b/lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart @@ -1,30 +1,33 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; import 'package:solid_lints/src/utils/types_utils.dart'; /// The AST visitor that will collect all Iterable access expressions /// which can be replaced with .last class PreferLastVisitor extends RecursiveAstVisitor { - final _expressions = []; + static const _lengthGetterName = 'length'; - /// List of all Iterable access expressions - Iterable get expressions => _expressions; + final PreferLastRule _rule; + + /// Creates a new instance of [PreferLastVisitor] + PreferLastVisitor(this._rule); @override void visitMethodInvocation(MethodInvocation node) { super.visitMethodInvocation(node); final target = node.realTarget; + final isIterable = isIterableOrSubclass(target?.staticType); + final isElementAt = node.methodName.name == 'elementAt'; + + if (!isIterable || !isElementAt) return; - if (isIterableOrSubclass(target?.staticType) && - node.methodName.name == 'elementAt') { - final arg = node.argumentList.arguments.first; + final arg = node.argumentList.arguments.firstOrNull; - if (arg is BinaryExpression && - _isLastElementAccess(arg, target.toString())) { - _expressions.add(node); - } + if (arg is BinaryExpression && _isLastElementAccess(arg, target)) { + _rule.reportAtNode(node); } } @@ -34,41 +37,76 @@ class PreferLastVisitor extends RecursiveAstVisitor { final target = node.realTarget; - if (isListOrSubclass(target.staticType)) { - final index = node.index; + if (!isListOrSubclass(target.staticType)) return; - if (index is BinaryExpression && - _isLastElementAccess(index, target.toString())) { - _expressions.add(node); - } + final index = node.index; + + if (index is BinaryExpression && _isLastElementAccess(index, target)) { + _rule.reportAtNode(node); } } - bool _isLastElementAccess(BinaryExpression expression, String targetName) { - final left = expression.leftOperand; + bool _isLastElementAccess(BinaryExpression expression, Expression? target) { final right = expression.rightOperand; - final leftName = _getLeftOperandName(left); + if (right is! IntegerLiteral || right.value != 1) return false; - if (right is! IntegerLiteral) return false; - if (right.value != 1) return false; if (expression.operator.type != TokenType.MINUS) return false; + if (target == null) return false; + + return _isLengthOfTarget(expression.leftOperand, target); + } - return leftName == '$targetName.length'; + bool _isLengthOfTarget(Expression expression, Expression target) { + final lengthReceiver = switch (expression) { + PropertyAccess( + propertyName: SimpleIdentifier(name: _lengthGetterName), + operator: Token(type: TokenType.PERIOD), + :final target, + ) => + target, + PrefixedIdentifier( + prefix: final prefix, + identifier: SimpleIdentifier(name: _lengthGetterName), + ) => + prefix, + _ => null, + }; + + if (lengthReceiver == null) return false; + + return _referencesSameTarget(lengthReceiver, target); } - String? _getLeftOperandName(Expression expression) { - if (expression is PrefixedIdentifier) { - return expression.name; + bool _referencesSameTarget( + Expression lengthReceiver, + Expression accessTarget, + ) { + final normalizedLengthReceiver = _unwrapExpression(lengthReceiver); + final normalizedAccessTarget = _unwrapExpression(accessTarget); + + if (normalizedLengthReceiver is Identifier && + normalizedAccessTarget is Identifier) { + return normalizedLengthReceiver.element?.id == + normalizedAccessTarget.element?.id; } - /// Access target like map.keys.length is being reported as PropertyAccess - /// expression this case will handle such cases - if (expression is PropertyAccess) { - if (expression.operator.type != TokenType.PERIOD) return null; + return normalizedLengthReceiver.toString() == + normalizedAccessTarget.toString(); + } + + Expression _unwrapExpression(Expression expression) { + return switch (expression) { + PostfixExpression( + :final operand, + operator: Token(type: TokenType.BANG), + ) => + _unwrapExpression(operand), - return expression.toString(); - } + ParenthesizedExpression(:final expression) => _unwrapExpression( + expression, + ), - return null; + _ => expression, + }; } } diff --git a/lint_test/prefer_last_test.dart b/lint_test/prefer_last_test.dart deleted file mode 100644 index 4b87476f..00000000 --- a/lint_test/prefer_last_test.dart +++ /dev/null @@ -1,25 +0,0 @@ -/// Check the `prefer_first` rule -void fun() { - final list = [0, 1, 2, 3]; - final length = list.length - 1; - final set = {0, 1, 2, 3}; - final map = {0: 0, 1: 1, 2: 2, 3: 3}; - - // expect_lint: prefer_last - list[list.length - 1]; - - list[length - 1]; - - // expect_lint: prefer_last - list.elementAt(list.length - 1); - list.elementAt(length - 1); - - // expect_lint: prefer_last - set.elementAt(set.length - 1); - - // expect_lint: prefer_last - map.keys.elementAt(map.keys.length - 1); - - // expect_lint: prefer_last - map.values.elementAt(map.values.length - 1); -} diff --git a/test/prefer_last/prefer_last_rule_test.dart b/test/prefer_last/prefer_last_rule_test.dart new file mode 100644 index 00000000..a25747c6 --- /dev/null +++ b/test/prefer_last/prefer_last_rule_test.dart @@ -0,0 +1,129 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferLastRuleTest); + }); +} + +@reflectiveTest +class PreferLastRuleTest extends AnalysisRuleTest with AutoTestLintOffsets { + @override + void setUp() { + rule = PreferLastRule(); + super.setUp(); + } + + void test_reports_on_list_index_access_with_length_minus_one() async { + await assertAutoDiagnostics(''' +final list1 = [0, 1, 2, 3]; +var a = ${expectLint('list1[list1.length - 1]')}; + +void main () { + final list2 = [1, 0, 2, 3]; + ${expectLint('list1[list1.length - 1]')}; + ${expectLint('list2[list2.length - 1]')}; +} +'''); + } + + void + test_does_not_report_on_list_index_access_with_variable_or_constant() async { + await assertNoDiagnostics(r''' +final list = [0, 1, 2, 3]; +final length = list.length - 1; + +var a = list[length - 1]; +'''); + } + + void test_reports_on_list_subclasses() async { + await assertAutoDiagnostics(''' +abstract class MyList implements List {} + +T getLast(MyList list) { + return ${expectLint('list[list.length - 1]')}; +} +'''); + } + + void test_reports_on_element_at_access_with_length_minus_one() async { + await assertAutoDiagnostics(''' +final list1 = [0, 1, 2, 3]; +var a = ${expectLint('list1.elementAt(list1.length - 1)')}; + +void main () { + final list2 = [1, 0, 2, 3]; + ${expectLint('list1.elementAt(list1.length - 1)')}; + ${expectLint('list2.elementAt(list2.length - 1)')}; +} +'''); + } + + void + test_does_not_report_on_element_at_access_with_variable_or_constant() async { + await assertNoDiagnostics(r''' +final list = [0, 1, 2, 3]; +final length = list.length - 1; + +var a = list.elementAt(length - 1); +'''); + } + + void test_reports_on_iterable_subclasses() async { + await assertAutoDiagnostics(''' +abstract class MyIterable implements Iterable {} + +T getLast(MyIterable iterable) { + return ${expectLint('iterable.elementAt(iterable.length - 1)')}; +} + +void main () { + final set = {0, 1, 2, 3}; + final map = {0: 0, 1: 1, 2: 2, 3: 3}; + + ${expectLint('set.elementAt(set.length - 1)')}; + ${expectLint('map.keys.elementAt(map.keys.length - 1)')}; + ${expectLint('map.values.elementAt(map.values.length - 1)')}; +} +'''); + } + + void test_reports_on_cascade_element_at_access_with_length_minus_one() async { + await assertAutoDiagnostics(''' +void main () { + final list2 = [1, 0, 2, 3]; + list2${expectLint('..elementAt(list2.length - 1)')}; +} +'''); + } + + void test_reports_on_null_aware_index_access_with_length_minus_one() async { + await assertAutoDiagnostics(''' +List? list1 = [0, 1, 2, 3]; +var a = ${expectLint('list1?[list1!.length - 1]')}; + +void test(List? list2) { + ${expectLint('list1?[list1!.length - 1]')}; + ${expectLint('list2?[list2.length - 1]')}; +} +'''); + } + + void + test_reports_on_null_aware_element_at_access_with_length_minus_one() async { + await assertAutoDiagnostics(''' +List? list1 = [0, 1, 2, 3]; +var a = ${expectLint('list1?.elementAt(list1!.length - 1)')}; + +void test(List? list2) { + ${expectLint('list1?.elementAt(list1!.length - 1)')}; + ${expectLint('list2?.elementAt(list2.length - 1)')}; +} +'''); + } +} From 8b1d145a6a4d6e025c33db928c1a4deaf1176a6a Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 26 Jun 2026 09:33:57 +0300 Subject: [PATCH 28/49] refactor: migrate no_magic_number rule (#300) * refactor: migrate no_magic_number rule * fix: ignore magic numbers in annotations and update boolean configuration parsing * fix: allow negative numbers in list index expressions for no_magic_number rule * feat: ignore magic numbers within record literals and named record fields * refactor: migrate no_magic_number test cases to unit tests and remove legacy integration tests * refactor: move AST inspection logic to AstNode extension and simplify magic number validation checks * refactor: cleanup code formatting, reorder imports, and adjust indentation across linting utilities and rules --------- Co-authored-by: Illia Aihistov --- lib/main.dart | 22 +- .../avoid_non_null_assertion_rule.dart | 10 +- .../models/no_magic_number_parameters.dart | 25 +- .../no_magic_number/no_magic_number_rule.dart | 196 ++------- .../no_magic_number_rule_visitor.dart | 98 +++++ .../visitors/no_magic_number_visitor.dart | 45 -- lib/src/utils/node_utils.dart | 50 ++- .../analysis_options.yaml | 8 - ..._number_allowed_in_widget_params_test.dart | 33 -- lint_test/no_magic_number_test.dart | 149 ------- .../no_magic_number_rule_test.dart | 393 ++++++++++++++++++ 11 files changed, 601 insertions(+), 428 deletions(-) create mode 100644 lib/src/lints/no_magic_number/visitors/no_magic_number_rule_visitor.dart delete mode 100644 lib/src/lints/no_magic_number/visitors/no_magic_number_visitor.dart delete mode 100644 lint_test/no_magic_number_allowed_in_widget_params_test/analysis_options.yaml delete mode 100644 lint_test/no_magic_number_allowed_in_widget_params_test/no_magic_number_allowed_in_widget_params_test.dart delete mode 100644 lint_test/no_magic_number_test.dart create mode 100644 test/src/lints/no_magic_number/no_magic_number_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index b62c9da2..546b34b8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,7 @@ import 'package:solid_lints/src/lints/double_literal_format/double_literal_forma import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart'; import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; +import 'package:solid_lints/src/lints/no_magic_number/no_magic_number_rule.dart'; import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; import 'package:solid_lints/src/lints/prefer_last/fixes/prefer_last_fix.dart'; @@ -55,22 +56,13 @@ class SolidLintsPlugin extends Plugin { AvoidDebugPrintInReleaseRule(), doubleLiteralFormatRule, ProperSuperCallsRule(), - AvoidReturningWidgetsRule( - analysisOptionsLoader: analysisLoader, - ), - FunctionLinesOfCodeRule( - analysisOptionsLoader: analysisLoader, - ), - AvoidUnusedParametersRule( - analysisOptionsLoader: analysisLoader, - ), - CyclomaticComplexityRule( - analysisOptionsLoader: analysisLoader, - ), - NoEmptyBlockRule( - analysisOptionsLoader: analysisLoader, - ), + AvoidReturningWidgetsRule(analysisOptionsLoader: analysisLoader), + FunctionLinesOfCodeRule(analysisOptionsLoader: analysisLoader), + AvoidUnusedParametersRule(analysisOptionsLoader: analysisLoader), + CyclomaticComplexityRule(analysisOptionsLoader: analysisLoader), + NoEmptyBlockRule(analysisOptionsLoader: analysisLoader), UseNearestContextRule(), + NoMagicNumberRule(analysisOptionsLoader: analysisLoader), preferFirstRule, preferLastRule, // TODO: Add more lint rules and use analysisLoader diff --git a/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart b/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart index ba98b75a..349297cd 100644 --- a/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart +++ b/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart @@ -47,11 +47,11 @@ class AvoidNonNullAssertionRule extends AnalysisRule { /// creates an instance of [AvoidNonNullAssertionRule] AvoidNonNullAssertionRule() - : super( - name: lintName, - description: - 'Warns about usages of bang operator (!) except valid Map access.', - ); + : super( + name: lintName, + description: + 'Warns about usages of bang operator (!) except valid Map access.', + ); @override LintCode get diagnosticCode => _code; diff --git a/lib/src/lints/no_magic_number/models/no_magic_number_parameters.dart b/lib/src/lints/no_magic_number/models/no_magic_number_parameters.dart index f23ab0bc..50bdc93f 100644 --- a/lib/src/lints/no_magic_number/models/no_magic_number_parameters.dart +++ b/lib/src/lints/no_magic_number/models/no_magic_number_parameters.dart @@ -47,12 +47,23 @@ class NoMagicNumberParameters { required this.allowedInWidgetParams, }); - /// Method for creating from json data - factory NoMagicNumberParameters.fromJson(Map json) => - NoMagicNumberParameters( - allowedNumbers: - json[_allowedConfigName] as Iterable? ?? _defaultMagicNumbers, - allowedInWidgetParams: - json[_allowedInWidgetParamsConfigName] as bool? ?? false, + /// Creates an empty/default instance of [NoMagicNumberParameters] + factory NoMagicNumberParameters.empty() => const NoMagicNumberParameters( + allowedNumbers: _defaultMagicNumbers, + allowedInWidgetParams: false, ); + + /// Method for creating from json data + factory NoMagicNumberParameters.fromJson(Map json) { + final allowedRaw = json[_allowedConfigName]; + final allowedList = allowedRaw is Iterable + ? allowedRaw.whereType().toList() + : _defaultMagicNumbers; + + return NoMagicNumberParameters( + allowedNumbers: allowedList, + allowedInWidgetParams: + json[_allowedInWidgetParamsConfigName] == true, + ); + } } diff --git a/lib/src/lints/no_magic_number/no_magic_number_rule.dart b/lib/src/lints/no_magic_number/no_magic_number_rule.dart index a1355219..57e5e9f9 100644 --- a/lib/src/lints/no_magic_number/no_magic_number_rule.dart +++ b/lib/src/lints/no_magic_number/no_magic_number_rule.dart @@ -1,34 +1,8 @@ -// MIT License -// -// Copyright (c) 2020-2021 Dart Code Checker team -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/element/type.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:collection/collection.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/no_magic_number/models/no_magic_number_parameters.dart'; -import 'package:solid_lints/src/lints/no_magic_number/visitors/no_magic_number_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; +import 'package:solid_lints/src/lints/no_magic_number/visitors/no_magic_number_rule_visitor.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// A `no_magic_number` rule which forbids having numbers without variable @@ -43,11 +17,12 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// ### Example config: /// /// ```yaml -/// custom_lint: -/// rules: -/// - no_magic_number: -/// allowed: [12, 42] -/// allowed_in_widget_params: true +/// plugins: +/// solid_lints: +/// diagnostics: +/// no_magic_number: +/// allowed: [12, 42] +/// allowed_in_widget_params: true /// ``` /// /// ### Example @@ -88,7 +63,7 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// const Circle({required this.r}); /// } /// const Circle(r: 5); -/// const circle = Circle(r: 10) +/// const circle = Circle(r: 10); /// ``` /// /// ### Allowed @@ -138,139 +113,36 @@ class NoMagicNumberRule extends SolidLintRule { /// the error when having magic number. static const String lintName = 'no_magic_number'; - NoMagicNumberRule._(super.config); - - /// Creates a new instance of [NoMagicNumberRule] - /// based on the lint configuration. - factory NoMagicNumberRule.createRule(CustomLintConfigs configs) { - final config = RuleConfig( - configs: configs, - name: lintName, - paramsParser: NoMagicNumberParameters.fromJson, - problemMessage: (_) => 'Avoid using magic numbers.' - 'Extract them to named constants or variables.', - ); - - return NoMagicNumberRule._(config); - } + static const _code = LintCode( + lintName, + 'Avoid using magic numbers. Extract them to named constants or variables.', + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, - ) { - context.registry.addCompilationUnit((node) { - final visitor = NoMagicNumberVisitor(); - node.accept(visitor); - - final magicNumbers = visitor.literals - .where(_isMagicNumber) - .where(_isNotInsideVariable) - .where(_isNotInsideCollectionLiteral) - .where(_isNotInsideConstMap) - .where(_isNotInsideConstConstructor) - .where(_isNotInDateTime) - .where(_isNotInsideIndexExpression) - .where(_isNotInsideEnumConstantArguments) - .where(_isNotDefaultValue) - .where(_isNotInConstructorInitializer) - .where(_isNotWidgetParameter); - - for (final magicNumber in magicNumbers) { - reporter.atNode(magicNumber, code); - } - }); - } - - bool _isMagicNumber(Literal l) => - (l is DoubleLiteral && - !config.parameters.allowedNumbers.contains(l.value)) || - (l is IntegerLiteral && - !config.parameters.allowedNumbers.contains(l.value)); - - bool _isNotInsideVariable(Literal l) { - // Whether we encountered such node, - // This is tracked because [InstanceCreationExpression] can be - // inside [VariableDeclaration] removing unwanted literals - - bool isInstanceCreationExpression = false; - return l.thisOrAncestorMatching((ancestor) { - if (ancestor is InstanceCreationExpression) { - isInstanceCreationExpression = true; - } - if (isInstanceCreationExpression) { - return false; - } else { - return ancestor is VariableDeclaration; - } - }) == - null; - } - - bool _isNotInDateTime(Literal l) => - l.thisOrAncestorMatching( - (a) => - a is InstanceCreationExpression && - a.staticType?.getDisplayString() == 'DateTime', - ) == - null; - - bool _isNotInsideEnumConstantArguments(Literal l) { - final node = l.thisOrAncestorMatching( - (ancestor) => ancestor is EnumConstantArguments, - ); - - return node == null; - } + DiagnosticCode get diagnosticCode => _code; - bool _isNotInsideCollectionLiteral(Literal l) => l.parent is! TypedLiteral; - - bool _isNotInsideConstMap(Literal l) { - final grandParent = l.parent?.parent; - - return !(grandParent is SetOrMapLiteral && grandParent.isConst); - } - - bool _isNotInsideConstConstructor(Literal l) => - l.thisOrAncestorMatching((ancestor) { - return ancestor is InstanceCreationExpression && ancestor.isConst; - }) == - null; - - bool _isNotInsideIndexExpression(Literal l) => l.parent is! IndexExpression; - - bool _isNotDefaultValue(Literal literal) { - return literal.thisOrAncestorOfType() == null; - } - - bool _isNotInConstructorInitializer(Literal literal) { - return literal.thisOrAncestorOfType() == null; - } - - bool _isNotWidgetParameter(Literal literal) { - if (!config.parameters.allowedInWidgetParams) return true; - - final widgetCreationExpression = literal.thisOrAncestorMatching( - _isWidgetCreationExpression, - ); - - return widgetCreationExpression == null; - } + /// Creates a new instance of [NoMagicNumberRule] + NoMagicNumberRule({ + required super.analysisOptionsLoader, + }) : super.withParameters( + name: lintName, + description: 'Forbids having numbers without variable.', + parametersParser: NoMagicNumberParameters.fromJson, + ); - bool _isWidgetCreationExpression( - AstNode node, + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - if (node is! InstanceCreationExpression) return false; - - final staticType = node.staticType; + super.registerNodeProcessors(registry, context); - if (staticType is! InterfaceType) return false; + final parameters = + getParametersForContext(context) ?? NoMagicNumberParameters.empty(); - final widgetSupertype = staticType.allSupertypes.firstWhereOrNull( - (supertype) => supertype.getDisplayString() == 'Widget', - ); + final visitor = NoMagicNumberRuleVisitor(this, parameters); - return widgetSupertype != null; + registry.addDoubleLiteral(this, visitor); + registry.addIntegerLiteral(this, visitor); } } diff --git a/lib/src/lints/no_magic_number/visitors/no_magic_number_rule_visitor.dart b/lib/src/lints/no_magic_number/visitors/no_magic_number_rule_visitor.dart new file mode 100644 index 00000000..efd6e13b --- /dev/null +++ b/lib/src/lints/no_magic_number/visitors/no_magic_number_rule_visitor.dart @@ -0,0 +1,98 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; +import 'package:solid_lints/src/lints/no_magic_number/models/no_magic_number_parameters.dart'; +import 'package:solid_lints/src/models/solid_lint_rule.dart'; +import 'package:solid_lints/src/utils/node_utils.dart'; + +/// The AST visitor that checks if double and integer literals are magic numbers. +class NoMagicNumberRuleVisitor extends SimpleAstVisitor { + final SolidLintRule _rule; + final NoMagicNumberParameters _parameters; + + /// Creates a new instance of [NoMagicNumberRuleVisitor]. + NoMagicNumberRuleVisitor(this._rule, this._parameters); + + @override + void visitDoubleLiteral(DoubleLiteral node) { + _checkLiteral(node); + } + + @override + void visitIntegerLiteral(IntegerLiteral node) { + _checkLiteral(node); + } + + void _checkLiteral(Literal node) { + if (_isNotMagicNumber(node)) return; + if (_isInsideVariable(node)) return; + if (_isInsideCollectionLiteral(node)) return; + if (_isWidgetParameter(node)) return; + + if (node.isInsideConstConstructor) return; + if (node.isInDateTime) return; + if (node.isInsideIndexExpression) return; + if (node.isInsideEnumConstantArguments) return; + if (node.isDefaultValue) return; + if (node.isInConstructorInitializer) return; + + _rule.reportAtNode(node); + } + + bool _isNotMagicNumber(Literal l) => + (l is DoubleLiteral && _parameters.allowedNumbers.contains(l.value)) || + (l is IntegerLiteral && _parameters.allowedNumbers.contains(l.value)); + + /// Returns the effective parent of [l], skipping over a unary + /// [PrefixExpression] (e.g. the `-` in `-42`). + AstNode? _effectiveParent(Literal l) => + l.parent is PrefixExpression ? l.parent?.parent : l.parent; + + /// Returns `true` if the literal is the direct initializer of a variable + /// declaration. + /// + /// Allows `var x = 42` (literal gets a name) but reports `var r = x + 42` + /// (42 is a magic number inside an expression). + bool _isInsideVariable(Literal l) => + _effectiveParent(l) is VariableDeclaration; + + /// Returns `true` if the literal is inside a collection literal. + /// + /// Covers list elements (`[42]`, `[-42]`), set elements (`{42}`, `{-42}`), + /// and map keys/values (`{42: 'a'}`, `{-42: 'a'}`, `{'a': -42}`). + /// + /// Unary prefix expressions (e.g. `-42`) are skipped transparently so that + /// the effective parent—the collection literal—is checked instead of the + /// intermediate [PrefixExpression] node. + bool _isInsideCollectionLiteral(Literal l) { + final p = _effectiveParent(l); + return p is TypedLiteral || + p is MapLiteralEntry || + p is RecordLiteral || + (p is NamedExpression && p.parent is RecordLiteral); + } + + bool _isWidgetParameter(Literal literal) { + if (!_parameters.allowedInWidgetParams) return false; + + final widgetCreationExpression = literal.thisOrAncestorMatching( + _isWidgetCreationExpression, + ); + + return widgetCreationExpression != null; + } + + bool _isWidgetCreationExpression(AstNode node) { + if (node is! InstanceCreationExpression) return false; + + final staticType = node.staticType; + if (staticType is! InterfaceType) return false; + + final widgetSupertype = staticType.allSupertypes.firstWhereOrNull( + (supertype) => supertype.getDisplayString() == 'Widget', + ); + + return widgetSupertype != null; + } +} diff --git a/lib/src/lints/no_magic_number/visitors/no_magic_number_visitor.dart b/lib/src/lints/no_magic_number/visitors/no_magic_number_visitor.dart deleted file mode 100644 index 7bec43f6..00000000 --- a/lib/src/lints/no_magic_number/visitors/no_magic_number_visitor.dart +++ /dev/null @@ -1,45 +0,0 @@ -// MIT License -// -// Copyright (c) 2020-2021 Dart Code Checker team -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/ast/visitor.dart'; - -/// The AST visitor that will find all double and integer literals -class NoMagicNumberVisitor extends RecursiveAstVisitor { - final _literals = []; - - /// List of all double and integer literals - Iterable get literals => _literals; - - @override - void visitDoubleLiteral(DoubleLiteral node) { - _literals.add(node); - super.visitDoubleLiteral(node); - } - - @override - void visitIntegerLiteral(IntegerLiteral node) { - _literals.add(node); - super.visitIntegerLiteral(node); - } -} diff --git a/lib/src/utils/node_utils.dart b/lib/src/utils/node_utils.dart index 2b008879..7afb28a1 100644 --- a/lib/src/utils/node_utils.dart +++ b/lib/src/utils/node_utils.dart @@ -2,12 +2,10 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/element/element.dart'; - /// Check node is override method from its metadata bool isOverride(List metadata) => metadata.any( - (node) => - node.name.name == 'override' && node.atSign.type == TokenType.AT, - ); + (node) => node.name.name == 'override' && node.atSign.type == TokenType.AT, +); /// Returns human readable node type /// Self explanatory @@ -61,3 +59,47 @@ extension SimpleIdentifierExtension on SimpleIdentifier { return declOffset >= body.offset && declOffset < body.end; } } + +/// Extension on [AstNode] to provide generic context/traversal checks. +extension AstNodeExtension on AstNode { + /// Returns `true` if the node is within the default value of a formal + /// parameter. + bool get isDefaultValue => + thisOrAncestorOfType() != null; + + /// Returns `true` if the node is within a constructor initializer. + bool get isInConstructorInitializer => + thisOrAncestorOfType() != null; + + /// Returns `true` if the node is within a const constructor invocation or + /// annotation. + bool get isInsideConstConstructor => + thisOrAncestorMatching((ancestor) { + return (ancestor is InstanceCreationExpression && ancestor.isConst) || + ancestor is Annotation; + }) != + null; + + /// Returns `true` if the node is within enum constant arguments. + bool get isInsideEnumConstantArguments => + thisOrAncestorMatching( + (ancestor) => ancestor is EnumConstantArguments, + ) != + null; + + /// Returns `true` if the node is within a DateTime constructor invocation. + bool get isInDateTime => + thisOrAncestorMatching( + (a) => + a is InstanceCreationExpression && + a.staticType?.getDisplayString() == 'DateTime', + ) != + null; + + /// Returns `true` if the node is the child (optionally wrapped in a prefix + /// expression) of an index expression (e.g. `list[42]` or `list[-42]`). + bool get isInsideIndexExpression { + final p = parent is PrefixExpression ? parent?.parent : parent; + return p is IndexExpression; + } +} diff --git a/lint_test/no_magic_number_allowed_in_widget_params_test/analysis_options.yaml b/lint_test/no_magic_number_allowed_in_widget_params_test/analysis_options.yaml deleted file mode 100644 index 8c1644ac..00000000 --- a/lint_test/no_magic_number_allowed_in_widget_params_test/analysis_options.yaml +++ /dev/null @@ -1,8 +0,0 @@ -analyzer: - plugins: - - ../custom_lint - -custom_lint: - rules: - - no_magic_number: - allowed_in_widget_params: true diff --git a/lint_test/no_magic_number_allowed_in_widget_params_test/no_magic_number_allowed_in_widget_params_test.dart b/lint_test/no_magic_number_allowed_in_widget_params_test/no_magic_number_allowed_in_widget_params_test.dart deleted file mode 100644 index b9f21f1a..00000000 --- a/lint_test/no_magic_number_allowed_in_widget_params_test/no_magic_number_allowed_in_widget_params_test.dart +++ /dev/null @@ -1,33 +0,0 @@ -// ignore_for_file: avoid_returning_widgets -// ignore_for_file: prefer_match_file_name - -// Allowed for numbers in a Widget subtype parameters. -abstract interface class Widget {} - -class StatelessWidget implements Widget {} - -class MyWidget extends StatelessWidget { - final MyWidgetDecoration decoration; - final int value; - - MyWidget({ - required this.decoration, - required this.value, - }); -} - -class MyWidgetDecoration { - final int size; - - MyWidgetDecoration({required this.size}); -} - -Widget build() { - return MyWidget( - /// allowed_in_widget_params: true - decoration: MyWidgetDecoration(size: 12), - - /// allowed_in_widget_params: true - value: 23, - ); -} diff --git a/lint_test/no_magic_number_test.dart b/lint_test/no_magic_number_test.dart deleted file mode 100644 index 4bdc69e2..00000000 --- a/lint_test/no_magic_number_test.dart +++ /dev/null @@ -1,149 +0,0 @@ -// ignore_for_file: unused_local_variable, avoid_returning_widgets -// ignore_for_file: prefer_match_file_name -// ignore_for_file: avoid_unused_parameters -// ignore_for_file: no_empty_block - -/// Check the `no_magic_number` rule - -const pi = 3.14; -const radiusToDiameterCoefficient = 2; - -// expect_lint: no_magic_number -double circumference(double radius) => 2 * 3.14 * radius; - -double correctCircumference(double radius) => - radiusToDiameterCoefficient * pi * radius; - -bool canDrive(int age, {bool isUSA = false}) { - // expect_lint: no_magic_number - return isUSA ? age >= 16 : age > 18; -} - -const usaDrivingAge = 16; -const worldWideDrivingAge = 18; - -bool correctCanDrive(int age, {bool isUSA = false}) { - return isUSA ? age >= usaDrivingAge : age > worldWideDrivingAge; -} - -class ConstClass { - final int a; - - const ConstClass(this.a); -} - -enum ConstEnum { - // Allowed in enum arguments - one(1), - two(2); - - final int value; - - const ConstEnum(this.value); -} - -void fun() { - // Allowed in const constructors - const classInstance = ConstClass(1); - - // Allowed in list literals - final list = [1, 2, 3]; - - // Allowed int map literals - final map = {1: 'One', 2: 'Two'}; - - // Allowed in indexed expression - final result = list[1]; - - // Allowed in DateTime because it doesn't have const constructor - final apocalypse = DateTime(2012, 12, 21); -} - -// Allowed for defaults in constructors and methods. -class DefaultValues { - final int value; - - DefaultValues.named({ - this.value = 2, - }); - - DefaultValues.positional([ - this.value = 3, - ]); - - void methodWithNamedParam({int value = 4}) {} - - void methodWithPositionalParam([int value = 5]) {} -} - -void topLevelFunctionWithDefaultParam({int value = 6}) { - ({int value = 7}) {}; -} - -// Allowed for numbers in constructor initializer. -class ConstructorInitializer { - final int value; - - ConstructorInitializer() : value = 10; -} - -abstract interface class Widget {} - -class StatelessWidget implements Widget {} - -class MyWidget extends StatelessWidget { - final MyWidgetDecoration decoration; - final int value; - - MyWidget({ - required this.decoration, - required this.value, - }); -} - -class MyWidgetDecoration { - final int size; - - MyWidgetDecoration({required this.size}); -} - -Widget build() { - return MyWidget( - // expect_lint: no_magic_number - decoration: MyWidgetDecoration(size: 12), - - // expect_lint: no_magic_number - value: 23, - ); -} - -class TestOperation { - final double res; - - const TestOperation({required this.res}); -} - -const n = 15; -const m = 20; - -void checkOperationInConstructor() { - // expect_lint: no_magic_number - TestOperation(res: (5 / 5) * 20); - - // expect_lint: no_magic_number - var variable = TestOperation(res: (n / m) * 20); - - // expect_lint: no_magic_number - final finalVar = TestOperation(res: (10 / m + 4 + n)); - - // Allowed for constant expressions - const constVar = TestOperation(res: (10 / m + 4 + n)); - - var constVar2 = const TestOperation(res: 15 + (10 / n)); - - final l = [ - // expect_lint: no_magic_number - TestOperation(res: 8), - const TestOperation(res: 9), - ]; -} diff --git a/test/src/lints/no_magic_number/no_magic_number_rule_test.dart b/test/src/lints/no_magic_number/no_magic_number_rule_test.dart new file mode 100644 index 00000000..59df9c7f --- /dev/null +++ b/test/src/lints/no_magic_number/no_magic_number_rule_test.dart @@ -0,0 +1,393 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/no_magic_number/no_magic_number_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(NoMagicNumberRuleTest); + }); +} + +@reflectiveTest +class NoMagicNumberRuleTest extends AnalysisRuleTest with AutoTestLintOffsets { + static const _importFlutterWidgets = "import 'package:flutter/widgets.dart';"; + static const _mockFlutterWidgetsContent = ''' +abstract class Widget { + const Widget(); +} + +class SizedBox extends Widget { + final double? width; + final Widget? child; + const SizedBox({this.width, this.child}); +} +'''; + + @override + void setUp() { + rule = NoMagicNumberRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + newPackage('flutter') + ..addFile('lib/widgets.dart', _mockFlutterWidgetsContent); + super.setUp(); + } + + Future test_reports_magic_number_in_expression() async { + await assertAutoDiagnostics(''' +int fn(int b) { + return b + ${expectLint('42')}; +} +'''); + } + + Future + test_reports_magic_numbers_in_ternary_comparison() async { + await assertAutoDiagnostics(''' +bool canDrive(int age, {bool isUSA = false}) { + return isUSA ? age >= ${expectLint('16')} : age > ${expectLint('18')}; +} +'''); + } + + Future test_does_not_report_allowed_numbers() async { + await assertNoDiagnostics(r''' +void fn() { + var a = 0; + var b = 1; + var c = -1; +} +'''); + } + + Future test_does_not_report_consts() async { + await assertNoDiagnostics(r''' +const pi = 3.14; +void fn() { + var a = pi; +} +'''); + } + + Future test_does_not_report_in_collections() async { + await assertNoDiagnostics(r''' +void fn() { + var list = [1, 2, 3]; + var map = {1: 'a', 2: 'b'}; + var set = {1, 2, 3}; +} +'''); + } + + Future test_does_not_report_negative_numbers_in_list() async { + await assertNoDiagnostics(r''' +void fn() { + var list = [1, -42, 3]; +} +'''); + } + + Future test_does_not_report_negative_numbers_in_set() async { + await assertNoDiagnostics(r''' +void fn() { + var set = {-42, -99}; +} +'''); + } + + Future test_does_not_report_negative_numbers_in_map() async { + await assertNoDiagnostics(r''' +void fn() { + var map = {-42: 'a', 'b': -99}; +} +'''); + } + + Future test_reports_widget_params_when_flag_false() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + analysisOptionsContent(rules: [rule.name]), + ); + + await assertAutoDiagnostics(''' +$_importFlutterWidgets + +Widget build() { + return SizedBox(width: ${expectLint('42')}); +} +'''); + } + + Future test_does_not_report_widget_params_when_flag_true() async { + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + no_magic_number: + allowed_in_widget_params: true +'''); + + await assertNoDiagnostics(''' +$_importFlutterWidgets + +Widget build() { + return SizedBox(width: 42); +} +'''); + } + + Future test_does_not_report_in_datetime() async { + await assertNoDiagnostics(r''' +class DateTime { + const DateTime(int year, int month, int day); +} +void fn() { + final apocalypse = DateTime(2012, 12, 21); +} +'''); + } + + Future test_does_not_report_in_enum_constant_arguments() async { + await assertNoDiagnostics(r''' +enum ConstEnum { + one(1), + two(2); + + final int value; + const ConstEnum(this.value); +} +'''); + } + + Future test_does_not_report_default_formal_parameter_values() async { + await assertNoDiagnostics(r''' +class DefaultValues { + final int value; + DefaultValues.named({this.value = 2}); + DefaultValues.positional([this.value = 3]); + void methodWithNamedParam({int value = 4}) {} + void methodWithPositionalParam([int value = 5]) {} +} +void topLevelFunctionWithDefaultParam({int value = 6}) {} +'''); + } + + Future test_does_not_report_in_constructor_initializers() async { + await assertNoDiagnostics(r''' +class ConstructorInitializer { + final int value; + ConstructorInitializer() : value = 10; +} +'''); + } + + Future test_reports_expression_in_constructors_when_not_const() async { + await assertAutoDiagnostics(''' +class TestOperation { + final double res; + const TestOperation({required this.res}); +} +void fn() { + TestOperation(res: (${expectLint('5')} / ${expectLint('5')}) * ${expectLint('20')}); +} +'''); + } + + Future + test_does_not_report_expression_in_constructors_when_const() async { + await assertNoDiagnostics(r''' +class TestOperation { + final double res; + const TestOperation({required this.res}); +} +void fn() { + const TestOperation(res: (10 / 2)); +} +'''); + } + + Future test_does_not_report_variable_declarations() async { + await assertNoDiagnostics(r''' +void fn() { + var x = 42; + final y = 3.14; +} +'''); + } + + Future test_reports_magic_number_in_variable_expression() async { + await assertAutoDiagnostics(''' +double circumference(double radius) { + var result = ${expectLint('2')} * ${expectLint('3.14')} * radius; + return result; +} +'''); + } + + Future + test_reports_magic_number_in_binary_expression_in_variable() async { + await assertAutoDiagnostics(''' +void fn(int x) { + var result = x + ${expectLint('42')}; +} +'''); + } + + Future test_does_not_report_index_expressions() async { + await assertNoDiagnostics(r''' +void fn(List list) { + var x = list[42]; + var y = list[-42]; +} +'''); + } + + Future test_does_not_report_map_literal_keys_and_values() async { + await assertNoDiagnostics(r''' +void fn() { + print({42: 3.14}); +} +'''); + } + + Future test_does_not_report_const_map_keys_and_values() async { + await assertNoDiagnostics(r''' +void fn() { + const map = {42: 3.14}; +} +'''); + } + + Future test_does_not_report_custom_allowed_numbers() async { + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + no_magic_number: + allowed: [42, 100] +'''); + + await assertNoDiagnostics(r''' +void fn() { + print(42); + print(100); +} +'''); + } + + Future test_does_not_report_in_annotations() async { + await assertNoDiagnostics(r''' +class Timeout { + final Duration duration; + const Timeout(this.duration); +} +class Duration { + final int seconds; + const Duration({required this.seconds}); +} + +@Timeout(Duration(seconds: 30)) +void fn() {} +'''); + } + + Future test_does_not_report_in_records() async { + await assertNoDiagnostics(r''' +void fn() { + var point = (10, 20); + var named = (x: 100, y: 200); +} +'''); + } + + Future + test_does_not_report_anonymous_function_default_param() async { + await assertNoDiagnostics(r''' +void fn() { + ({int value = 7}) {}; +} +'''); + } + + Future + test_reports_magic_number_mixed_with_consts_in_constructor() async { + await assertAutoDiagnostics(''' +class TestOperation { + final double res; + const TestOperation({required this.res}); +} +const n = 15; +const m = 20; +void fn() { + TestOperation(res: (n / m) * ${expectLint('20')}); +} +'''); + } + + Future + test_reports_multiple_magic_numbers_in_constructor_expression() async { + await assertAutoDiagnostics(''' +class TestOperation { + final double res; + const TestOperation({required this.res}); +} +const m = 20; +const n = 15; +void fn() { + final v = TestOperation(res: (${expectLint('10')} / m + ${expectLint('4')} + n)); +} +'''); + } + + Future + test_does_not_report_var_with_const_constructor_call() async { + await assertNoDiagnostics(r''' +class TestOperation { + final double res; + const TestOperation({required this.res}); +} +const n = 15; +void fn() { + var v = const TestOperation(res: 15 + (10 / n)); +} +'''); + } + + Future + test_reports_magic_number_in_constructor_inside_list() async { + await assertAutoDiagnostics(''' +class TestOperation { + final double res; + const TestOperation({required this.res}); +} +void fn() { + final l = [ + TestOperation(res: ${expectLint('8')}), + ]; +} +'''); + } + + Future + test_does_not_report_const_constructor_inside_list() async { + await assertNoDiagnostics(r''' +class TestOperation { + final double res; + const TestOperation({required this.res}); +} +void fn() { + final l = [ + const TestOperation(res: 9), + ]; +} +'''); + } +} From 16e3e137cfe897976b4bb65263ee98364c741f6d Mon Sep 17 00:00:00 2001 From: Eslam Shaaban <159810907+Islam-Shaaban-Ibrahim@users.noreply.github.com> Date: Fri, 26 Jun 2026 18:59:53 +0300 Subject: [PATCH 29/49] Migrate avoid_late_keyword rule (default behavior) and tests (#231) * Migrate avoid_late_keyword rule (default behavior) and tests * feat: read parameter by extending SolidLintRule fix: add old behavior test: migrate avoid late keyword rule tests * refactor: use AutoTestLintOffsets * style: inline methods --------- Co-authored-by: Andrew Bekhiet --- .../avoid_late_keyword_rule.dart | 80 ++--- .../visitors/avoid_late_keyword_visitor.dart | 33 ++ .../allow_initialized/analysis_options.yaml | 10 - ...d_late_keyword_allow_initialized_test.dart | 84 ----- .../no_generics/analysis_options.yaml | 10 - .../avoid_late_keyword_no_generics_test.dart | 52 ---- .../with_generics/analysis_options.yaml | 14 - ...avoid_late_keyword_with_generics_test.dart | 114 ------- test/avoid_late_keyword_rule_test.dart | 288 ++++++++++++++++++ 9 files changed, 348 insertions(+), 337 deletions(-) create mode 100644 lib/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart delete mode 100644 lint_test/avoid_late_keyword/allow_initialized/analysis_options.yaml delete mode 100644 lint_test/avoid_late_keyword/allow_initialized/avoid_late_keyword_allow_initialized_test.dart delete mode 100644 lint_test/avoid_late_keyword/no_generics/analysis_options.yaml delete mode 100644 lint_test/avoid_late_keyword/no_generics/avoid_late_keyword_no_generics_test.dart delete mode 100644 lint_test/avoid_late_keyword/with_generics/analysis_options.yaml delete mode 100644 lint_test/avoid_late_keyword/with_generics/avoid_late_keyword_with_generics_test.dart create mode 100644 test/avoid_late_keyword_rule_test.dart diff --git a/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart b/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart index 5110bbf7..0b288a0d 100644 --- a/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart +++ b/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart @@ -1,10 +1,9 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; +import 'package:solid_lints/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; -import 'package:solid_lints/src/utils/types_utils.dart'; /// Avoid `late` keyword /// @@ -48,62 +47,37 @@ import 'package:solid_lints/src/utils/types_utils.dart'; /// } /// ``` class AvoidLateKeywordRule extends SolidLintRule { - /// This lint rule represents - /// the error whether we use `late` keyword. - static const lintName = 'avoid_late_keyword'; + static const String _lintName = 'avoid_late_keyword'; - AvoidLateKeywordRule._(super.config); + static const LintCode _code = LintCode( + _lintName, + 'Avoid using the "late" keyword. It may result in runtime exceptions.', + ); - /// Creates a new instance of [AvoidLateKeywordRule] - /// based on the lint configuration. - factory AvoidLateKeywordRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - paramsParser: AvoidLateKeywordParameters.fromJson, - problemMessage: (_) => 'Avoid using the "late" keyword. ' - 'It may result in runtime exceptions.', - ); + /// Creates an instance of [AvoidLateKeywordRule]. + AvoidLateKeywordRule({required super.analysisOptionsLoader}) + : super.withParameters( + name: _lintName, + description: 'Warns against using the late keyword.', + parametersParser: AvoidLateKeywordParameters.fromJson, + ); - return AvoidLateKeywordRule._(rule); - } + @override + LintCode get diagnosticCode => _code; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addVariableDeclaration((node) { - if (_shouldLint(node)) { - reporter.atNode(node, code); - } - }); - } - - bool _shouldLint(VariableDeclaration node) { - final isLateDeclaration = node.isLate; - if (!isLateDeclaration) return false; - - final hasIgnoredType = _hasIgnoredType(node); - if (hasIgnoredType) return false; - - final allowInitialized = config.parameters.allowInitialized; - if (!allowInitialized) return true; - - final hasInitializer = node.initializer != null; - return !hasInitializer; - } - - bool _hasIgnoredType(VariableDeclaration node) { - final ignoredTypes = config.parameters.ignoredTypes.toSet(); - if (ignoredTypes.isEmpty) return false; + final parameters = + getParametersForContext(context) ?? const AvoidLateKeywordParameters(); - final variableType = node.declaredFragment?.element.type; - if (variableType == null) return false; + final visitor = AvoidLateKeywordVisitor(this, parameters); - return variableType.hasIgnoredType( - ignoredTypes: ignoredTypes, + registry.addVariableDeclaration( + this, + visitor, ); } } diff --git a/lib/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart b/lib/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart new file mode 100644 index 00000000..a5511622 --- /dev/null +++ b/lib/src/lints/avoid_late_keyword/visitors/avoid_late_keyword_visitor.dart @@ -0,0 +1,33 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart'; +import 'package:solid_lints/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; + +/// Visitor for [AvoidLateKeywordRule]. +class AvoidLateKeywordVisitor extends SimpleAstVisitor { + final AvoidLateKeywordRule _rule; + + final AvoidLateKeywordParameters _parameters; + + /// Creates an instance of [AvoidLateKeywordVisitor]. + AvoidLateKeywordVisitor(this._rule, this._parameters); + + @override + void visitVariableDeclaration(VariableDeclaration node) { + if (!_shouldReport(node)) return; + + _rule.reportAtNode(node); + } + + bool _shouldReport(VariableDeclaration node) => + node.isLate && + !_hasIgnoredType(node) && + !(_parameters.allowInitialized && node.initializer != null); + + bool _hasIgnoredType(VariableDeclaration node) => + node.declaredFragment?.element.type.hasIgnoredType( + ignoredTypes: _parameters.ignoredTypes.toSet(), + ) ?? + false; +} diff --git a/lint_test/avoid_late_keyword/allow_initialized/analysis_options.yaml b/lint_test/avoid_late_keyword/allow_initialized/analysis_options.yaml deleted file mode 100644 index 7f92efe8..00000000 --- a/lint_test/avoid_late_keyword/allow_initialized/analysis_options.yaml +++ /dev/null @@ -1,10 +0,0 @@ -analyzer: - plugins: - - ../custom_lint - -custom_lint: - rules: - - avoid_late_keyword: - allow_initialized: false - ignored_types: - - Animation diff --git a/lint_test/avoid_late_keyword/allow_initialized/avoid_late_keyword_allow_initialized_test.dart b/lint_test/avoid_late_keyword/allow_initialized/avoid_late_keyword_allow_initialized_test.dart deleted file mode 100644 index 05af24d1..00000000 --- a/lint_test/avoid_late_keyword/allow_initialized/avoid_late_keyword_allow_initialized_test.dart +++ /dev/null @@ -1,84 +0,0 @@ -// ignore_for_file: prefer_const_declarations, unused_local_variable, prefer_match_file_name -// ignore_for_file: avoid_global_state - -abstract class Animation {} - -class AnimationController implements Animation {} - -class SubAnimationController extends AnimationController {} - -class ColorTween {} - -/// Check "late" keyword fail -/// -/// `avoid_late_keyword` -/// allow_initialized option disabled -class AvoidLateKeyword { - /// ignored_types: Animation - late final Animation animation1; - - /// ignored_types: Animation - late final animation2 = AnimationController(); - - /// ignored_types: Animation - late final animation3 = SubAnimationController(); - - /// expect_lint: avoid_late_keyword - late final ColorTween colorTween1; - - /// expect_lint: avoid_late_keyword - late final colorTween2 = ColorTween(); - - /// expect_lint: avoid_late_keyword - late final colorTween3 = colorTween2; - - /// ignored_types: Animation - late final AnimationController controller1; - - /// expect_lint: avoid_late_keyword - late final field1 = 'string'; - - /// expect_lint: avoid_late_keyword - late final String field2; - - /// expect_lint: avoid_late_keyword - late final String field3 = 'string'; - - /// expect_lint: avoid_late_keyword - late final field4; - - void test() { - /// ignored_types: Animation - late final Animation animation1; - - /// ignored_types: Animation - late final animation2 = AnimationController(); - - /// ignored_types: Animation - late final animation3 = SubAnimationController(); - - /// expect_lint: avoid_late_keyword - late final ColorTween colorTween1; - - /// expect_lint: avoid_late_keyword - late final colorTween2 = ColorTween(); - - /// expect_lint: avoid_late_keyword - late final colorTween3 = colorTween2; - - /// ignored_types: Animation - late final AnimationController controller1; - - /// expect_lint: avoid_late_keyword - late final local1 = 'string'; - - /// expect_lint: avoid_late_keyword - late final String local2; - - /// expect_lint: avoid_late_keyword - late final String local4 = 'string'; - - /// expect_lint: avoid_late_keyword - late final local3; - } -} diff --git a/lint_test/avoid_late_keyword/no_generics/analysis_options.yaml b/lint_test/avoid_late_keyword/no_generics/analysis_options.yaml deleted file mode 100644 index da6add2f..00000000 --- a/lint_test/avoid_late_keyword/no_generics/analysis_options.yaml +++ /dev/null @@ -1,10 +0,0 @@ -analyzer: - plugins: - - ../custom_lint - -custom_lint: - rules: - - avoid_late_keyword: - allow_initialized: false - ignored_types: - - Subscription diff --git a/lint_test/avoid_late_keyword/no_generics/avoid_late_keyword_no_generics_test.dart b/lint_test/avoid_late_keyword/no_generics/avoid_late_keyword_no_generics_test.dart deleted file mode 100644 index 7c166749..00000000 --- a/lint_test/avoid_late_keyword/no_generics/avoid_late_keyword_no_generics_test.dart +++ /dev/null @@ -1,52 +0,0 @@ -// ignore_for_file: prefer_const_declarations, unused_local_variable, prefer_match_file_name -// ignore_for_file: avoid_global_state - -class Subscription {} - -class ConcreteTypeWithNoGenerics {} - -class NotAllowed {} - -/// Check "late" keyword fail -/// -/// `avoid_late_keyword` -/// allow_initialized option disabled -class AvoidLateKeyword { - /// expect_lint: avoid_late_keyword - late final NotAllowed na1; - - /// ignored_types: Subscription - late final Subscription subscription1; - - /// ignored_types: Subscription - late final Subscription subscription2; - - /// ignored_types: Subscription - late final Subscription> subscription3; - - /// ignored_types: Subscription - late final Subscription>> subscription4; - - /// ignored_types: Subscription - late final Subscription> subscription5; - - void test() { - /// expect_lint: avoid_late_keyword - late final NotAllowed na1; - - /// ignored_types: Subscription - late final Subscription subscription1; - - /// ignored_types: Subscription - late final Subscription subscription2; - - /// ignored_types: Subscription - late final Subscription> subscription3; - - /// ignored_types: Subscription - late final Subscription>> subscription4; - - /// ignored_types: Subscription - late final Subscription> subscription5; - } -} diff --git a/lint_test/avoid_late_keyword/with_generics/analysis_options.yaml b/lint_test/avoid_late_keyword/with_generics/analysis_options.yaml deleted file mode 100644 index 47219688..00000000 --- a/lint_test/avoid_late_keyword/with_generics/analysis_options.yaml +++ /dev/null @@ -1,14 +0,0 @@ -analyzer: - plugins: - - ../custom_lint - -custom_lint: - rules: - - avoid_late_keyword: - allow_initialized: true - ignored_types: - - ColorTween - - AnimationController - - Subscription> - - Subscription> - - Subscription diff --git a/lint_test/avoid_late_keyword/with_generics/avoid_late_keyword_with_generics_test.dart b/lint_test/avoid_late_keyword/with_generics/avoid_late_keyword_with_generics_test.dart deleted file mode 100644 index 9778438e..00000000 --- a/lint_test/avoid_late_keyword/with_generics/avoid_late_keyword_with_generics_test.dart +++ /dev/null @@ -1,114 +0,0 @@ -// ignore_for_file: prefer_const_declarations, unused_local_variable, prefer_match_file_name -// ignore_for_file: avoid_global_state - -class ColorTween {} - -class AnimationController {} - -class SubAnimationController extends AnimationController {} - -class Allowed {} - -class NotAllowed {} - -class Subscription {} - -class ConcreteTypeWithNoGenerics {} - -/// Check "late" keyword fail -/// -/// `avoid_late_keyword` -/// allow_initialized option enabled -class AvoidLateKeyword { - /// ignored_types: ColorTween - late final ColorTween colorTween; - - /// ignored_types: AnimationController - late final AnimationController controller1; - - /// ignored_types: AnimationController - late final SubAnimationController controller2; - - /// ignored_types: AnimationController - late final controller3 = AnimationController(); - - /// ignored_types: AnimationController - late final controller4 = SubAnimationController(); - - /// allow_initialized: true - late final field1 = 'string'; - - /// expect_lint: avoid_late_keyword - late final String field2; - - /// expect_lint: avoid_late_keyword - late final field3; - - /// expect_lint: avoid_late_keyword - late final NotAllowed na1; - - /// allow_initialized: true - late final a = Allowed(); - - /// expect_lint: avoid_late_keyword - late final Subscription subscription1; - - /// ignored_types: Subscription - late final Subscription subscription2; - - /// ignored_types: Subscription> - late final Subscription> subscription3; - - /// ignored_types: Subscription> - late final Subscription>> subscription4; - - /// ignored_types: Subscription> - late final Subscription> subscription5; - - /// ignored_types: Subscription> - late final Subscription> subscription6; - - /// expect_lint: avoid_late_keyword - late final Subscription> subscription7; - - void test() { - /// ignored_types: ColorTween - late final ColorTween colorTween; - - /// ignored_types: AnimationController - late final AnimationController controller1; - - /// ignored_types: AnimationController - late final SubAnimationController controller2; - - /// ignored_types: AnimationController - late final controller3 = AnimationController(); - - /// ignored_types: AnimationController - late final controller4 = SubAnimationController(); - - /// allow_initialized: true - late final local1 = 'string'; - - /// expect_lint: avoid_late_keyword - late final String local2; - - /// expect_lint: avoid_late_keyword - late final local3; - - /// expect_lint: avoid_late_keyword - late final NotAllowed na1; - - /// allow_initialized: true - late final a = Allowed(); - - /// expect_lint: avoid_late_keyword - late final Subscription subscription1; - - /// ignored_types: Subscription - late final Subscription subscription2; - - /// ignored_types: Subscription> - late final Subscription> subscription3; - } -} diff --git a/test/avoid_late_keyword_rule_test.dart b/test/avoid_late_keyword_rule_test.dart new file mode 100644 index 00000000..1211ac6a --- /dev/null +++ b/test/avoid_late_keyword_rule_test.dart @@ -0,0 +1,288 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import 'lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidLateKeywordRuleTest); + defineReflectiveTests(AvoidLateKeywordNoGenericsTest); + defineReflectiveTests(AvoidLateKeywordWithGenericsTest); + }); +} + +@reflectiveTest +class AvoidLateKeywordRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + final String _typesDefinitions = ''' +abstract class Animation {} + +class AnimationController implements Animation {} + +class SubAnimationController extends AnimationController {} + +class ColorTween {} +'''; + + @override + void setUp() { + rule = AvoidLateKeywordRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + super.setUp(); + + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + ${rule.name}: + allow_initialized: false + ignored_types: + - Animation +'''); + } + + Future test_does_not_report_ignored_types_fields() async { + await assertNoDiagnostics(''' +class Test { + late final Animation animation1; + late final animation2 = AnimationController(); + late final animation3 = SubAnimationController(); + late final AnimationController controller1; +} +$_typesDefinitions + '''); + } + + Future test_does_not_report_ignored_types_local_variables() async { + await assertNoDiagnostics(''' +void test() { + late final Animation animation1; + late final animation2 = AnimationController(); + late final animation3 = SubAnimationController(); + late final AnimationController controller1; +} +$_typesDefinitions + '''); + } + + Future test_reports_non_ignored_types_fields() async { + await assertAutoDiagnostics(''' +class Test { + late final ColorTween ${expectLint('colorTween1')}; + late final ${expectLint('colorTween2 = ColorTween()')}; + late final ${expectLint('colorTween3 = colorTween2')}; + late final ${expectLint('field1 = \'string\'')}; + late final String ${expectLint('field2')}; + late final String ${expectLint('field3 = \'string\'')}; + late final ${expectLint('field4')}; +} +$_typesDefinitions + '''); + } + + Future test_reports_non_ignored_types_local_variables() async { + await assertAutoDiagnostics(''' +void test() { + late final ColorTween ${expectLint('colorTween1')}; + late final ${expectLint('colorTween2 = ColorTween()')}; + late final ${expectLint('colorTween3 = colorTween2')}; + late final ${expectLint('local1 = \'string\'')}; + late final String ${expectLint('local2')}; + late final String ${expectLint('local4 = \'string\'')}; + late final ${expectLint('local3')}; +} +$_typesDefinitions + '''); + } +} + +@reflectiveTest +class AvoidLateKeywordNoGenericsTest extends AnalysisRuleTest + with AutoTestLintOffsets { + final String _typesDefinitions = ''' +class Subscription {} + +class ConcreteTypeWithNoGenerics {} + +class NotAllowed {} +'''; + + @override + void setUp() { + rule = AvoidLateKeywordRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + super.setUp(); + + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + ${rule.name}: + allow_initialized: false + ignored_types: + - Subscription +'''); + } + + Future test_does_not_report_ignored_types_fields() async { + await assertNoDiagnostics(''' +class Test { + late final Subscription subscription1; + late final Subscription subscription2; + late final Subscription> subscription3; + late final Subscription>> subscription4; + late final Subscription> subscription5; +} +$_typesDefinitions + '''); + } + + Future test_does_not_report_ignored_types_local_variables() async { + await assertNoDiagnostics(''' +void test() { + late final Subscription subscription1; + late final Subscription subscription2; + late final Subscription> subscription3; + late final Subscription>> subscription4; + late final Subscription> subscription5; +} +$_typesDefinitions + '''); + } + + Future test_reports_non_ignored_types_fields() async { + await assertAutoDiagnostics(''' +class Test { + late final NotAllowed ${expectLint('na1')}; +} +$_typesDefinitions + '''); + } + + Future test_reports_non_ignored_types_local_variables() async { + await assertAutoDiagnostics(''' +void test() { + late final NotAllowed ${expectLint('na1')}; +} +$_typesDefinitions + '''); + } +} + +@reflectiveTest +class AvoidLateKeywordWithGenericsTest extends AnalysisRuleTest + with AutoTestLintOffsets { + final String _typesDefinitions = ''' +class ColorTween {} + +class AnimationController {} + +class SubAnimationController extends AnimationController {} + +class Allowed {} + +class NotAllowed {} + +class Subscription {} + +class ConcreteTypeWithNoGenerics {} +'''; + + @override + void setUp() { + rule = AvoidLateKeywordRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + super.setUp(); + + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + ${rule.name}: + allow_initialized: true + ignored_types: + - ColorTween + - AnimationController + - Subscription> + - Subscription> + - Subscription +'''); + } + + Future test_does_not_report_ignored_types_fields() async { + await assertNoDiagnostics(''' +class Test { + late final ColorTween colorTween; + late final AnimationController controller1; + late final SubAnimationController controller2; + late final controller3 = AnimationController(); + late final controller4 = SubAnimationController(); + late final Subscription subscription2; + late final Subscription> subscription3; + late final Subscription>> subscription4; + late final Subscription> subscription5; + late final Subscription> subscription6; + late final field1 = 'string'; + late final a = Allowed(); +} +$_typesDefinitions + '''); + } + + Future test_does_not_report_ignored_types_local_variables() async { + await assertNoDiagnostics(''' +void test() { + late final ColorTween colorTween; + late final AnimationController controller1; + late final SubAnimationController controller2; + late final controller3 = AnimationController(); + late final controller4 = SubAnimationController(); + late final Subscription subscription2; + late final Subscription> subscription3; + late final local1 = 'string'; + late final a = Allowed(); +} +$_typesDefinitions + '''); + } + + Future test_reports_non_ignored_types_fields() async { + await assertAutoDiagnostics(''' +class Test { + late final String ${expectLint('field2')}; + late final ${expectLint('field3')}; + late final NotAllowed ${expectLint('na1')}; + late final Subscription ${expectLint('subscription1')}; + late final Subscription> ${expectLint('subscription7')}; +} +$_typesDefinitions + '''); + } + + Future test_reports_non_ignored_types_local_variables() async { + await assertAutoDiagnostics(''' +void test() { + late final String ${expectLint('local2')}; + late final ${expectLint('local3')}; + late final NotAllowed ${expectLint('na1')}; + late final Subscription ${expectLint('subscription1')}; +} +$_typesDefinitions + '''); + } +} From 823f3ad2ac5d0e67cc164806528f253f89c97192 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Tue, 30 Jun 2026 10:37:26 +0300 Subject: [PATCH 30/49] refactor: migrate number_of_parameters (#302) Co-authored-by: Illia Aihistov --- lib/main.dart | 6 ++ .../number_of_parameters_parameters.dart | 8 ++ .../number_of_parameters_rule.dart | 85 ++++++++--------- .../number_of_parameters_visitor.dart | 46 +++++++++ .../analysis_options.yaml | 1 - .../number_of_parameters_copy_with_test.dart | 90 ------------------ .../pubspec.yaml | 15 --- lint_test/number_of_parameters_test.dart | 20 ---- .../number_of_parameters_rule_test.dart | 93 +++++++++++++++++++ 9 files changed, 193 insertions(+), 171 deletions(-) create mode 100644 lib/src/lints/number_of_parameters/visitors/number_of_parameters_visitor.dart delete mode 100644 lint_test/number_of_parameters_copy_with_test/analysis_options.yaml delete mode 100644 lint_test/number_of_parameters_copy_with_test/lib/src/number_of_parameters_copy_with_test.dart delete mode 100644 lint_test/number_of_parameters_copy_with_test/pubspec.yaml delete mode 100644 lint_test/number_of_parameters_test.dart create mode 100644 test/src/lints/number_of_parameters/number_of_parameters_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index 546b34b8..586c5d13 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,7 @@ import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart'; import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; import 'package:solid_lints/src/lints/no_magic_number/no_magic_number_rule.dart'; +import 'package:solid_lints/src/lints/number_of_parameters/number_of_parameters_rule.dart'; import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; import 'package:solid_lints/src/lints/prefer_last/fixes/prefer_last_fix.dart'; @@ -60,6 +61,11 @@ class SolidLintsPlugin extends Plugin { FunctionLinesOfCodeRule(analysisOptionsLoader: analysisLoader), AvoidUnusedParametersRule(analysisOptionsLoader: analysisLoader), CyclomaticComplexityRule(analysisOptionsLoader: analysisLoader), + NumberOfParametersRule(analysisOptionsLoader: analysisLoader), + AvoidReturningWidgetsRule(analysisOptionsLoader: analysisLoader), + FunctionLinesOfCodeRule(analysisOptionsLoader: analysisLoader), + AvoidUnusedParametersRule(analysisOptionsLoader: analysisLoader), + CyclomaticComplexityRule(analysisOptionsLoader: analysisLoader), NoEmptyBlockRule(analysisOptionsLoader: analysisLoader), UseNearestContextRule(), NoMagicNumberRule(analysisOptionsLoader: analysisLoader), diff --git a/lib/src/lints/number_of_parameters/models/number_of_parameters_parameters.dart b/lib/src/lints/number_of_parameters/models/number_of_parameters_parameters.dart index 0ac631a4..2c0f0e89 100644 --- a/lib/src/lints/number_of_parameters/models/number_of_parameters_parameters.dart +++ b/lib/src/lints/number_of_parameters/models/number_of_parameters_parameters.dart @@ -17,6 +17,14 @@ class NumberOfParametersParameters { required this.exclude, }); + /// Empty [NumberOfParametersParameters] model. + factory NumberOfParametersParameters.empty() { + return NumberOfParametersParameters( + maxParameters: _defaultMaxParameters, + exclude: ExcludedIdentifiersListParameter(exclude: []), + ); + } + /// Method for creating from json data factory NumberOfParametersParameters.fromJson(Map json) => NumberOfParametersParameters( diff --git a/lib/src/lints/number_of_parameters/number_of_parameters_rule.dart b/lib/src/lints/number_of_parameters/number_of_parameters_rule.dart index c31bc78d..dbea292d 100644 --- a/lib/src/lints/number_of_parameters/number_of_parameters_rule.dart +++ b/lib/src/lints/number_of_parameters/number_of_parameters_rule.dart @@ -1,8 +1,8 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/number_of_parameters/models/number_of_parameters_parameters.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; +import 'package:solid_lints/src/lints/number_of_parameters/visitors/number_of_parameters_visitor.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// A number of parameters metric which checks whether we didn't exceed @@ -14,10 +14,11 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Assuming config: /// /// ```yaml -/// custom_lint: -/// rules: -/// - number_of_parameters: -/// max_parameters: 2 +/// plugins: +/// solid_lints: +/// diagnostics: +/// number_of_parameters: +/// max_parameters: 2 /// ``` /// /// #### BAD: @@ -37,50 +38,44 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// ``` class NumberOfParametersRule extends SolidLintRule { - /// This lint rule represents the error if number of - /// parameters reaches the maximum value. + /// The name of this lint rule. static const lintName = 'number_of_parameters'; - NumberOfParametersRule._(super.rule); + static const _code = LintCode( + lintName, + 'The maximum allowed number of parameters is {0}. ' + 'Try reducing the number of parameters.', + ); - /// Creates a new instance of [NumberOfParametersRule] - /// based on the lint configuration. - factory NumberOfParametersRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - configs: configs, - name: lintName, - paramsParser: NumberOfParametersParameters.fromJson, - problemMessage: (value) => - 'The maximum allowed number of parameters is ${value.maxParameters}. ' - 'Try reducing the number of parameters.', - ); + @override + DiagnosticCode get diagnosticCode => _code; - return NumberOfParametersRule._(rule); - } + /// Creates a new instance of [NumberOfParametersRule]. + NumberOfParametersRule({ + required super.analysisOptionsLoader, + }) : super.withParameters( + name: lintName, + description: + "Checks whether we didn't exceed the maximum allowed number " + 'of parameters for a function, method or constructor.', + parametersParser: NumberOfParametersParameters.fromJson, + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addDeclaration((node) { - final isIgnored = config.parameters.exclude.shouldIgnore(node); - final parameters = switch (node) { - (final MethodDeclaration node) => - node.parameters?.parameters.length ?? 0, - (final FunctionDeclaration node) => - node.functionExpression.parameters?.parameters.length ?? 0, - _ => 0, - }; + super.registerNodeProcessors(registry, context); + + final parameters = + getParametersForContext(context) ?? + NumberOfParametersParameters.empty(); + + final visitor = NumberOfParametersVisitor(this, parameters); - if (!isIgnored && parameters > config.parameters.maxParameters) { - reporter.atOffset( - offset: node.firstTokenAfterCommentAndMetadata.offset, - length: node.end, - diagnosticCode: code, - ); - } - }); + registry.addFunctionDeclaration(this, visitor); + registry.addMethodDeclaration(this, visitor); + registry.addConstructorDeclaration(this, visitor); } } diff --git a/lib/src/lints/number_of_parameters/visitors/number_of_parameters_visitor.dart b/lib/src/lints/number_of_parameters/visitors/number_of_parameters_visitor.dart new file mode 100644 index 00000000..c898b798 --- /dev/null +++ b/lib/src/lints/number_of_parameters/visitors/number_of_parameters_visitor.dart @@ -0,0 +1,46 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/number_of_parameters/models/number_of_parameters_parameters.dart'; +import 'package:solid_lints/src/lints/number_of_parameters/number_of_parameters_rule.dart'; +import 'package:solid_lints/src/utils/node_utils.dart'; + +/// A visitor that checks the number of parameters for functions, methods, and +/// constructors. +class NumberOfParametersVisitor extends SimpleAstVisitor { + final NumberOfParametersRule _rule; + final NumberOfParametersParameters _parameters; + + /// Creates a new instance of [NumberOfParametersVisitor]. + NumberOfParametersVisitor(this._rule, this._parameters); + + void _check(Declaration node, FormalParameterList? parameterList) { + if (parameterList == null) return; + + final isIgnored = _parameters.exclude.shouldIgnore(node); + if (isIgnored) return; + + final count = parameterList.parameters.length; + if (count > _parameters.maxParameters) { + _rule.reportAtNode( + parameterList, + arguments: [_parameters.maxParameters.toString()], + ); + } + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + _check(node, node.functionExpression.parameters); + } + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (isOverride(node.metadata)) return; + _check(node, node.parameters); + } + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) { + _check(node, node.parameters); + } +} diff --git a/lint_test/number_of_parameters_copy_with_test/analysis_options.yaml b/lint_test/number_of_parameters_copy_with_test/analysis_options.yaml deleted file mode 100644 index 3fdd2d5e..00000000 --- a/lint_test/number_of_parameters_copy_with_test/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../../lib/analysis_options.yaml diff --git a/lint_test/number_of_parameters_copy_with_test/lib/src/number_of_parameters_copy_with_test.dart b/lint_test/number_of_parameters_copy_with_test/lib/src/number_of_parameters_copy_with_test.dart deleted file mode 100644 index 60a6f42f..00000000 --- a/lint_test/number_of_parameters_copy_with_test/lib/src/number_of_parameters_copy_with_test.dart +++ /dev/null @@ -1,90 +0,0 @@ -// ignore_for_file: prefer_match_file_name, public_member_api_docs -// Check number of parameters fail -/// -/// `number_of_parameters: max_parameters` - -class UserDto { - final String a; - final String b; - final String c; - final String d; - final String e; - final String f; - final String g; - final String h; - - const UserDto({ - required this.a, - required this.b, - required this.c, - required this.d, - required this.e, - required this.f, - required this.g, - required this.h, - }); - - /// Excluded by method_name - UserDto copyWith({ - String? a, - String? b, - String? c, - String? d, - String? e, - String? f, - String? g, - String? h, - }) { - return UserDto( - a: a ?? this.a, - b: b ?? this.b, - c: c ?? this.c, - d: d ?? this.d, - e: e ?? this.e, - f: f ?? this.f, - g: g ?? this.g, - h: h ?? this.h, - ); - } - - /// expect_lint: number_of_parameters - UserDto noCopyWith({ - String? a, - String? b, - String? c, - String? d, - String? e, - String? f, - String? g, - String? h, - }) { - return UserDto( - a: a ?? this.a, - b: b ?? this.b, - c: c ?? this.c, - d: d ?? this.d, - e: e ?? this.e, - f: f ?? this.f, - g: g ?? this.g, - h: h ?? this.h, - ); - } - - /// Allow - UserDto noLongCopyWith({ - String? a, - String? b, - String? c, - }) { - return UserDto( - a: a ?? this.a, - b: b ?? this.b, - c: c ?? this.c, - d: '', - e: '', - f: '', - g: '', - h: '', - ); - } -} diff --git a/lint_test/number_of_parameters_copy_with_test/pubspec.yaml b/lint_test/number_of_parameters_copy_with_test/pubspec.yaml deleted file mode 100644 index e753375a..00000000 --- a/lint_test/number_of_parameters_copy_with_test/pubspec.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: solid_lints_number_of_parameters_copy_with_test -description: A starting point for Dart libraries or applications. -publish_to: none - -environment: - sdk: '>=3.0.0 <4.0.0' - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - solid_lints: - path: ../../ - test: ^1.20.1 diff --git a/lint_test/number_of_parameters_test.dart b/lint_test/number_of_parameters_test.dart deleted file mode 100644 index 40f9b5b8..00000000 --- a/lint_test/number_of_parameters_test.dart +++ /dev/null @@ -1,20 +0,0 @@ -// ignore_for_file: prefer_match_file_name -// Check number of parameters fail -/// -/// `number_of_parameters: max_parameters` -/// expect_lint: number_of_parameters -String numberOfParameters(String a, String b, String c) { - return a + b + c; -} - -//no lint -String avoidNumberOfParameters(String a, String b, String c) { - return a + b + c; -} - -class Exclude { - //no lint - String avoidNumberOfParameters(String a, String b, String c) { - return a + b + c; - } -} diff --git a/test/src/lints/number_of_parameters/number_of_parameters_rule_test.dart b/test/src/lints/number_of_parameters/number_of_parameters_rule_test.dart new file mode 100644 index 00000000..04dd4197 --- /dev/null +++ b/test/src/lints/number_of_parameters/number_of_parameters_rule_test.dart @@ -0,0 +1,93 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/number_of_parameters/number_of_parameters_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(NumberOfParametersRuleTest); + }); +} + +@reflectiveTest +class NumberOfParametersRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + static const _mockAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + number_of_parameters: + max_parameters: 2 + exclude: + - method_name: copyWith + - class_name: Base + method_name: myMethod + '''; + + @override + void setUp() { + rule = NumberOfParametersRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); + } + + @override + String get analysisRule => NumberOfParametersRule.lintName; + + Future test_reports_on_exceeding_max_parameters() async { + await assertAutoDiagnostics(''' +void fun${expectLint(r'(int a, int b, int c)')} {} +'''); + } + + Future test_does_not_report_on_limit_or_below() async { + await assertNoDiagnostics(r''' +void fun(int a, int b) {} +void fun2(int a) {} +void fun3() {} +'''); + } + + Future test_does_not_report_on_excluded_copyWith() async { + await assertNoDiagnostics(r''' +class UserDto { + UserDto copyWith(int a, int b, int c) { + return UserDto(); + } +} +'''); + } + + Future test_reports_on_constructors_exceeding_max_parameters() async { + await assertAutoDiagnostics(''' +class Test { + Test${expectLint(r'(int a, int b, int c)')}; +} +'''); + } + + Future test_does_not_report_on_overridden_methods() async { + await assertNoDiagnostics(r''' +class Base { + void myMethod(int a, int b, int c) {} +} + +class Derived extends Base { + @override + void myMethod(int a, int b, int c) {} +} +'''); + } +} From d4f3c8d1f227f8912d3b7dd60687e8dd18e4dcc9 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 1 Jul 2026 14:42:35 +0300 Subject: [PATCH 31/49] refactor: migrate member_ordering rule (#297) * refactor: migrate member_ordering rule * refactor: extract member group satisfaction logic to extensions and simplify visitor reporting methods * refactor: unify member ordering diagnostic reporting with a shared helper method * refactor: simplify member group satisfaction checks * refactor: replace member group satisfaction logic with a formal implies utility * refactor: replace switch expression with type check in implies methods and clean up formatting * refactor: use switch expression to determine field keyword in FieldMemberGroupUtils * refactor: modularize member ordering visitor logic --------- Co-authored-by: Illia Aihistov --- lib/main.dart | 6 +- .../lints/member_ordering/config_parser.dart | 1 - .../member_ordering/member_ordering_rule.dart | 172 ++--- .../member_ordering/models/annotation.dart | 19 +- .../member_ordering/models/field_keyword.dart | 15 +- .../constructor_member_group.dart | 8 + .../member_group/field_member_group.dart | 26 +- .../member_group/get_set_member_group.dart | 9 + .../models/member_group/member_group.dart | 4 + .../member_group/method_member_group.dart | 28 +- .../member_ordering/models/member_info.dart | 17 + .../member_ordering/models/member_names.dart | 22 + .../member_ordering/models/member_order.dart | 33 + .../models/member_ordering_parameters.dart | 8 + .../member_ordering/models/member_type.dart | 16 +- .../member_ordering/models/modifier.dart | 8 + .../declaration_ordering_visitor.dart | 187 ++++++ .../visitors/member_ordering_reporter.dart | 96 +++ .../visitors/member_ordering_visitor.dart | 337 +--------- lib/src/models/rule_parameters_parser.dart | 2 + lib/src/models/solid_lint_rule.dart | 4 +- lib/src/models/solid_multi_lint_rule.dart | 37 ++ lib/src/utils/implies.dart | 19 + lint_test/member_ordering_test.dart | 232 ------- .../member_ordering_rule_test.dart | 620 ++++++++++++++++++ 25 files changed, 1231 insertions(+), 695 deletions(-) create mode 100644 lib/src/lints/member_ordering/models/member_info.dart create mode 100644 lib/src/lints/member_ordering/models/member_names.dart create mode 100644 lib/src/lints/member_ordering/models/member_order.dart create mode 100644 lib/src/lints/member_ordering/visitors/declaration_ordering_visitor.dart create mode 100644 lib/src/lints/member_ordering/visitors/member_ordering_reporter.dart create mode 100644 lib/src/models/rule_parameters_parser.dart create mode 100644 lib/src/models/solid_multi_lint_rule.dart create mode 100644 lib/src/utils/implies.dart delete mode 100644 lint_test/member_ordering_test.dart create mode 100644 test/src/lints/member_ordering/member_ordering_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index 586c5d13..feae0050 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,7 @@ import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexit import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart'; +import 'package:solid_lints/src/lints/member_ordering/member_ordering_rule.dart'; import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; import 'package:solid_lints/src/lints/no_magic_number/no_magic_number_rule.dart'; import 'package:solid_lints/src/lints/number_of_parameters/number_of_parameters_rule.dart'; @@ -58,13 +59,10 @@ class SolidLintsPlugin extends Plugin { doubleLiteralFormatRule, ProperSuperCallsRule(), AvoidReturningWidgetsRule(analysisOptionsLoader: analysisLoader), - FunctionLinesOfCodeRule(analysisOptionsLoader: analysisLoader), + MemberOrderingRule(analysisOptionsLoader: analysisLoader), AvoidUnusedParametersRule(analysisOptionsLoader: analysisLoader), - CyclomaticComplexityRule(analysisOptionsLoader: analysisLoader), NumberOfParametersRule(analysisOptionsLoader: analysisLoader), - AvoidReturningWidgetsRule(analysisOptionsLoader: analysisLoader), FunctionLinesOfCodeRule(analysisOptionsLoader: analysisLoader), - AvoidUnusedParametersRule(analysisOptionsLoader: analysisLoader), CyclomaticComplexityRule(analysisOptionsLoader: analysisLoader), NoEmptyBlockRule(analysisOptionsLoader: analysisLoader), UseNearestContextRule(), diff --git a/lib/src/lints/member_ordering/config_parser.dart b/lib/src/lints/member_ordering/config_parser.dart index 2d847d71..0bf0c5e2 100644 --- a/lib/src/lints/member_ordering/config_parser.dart +++ b/lib/src/lints/member_ordering/config_parser.dart @@ -54,7 +54,6 @@ class MemberOrderingConfigParser { 'final_fields', 'init_state_method', 'var_fields', - 'init_state_method', 'private_methods', 'overridden_public_methods', 'build_method', diff --git a/lib/src/lints/member_ordering/member_ordering_rule.dart b/lib/src/lints/member_ordering/member_ordering_rule.dart index 3f83f1c1..0c44f6bb 100644 --- a/lib/src/lints/member_ordering/member_ordering_rule.dart +++ b/lib/src/lints/member_ordering/member_ordering_rule.dart @@ -1,9 +1,9 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/member_ordering/models/member_ordering_parameters.dart'; import 'package:solid_lints/src/lints/member_ordering/visitors/member_ordering_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; +import 'package:solid_lints/src/models/solid_multi_lint_rule.dart'; /// A lint which allows to enforce a particular class member ordering /// conventions. @@ -45,14 +45,15 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Assuming config: /// /// ```yaml -/// custom_lint: -/// rules: -/// - member_ordering: -/// alphabetize: true -/// order: -/// - fields -/// - getters_setters -/// - methods +/// plugins: +/// solid_lints: +/// diagnostics: +/// member_ordering: +/// alphabetize: true +/// order: +/// - fields +/// - getters_setters +/// - methods /// ``` /// /// #### BAD: @@ -82,113 +83,60 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// void method() {} /// } /// ``` -class MemberOrderingRule extends SolidLintRule { - /// This lint rule represents - /// the error whether we use bad formatted double literals. +class MemberOrderingRule extends SolidMultiLintRule { + /// The name of this lint rule. static const lintName = 'member_ordering'; - static const _warningMessage = 'should be before'; - static const _warningAlphabeticalMessage = 'should be alphabetically before'; - static const _warningTypeAlphabeticalMessage = - 'type name should be alphabetically before'; + /// Reported when a class member is declared out of order. + static const wrongOrderCode = LintCode( + lintName, + '{0} should be before {1}.', + uniqueName: 'wrong_order', + ); + + /// Reported when class members in the same group are not sorted + /// alphabetically. + static const alphabeticalOrderCode = LintCode( + lintName, + '{0} should be alphabetically before {1}.', + uniqueName: 'alphabetical_order', + ); + + /// Reported when class members in the same group are not sorted + /// alphabetically by type name. + static const alphabeticalByTypeOrderCode = LintCode( + lintName, + '{0} type name should be alphabetically before {1}.', + uniqueName: 'alphabetical_by_type_order', + ); + + /// Creates a new instance of [MemberOrderingRule]. + MemberOrderingRule({ + required super.analysisOptionsLoader, + }) : super( + name: lintName, + description: 'Enforces a particular class member ordering convention.', + parametersParser: MemberOrderingParameters.fromJson, + ); - MemberOrderingRule._(super.config); - - /// Creates a new instance of [MemberOrderingRule] - /// based on the lint configuration. - factory MemberOrderingRule.createRule(CustomLintConfigs configs) { - final config = RuleConfig( - configs: configs, - name: lintName, - paramsParser: MemberOrderingParameters.fromJson, - problemMessage: (_) => "Order of class member is wrong", - ); - - return MemberOrderingRule._(config); - } + @override + List get diagnosticCodes => [ + wrongOrderCode, + alphabeticalOrderCode, + alphabeticalByTypeOrderCode, + ]; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addClassDeclaration((node) { - final visitor = MemberOrderingVisitor( - config.parameters.groupsOrder, - config.parameters.widgetsGroupsOrder, - ); - - final membersInfo = visitor.visitClassDeclaration(node); - final wrongOrderMembers = membersInfo.where( - (info) => info.memberOrder.isWrong, - ); - - for (final memberInfo in wrongOrderMembers) { - reporter.atNode( - memberInfo.classMember, - _createWrongOrderLintCode(memberInfo), - ); - } - - if (config.parameters.alphabetize) { - final alphabeticallyWrongOrderMembers = membersInfo.where( - (info) => info.memberOrder.isAlphabeticallyWrong, - ); - - for (final memberInfo in alphabeticallyWrongOrderMembers) { - reporter.atNode( - memberInfo.classMember, - _createAlphabeticallyWrongOrderLintCode(memberInfo), - ); - } - } - - if (!config.parameters.alphabetize && - config.parameters.alphabetizeByType) { - final alphabeticallyByTypeWrongOrderMembers = membersInfo.where( - (info) => info.memberOrder.isByTypeWrong, - ); - - for (final memberInfo in alphabeticallyByTypeWrongOrderMembers) { - reporter.atNode( - memberInfo.classMember, - _createAlphabeticallyByTypeWrongOrderLintCode(memberInfo), - ); - } - } - }); - } - - LintCode _createWrongOrderLintCode(MemberInfo info) { - final memberGroup = info.memberOrder.memberGroup; - final previousMemberGroup = info.memberOrder.previousMemberGroup; - - return LintCode( - name: lintName, - problemMessage: "$memberGroup $_warningMessage $previousMemberGroup.", - ); - } - - LintCode _createAlphabeticallyWrongOrderLintCode(MemberInfo info) { - final names = info.memberOrder.memberNames; - final current = names.currentName; - final previous = names.previousName; - - return LintCode( - name: lintName, - problemMessage: "$current $_warningAlphabeticalMessage $previous.", - ); - } + super.registerNodeProcessors(registry, context); - LintCode _createAlphabeticallyByTypeWrongOrderLintCode(MemberInfo info) { - final names = info.memberOrder.memberNames; - final current = names.currentName; - final previous = names.previousName; + final parameters = + getParametersForContext(context) ?? MemberOrderingParameters.empty(); + final visitor = MemberOrderingVisitor(this, parameters); - return LintCode( - name: lintName, - problemMessage: "$current $_warningTypeAlphabeticalMessage $previous", - ); + registry.addClassDeclaration(this, visitor); } } diff --git a/lib/src/lints/member_ordering/models/annotation.dart b/lib/src/lints/member_ordering/models/annotation.dart index 1e16249c..d7f4a0f3 100644 --- a/lib/src/lints/member_ordering/models/annotation.dart +++ b/lib/src/lints/member_ordering/models/annotation.dart @@ -21,6 +21,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import 'package:solid_lints/src/utils/implies.dart'; + /// A data model enum represents annotation enum Annotation { /// override annotation @@ -43,9 +45,16 @@ enum Annotation { /// Parses a String name and returns instance of [Annotation] static Annotation parse(String? name) => values.firstWhere( - (annotation) => - annotation.name == name || - (annotation.publicName != null && annotation.publicName == name), - orElse: () => Annotation.unset, - ); + (annotation) => + annotation.name == name || + (annotation.publicName != null && annotation.publicName == name), + orElse: () => Annotation.unset, + ); +} + +/// Logical implication operation for annotation +extension AnnotationImplies on Annotation { + /// Logical implication operation. + bool implies(Annotation other) => + objectImplies(this, other, Annotation.unset); } diff --git a/lib/src/lints/member_ordering/models/field_keyword.dart b/lib/src/lints/member_ordering/models/field_keyword.dart index d2716ca2..31c9f55c 100644 --- a/lib/src/lints/member_ordering/models/field_keyword.dart +++ b/lib/src/lints/member_ordering/models/field_keyword.dart @@ -21,6 +21,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import 'package:solid_lints/src/utils/implies.dart'; + /// A data model enum represents field type keyword enum FieldKeyword { /// final keyword @@ -43,7 +45,14 @@ enum FieldKeyword { /// Parses a String field type and returns instance of [FieldKeyword] static FieldKeyword parse(String? name) => values.firstWhere( - (type) => type.type == name, - orElse: () => FieldKeyword.unset, - ); + (type) => type.type == name, + orElse: () => FieldKeyword.unset, + ); +} + +/// Logical implication operation for field keyword +extension FieldKeywordImplies on FieldKeyword { + /// Logical implication operation. + bool implies(FieldKeyword other) => + objectImplies(this, other, FieldKeyword.unset); } diff --git a/lib/src/lints/member_ordering/models/member_group/constructor_member_group.dart b/lib/src/lints/member_ordering/models/member_group/constructor_member_group.dart index 206d02bd..5faaaac6 100644 --- a/lib/src/lints/member_ordering/models/member_group/constructor_member_group.dart +++ b/lib/src/lints/member_ordering/models/member_group/constructor_member_group.dart @@ -27,6 +27,7 @@ import 'package:solid_lints/src/lints/member_ordering/models/annotation.dart'; import 'package:solid_lints/src/lints/member_ordering/models/member_group/member_group.dart'; import 'package:solid_lints/src/lints/member_ordering/models/member_type.dart'; import 'package:solid_lints/src/lints/member_ordering/models/modifier.dart'; +import 'package:solid_lints/src/utils/implies.dart'; /// Data class represents class constructor class ConstructorMemberGroup extends MemberGroup { @@ -80,4 +81,11 @@ class ConstructorMemberGroup extends MemberGroup { @override String toString() => rawRepresentation; + + @override + bool implies(MemberGroup other) => + other is ConstructorMemberGroup && + super.implies(other) && + isFactory.implies(other.isFactory) && + isNamed.implies(other.isNamed); } diff --git a/lib/src/lints/member_ordering/models/member_group/field_member_group.dart b/lib/src/lints/member_ordering/models/member_group/field_member_group.dart index f5431eb9..96b5b665 100644 --- a/lib/src/lints/member_ordering/models/member_group/field_member_group.dart +++ b/lib/src/lints/member_ordering/models/member_group/field_member_group.dart @@ -29,6 +29,7 @@ import 'package:solid_lints/src/lints/member_ordering/models/field_keyword.dart' import 'package:solid_lints/src/lints/member_ordering/models/member_group/member_group.dart'; import 'package:solid_lints/src/lints/member_ordering/models/member_type.dart'; import 'package:solid_lints/src/lints/member_ordering/models/modifier.dart'; +import 'package:solid_lints/src/utils/implies.dart'; /// Data class represents class field class FieldMemberGroup extends MemberGroup { @@ -62,7 +63,8 @@ class FieldMemberGroup extends MemberGroup { final modifier = parseModifier( declaration.fields.variables.first.name.lexeme, ); - final isNullable = declaration.fields.type?.type?.nullabilitySuffix == + final isNullable = + declaration.fields.type?.type?.nullabilitySuffix == NullabilitySuffix.question; final keyword = _FieldMemberGroupUtils.parseKeyWord(declaration); @@ -94,14 +96,22 @@ class FieldMemberGroup extends MemberGroup { @override String toString() => rawRepresentation; + + @override + bool implies(MemberGroup other) => + other is FieldMemberGroup && + super.implies(other) && + isLate.implies(other.isLate) && + isStatic.implies(other.isStatic) && + isNullable.implies(other.isNullable) && + keyword.implies(other.keyword); } class _FieldMemberGroupUtils { - static FieldKeyword parseKeyWord(FieldDeclaration declaration) { - return declaration.fields.isConst - ? FieldKeyword.isConst - : declaration.fields.isFinal - ? FieldKeyword.isFinal - : FieldKeyword.unset; - } + static FieldKeyword parseKeyWord(FieldDeclaration declaration) => + switch ((declaration.fields.isConst, declaration.fields.isFinal)) { + (true, _) => FieldKeyword.isConst, + (_, true) => FieldKeyword.isFinal, + _ => FieldKeyword.unset, + }; } diff --git a/lib/src/lints/member_ordering/models/member_group/get_set_member_group.dart b/lib/src/lints/member_ordering/models/member_group/get_set_member_group.dart index b5ba70e0..b8e7343d 100644 --- a/lib/src/lints/member_ordering/models/member_group/get_set_member_group.dart +++ b/lib/src/lints/member_ordering/models/member_group/get_set_member_group.dart @@ -27,6 +27,7 @@ import 'package:solid_lints/src/lints/member_ordering/models/annotation.dart'; import 'package:solid_lints/src/lints/member_ordering/models/member_group/member_group.dart'; import 'package:solid_lints/src/lints/member_ordering/models/member_type.dart'; import 'package:solid_lints/src/lints/member_ordering/models/modifier.dart'; +import 'package:solid_lints/src/utils/implies.dart'; /// Data class represents class getter or setter class GetSetMemberGroup extends MemberGroup { @@ -76,4 +77,12 @@ class GetSetMemberGroup extends MemberGroup { @override String toString() => rawRepresentation; + + @override + bool implies(MemberGroup other) => + other is GetSetMemberGroup && + super.implies(other) && + isStatic.implies(other.isStatic) && + isNullable.implies(other.isNullable) && + memberType.implies(other.memberType); } diff --git a/lib/src/lints/member_ordering/models/member_group/member_group.dart b/lib/src/lints/member_ordering/models/member_group/member_group.dart index 6eda1207..e561590a 100644 --- a/lib/src/lints/member_ordering/models/member_group/member_group.dart +++ b/lib/src/lints/member_ordering/models/member_group/member_group.dart @@ -49,4 +49,8 @@ abstract class MemberGroup { /// Method to get sorting coefficient of the member group int getSortingCoefficient(); + + /// Logical implication operation. + bool implies(MemberGroup other) => + modifier.implies(other.modifier) && annotation.implies(other.annotation); } diff --git a/lib/src/lints/member_ordering/models/member_group/method_member_group.dart b/lib/src/lints/member_ordering/models/member_group/method_member_group.dart index 467afb22..7a3b09a4 100644 --- a/lib/src/lints/member_ordering/models/member_group/method_member_group.dart +++ b/lib/src/lints/member_ordering/models/member_group/method_member_group.dart @@ -27,6 +27,7 @@ import 'package:solid_lints/src/lints/member_ordering/models/annotation.dart'; import 'package:solid_lints/src/lints/member_ordering/models/member_group/member_group.dart'; import 'package:solid_lints/src/lints/member_ordering/models/member_type.dart'; import 'package:solid_lints/src/lints/member_ordering/models/modifier.dart'; +import 'package:solid_lints/src/utils/implies.dart'; /// Data class represents class method class MethodMemberGroup extends MemberGroup { @@ -55,16 +56,15 @@ class MethodMemberGroup extends MemberGroup { required String name, required MemberType memberType, required String rawRepresentation, - }) => - MethodMemberGroup( - name: name, - isNullable: false, - isStatic: false, - modifier: Modifier.unset, - annotation: Annotation.unset, - memberType: memberType, - rawRepresentation: rawRepresentation, - ); + }) => MethodMemberGroup( + name: name, + isNullable: false, + isStatic: false, + modifier: Modifier.unset, + annotation: Annotation.unset, + memberType: memberType, + rawRepresentation: rawRepresentation, + ); /// Parses [MethodDeclaration] and returns instance of [MethodMemberGroup] factory MethodMemberGroup.parse(MethodDeclaration declaration) { @@ -100,4 +100,12 @@ class MethodMemberGroup extends MemberGroup { @override String toString() => rawRepresentation; + + @override + bool implies(MemberGroup other) => + other is MethodMemberGroup && + super.implies(other) && + isStatic.implies(other.isStatic) && + isNullable.implies(other.isNullable) && + name.implies(other.name); } diff --git a/lib/src/lints/member_ordering/models/member_info.dart b/lib/src/lints/member_ordering/models/member_info.dart new file mode 100644 index 00000000..56a7fd4e --- /dev/null +++ b/lib/src/lints/member_ordering/models/member_info.dart @@ -0,0 +1,17 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_order.dart'; + +/// Data class that holds AST class member and it's order info +class MemberInfo { + /// AST instance of an [ClassMember] + final ClassMember classMember; + + /// Class member order info + final MemberOrder memberOrder; + + /// Creates instance of an [MemberInfo] + const MemberInfo({ + required this.classMember, + required this.memberOrder, + }); +} diff --git a/lib/src/lints/member_ordering/models/member_names.dart b/lib/src/lints/member_ordering/models/member_names.dart new file mode 100644 index 00000000..b24b333a --- /dev/null +++ b/lib/src/lints/member_ordering/models/member_names.dart @@ -0,0 +1,22 @@ +/// Data class contains info about current and previous class member names +class MemberNames { + /// Name of current class member + final String currentName; + + /// Name of previous class member + final String? previousName; + + /// Type name of current class member + final String currentTypeName; + + /// Type name of previous class member + final String? previousTypeName; + + /// Creates instance of [MemberNames] + const MemberNames({ + required this.currentName, + required this.currentTypeName, + this.previousName, + this.previousTypeName, + }); +} diff --git a/lib/src/lints/member_ordering/models/member_order.dart b/lib/src/lints/member_ordering/models/member_order.dart new file mode 100644 index 00000000..8ec59a5b --- /dev/null +++ b/lib/src/lints/member_ordering/models/member_order.dart @@ -0,0 +1,33 @@ +import 'package:solid_lints/src/lints/member_ordering/models/member_group/member_group.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_names.dart'; + +/// Data class holds information about class member order info +class MemberOrder { + /// Indicates if order is wrong + final bool isWrong; + + /// Indicates if order is wrong alphabetically + final bool isAlphabeticallyWrong; + + /// Indicates if order is wrong alphabetically by type + final bool isByTypeWrong; + + /// Info about current and previous class member name + final MemberNames memberNames; + + /// Info about current member member group + final MemberGroup memberGroup; + + /// Info about previous member member group + final MemberGroup? previousMemberGroup; + + /// Creates instance of [MemberOrder] + const MemberOrder({ + required this.isWrong, + required this.isAlphabeticallyWrong, + required this.isByTypeWrong, + required this.memberNames, + required this.memberGroup, + this.previousMemberGroup, + }); +} diff --git a/lib/src/lints/member_ordering/models/member_ordering_parameters.dart b/lib/src/lints/member_ordering/models/member_ordering_parameters.dart index 129ccf6a..647abec5 100644 --- a/lib/src/lints/member_ordering/models/member_ordering_parameters.dart +++ b/lib/src/lints/member_ordering/models/member_ordering_parameters.dart @@ -54,6 +54,14 @@ class MemberOrderingParameters { required this.alphabetizeByType, }); + /// Factory for creating empty/default parameters. + factory MemberOrderingParameters.empty() => MemberOrderingParameters( + groupsOrder: MemberOrderingConfigParser.parseOrder(null), + widgetsGroupsOrder: MemberOrderingConfigParser.parseWidgetsOrder(null), + alphabetize: false, + alphabetizeByType: false, + ); + /// Method for creating from json data factory MemberOrderingParameters.fromJson(Map json) => MemberOrderingParameters( diff --git a/lib/src/lints/member_ordering/models/member_type.dart b/lib/src/lints/member_ordering/models/member_type.dart index f7d01737..686719cd 100644 --- a/lib/src/lints/member_ordering/models/member_type.dart +++ b/lib/src/lints/member_ordering/models/member_type.dart @@ -32,7 +32,7 @@ enum MemberType { method('methods', typeAlias: 'method'), /// Indicates constructor affiliation - constructor('constructors'), + constructor('constructors', typeAlias: 'constructor'), /// Indicates getters affiliation getter('getters'), @@ -52,6 +52,16 @@ enum MemberType { const MemberType(this.type, {this.typeAlias}); /// Parses a String member type and returns instance of [MemberType] - static MemberType? parse(String? name) => values - .firstWhereOrNull((type) => name == type.type || name == type.typeAlias); + static MemberType? parse(String? name) => values.firstWhereOrNull( + (type) => name == type.type || name == type.typeAlias, + ); +} + +/// Logical implication operation for member type +extension MemberTypeImplies on MemberType { + /// Logical implication operation. + bool implies(MemberType other) => + this == other || + (this == MemberType.getterAndSetter && + (other == MemberType.getter || other == MemberType.setter)); } diff --git a/lib/src/lints/member_ordering/models/modifier.dart b/lib/src/lints/member_ordering/models/modifier.dart index b47b7f89..f840f261 100644 --- a/lib/src/lints/member_ordering/models/modifier.dart +++ b/lib/src/lints/member_ordering/models/modifier.dart @@ -21,6 +21,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import 'package:solid_lints/src/utils/implies.dart'; + /// A data model enum represents class member access modifier enum Modifier { /// Indicates public access modifier @@ -42,3 +44,9 @@ enum Modifier { static Modifier parse(String? name) => values .firstWhere((type) => type.type == name, orElse: () => Modifier.unset); } + +/// Logical implication operation for access modifier +extension ModifierImplies on Modifier { + /// Logical implication operation. + bool implies(Modifier other) => objectImplies(this, other, Modifier.unset); +} diff --git a/lib/src/lints/member_ordering/visitors/declaration_ordering_visitor.dart b/lib/src/lints/member_ordering/visitors/declaration_ordering_visitor.dart new file mode 100644 index 00000000..709cb2ed --- /dev/null +++ b/lib/src/lints/member_ordering/visitors/declaration_ordering_visitor.dart @@ -0,0 +1,187 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:collection/collection.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_group/constructor_member_group.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_group/field_member_group.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_group/get_set_member_group.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_group/member_group.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_group/method_member_group.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_info.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_names.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_order.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_ordering_parameters.dart'; + +/// Visitor for determining class member ordering. +class DeclarationOrderingVisitor { + final bool _isFlutterWidget; + final MemberOrderingParameters _parameters; + + /// Collected member info. + final membersInfo = []; + + /// Creates instance of [DeclarationOrderingVisitor]. + DeclarationOrderingVisitor({ + required MemberOrderingParameters parameters, + required bool isFlutterWidget, + }) : _parameters = parameters, + _isFlutterWidget = isFlutterWidget; + + /// Visits a [ClassMember]. + void visit(ClassMember member) => switch (member) { + FieldDeclaration() => _visitFieldDeclaration(member), + ConstructorDeclaration() => _visitConstructorDeclaration(member), + MethodDeclaration() => _visitMethodDeclaration(member), + _ => (), + }; + + void _visitFieldDeclaration(FieldDeclaration declaration) => _addMemberInfo( + classMember: declaration, + parsedGroup: FieldMemberGroup.parse(declaration), + token: declaration.fields.variables.first.name, + type: declaration.fields.type?.type?.getDisplayString() ?? '_', + ); + + void _visitConstructorDeclaration(ConstructorDeclaration declaration) => + _addMemberInfo( + classMember: declaration, + parsedGroup: ConstructorMemberGroup.parse(declaration), + token: declaration.name, + type: declaration.typeName?.name ?? '', + ); + + void _visitMethodDeclaration(MethodDeclaration declaration) { + final group = (declaration.isGetter || declaration.isSetter) + ? GetSetMemberGroup.parse(declaration) + : MethodMemberGroup.parse(declaration); + + _addMemberInfo( + classMember: declaration, + parsedGroup: group, + token: declaration.name, + type: declaration.returnType?.type?.getDisplayString() ?? '_', + ); + } + + void _addMemberInfo({ + required ClassMember classMember, + required MemberGroup parsedGroup, + required Token? token, + required String type, + }) { + final closestGroup = _getClosestGroup(parsedGroup); + if (closestGroup == null) return; + + membersInfo.add( + MemberInfo( + classMember: classMember, + memberOrder: _getOrder( + closestGroup, + token?.lexeme ?? '', + type, + ), + ), + ); + } + + MemberGroup? _getClosestGroup(MemberGroup parsedGroup) { + final closestGroups = + (_isFlutterWidget + ? _parameters.widgetsGroupsOrder + : _parameters.groupsOrder) + .where((group) => group.implies(parsedGroup)) + .sorted( + (a, b) => b.getSortingCoefficient() - a.getSortingCoefficient(), + ); + + return closestGroups.firstOrNull; + } + + MemberOrder _getOrder( + MemberGroup memberGroup, + String memberName, + String typeName, + ) { + if (membersInfo.isEmpty) { + return MemberOrder( + memberNames: MemberNames( + currentName: memberName, + currentTypeName: typeName, + ), + isAlphabeticallyWrong: false, + isByTypeWrong: false, + memberGroup: memberGroup, + isWrong: false, + ); + } + + final lastMemberOrder = membersInfo.last.memberOrder; + final hasSameGroup = lastMemberOrder.memberGroup == memberGroup; + + final previousMemberGroup = + hasSameGroup && lastMemberOrder.previousMemberGroup != null + ? lastMemberOrder.previousMemberGroup + : lastMemberOrder.memberGroup; + + final memberNames = MemberNames( + currentName: memberName, + previousName: lastMemberOrder.memberNames.currentName, + currentTypeName: typeName, + previousTypeName: lastMemberOrder.memberNames.currentTypeName, + ); + + return MemberOrder( + memberNames: memberNames, + isAlphabeticallyWrong: + hasSameGroup && + memberNames.currentName.compareTo(memberNames.previousName!) < 0, + isByTypeWrong: + hasSameGroup && + memberNames.currentTypeName.toLowerCase().compareTo( + memberNames.previousTypeName!.toLowerCase(), + ) < + 0, + memberGroup: memberGroup, + previousMemberGroup: previousMemberGroup, + isWrong: + (hasSameGroup && lastMemberOrder.isWrong) || + _isCurrentGroupBefore( + lastMemberOrder.memberGroup, + memberGroup, + ), + ); + } + + bool _isCurrentGroupBefore( + MemberGroup lastMemberGroup, + MemberGroup memberGroup, + ) { + final group = _isFlutterWidget + ? _parameters.widgetsGroupsOrder + : _parameters.groupsOrder; + + return group.indexOf(lastMemberGroup) > group.indexOf(memberGroup); + } +} diff --git a/lib/src/lints/member_ordering/visitors/member_ordering_reporter.dart b/lib/src/lints/member_ordering/visitors/member_ordering_reporter.dart new file mode 100644 index 00000000..27be61e4 --- /dev/null +++ b/lib/src/lints/member_ordering/visitors/member_ordering_reporter.dart @@ -0,0 +1,96 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/member_ordering/member_ordering_rule.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_info.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_order.dart'; + +/// Reporter for member ordering diagnostics. +class MemberOrderingReporter { + final MemberOrderingRule _rule; + final List _membersInfo; + + /// Creates instance of [MemberOrderingReporter]. + const MemberOrderingReporter({ + required List membersInfo, + required MemberOrderingRule rule, + }) : _membersInfo = membersInfo, + _rule = rule; + + /// Generates diagnostic reports based on the configuration parameters. + void report({ + required bool alphabetize, + required bool alphabetizeByType, + }) { + _reportWrongOrder(); + if (alphabetize) { + _reportAlphabeticalOrder(); + } else if (alphabetizeByType) { + _reportAlphabeticalTypeOrder(); + } + } + + void _reportWrongOrder() => _reportMembers( + (order) => order.isWrong, + MemberOrderingRule.wrongOrderCode, + (order) => [ + order.memberGroup.toString(), + order.previousMemberGroup?.toString() ?? '', + ], + ); + + void _reportAlphabeticalOrder() => _reportMembers( + (order) => order.isAlphabeticallyWrong, + MemberOrderingRule.alphabeticalOrderCode, + (order) => [ + order.memberNames.currentName, + order.memberNames.previousName ?? '', + ], + ); + + void _reportAlphabeticalTypeOrder() => _reportMembers( + (order) => order.isByTypeWrong, + MemberOrderingRule.alphabeticalByTypeOrderCode, + (order) => [ + order.memberNames.currentTypeName, + order.memberNames.previousTypeName ?? '', + ], + ); + + void _reportMembers( + bool Function(MemberOrder) filter, + LintCode code, + List Function(MemberOrder) getArguments, + ) { + final filtered = _membersInfo.where((info) => filter(info.memberOrder)); + + for (final memberInfo in filtered) { + _rule.reportAtNode( + memberInfo.classMember, + diagnosticCode: code, + arguments: getArguments(memberInfo.memberOrder), + ); + } + } +} diff --git a/lib/src/lints/member_ordering/visitors/member_ordering_visitor.dart b/lib/src/lints/member_ordering/visitors/member_ordering_visitor.dart index 6b2035d0..1a4f7eab 100644 --- a/lib/src/lints/member_ordering/visitors/member_ordering_visitor.dart +++ b/lib/src/lints/member_ordering/visitors/member_ordering_visitor.dart @@ -23,334 +23,43 @@ import 'package:analyzer/dart/ast/ast.dart' hide Annotation; import 'package:analyzer/dart/ast/visitor.dart'; -import 'package:collection/collection.dart'; -import 'package:solid_lints/src/lints/member_ordering/models/annotation.dart'; -import 'package:solid_lints/src/lints/member_ordering/models/field_keyword.dart'; -import 'package:solid_lints/src/lints/member_ordering/models/member_group/constructor_member_group.dart'; -import 'package:solid_lints/src/lints/member_ordering/models/member_group/field_member_group.dart'; -import 'package:solid_lints/src/lints/member_ordering/models/member_group/get_set_member_group.dart'; -import 'package:solid_lints/src/lints/member_ordering/models/member_group/member_group.dart'; -import 'package:solid_lints/src/lints/member_ordering/models/member_group/method_member_group.dart'; -import 'package:solid_lints/src/lints/member_ordering/models/member_type.dart'; -import 'package:solid_lints/src/lints/member_ordering/models/modifier.dart'; +import 'package:solid_lints/src/lints/member_ordering/member_ordering_rule.dart'; +import 'package:solid_lints/src/lints/member_ordering/models/member_ordering_parameters.dart'; +import 'package:solid_lints/src/lints/member_ordering/visitors/declaration_ordering_visitor.dart'; +import 'package:solid_lints/src/lints/member_ordering/visitors/member_ordering_reporter.dart'; import 'package:solid_lints/src/utils/types_utils.dart'; /// AST Visitor which finds all class members and checks if they are /// in order provided from rule config or default config -class MemberOrderingVisitor extends RecursiveAstVisitor> { - final List _groupsOrder; - final List _widgetsGroupsOrder; - - final _membersInfo = []; +class MemberOrderingVisitor extends SimpleAstVisitor { + final MemberOrderingRule _rule; + final MemberOrderingParameters _parameters; /// Creates instance of [MemberOrderingVisitor] - /// [_groupsOrder] config is used for regular classes - /// [_widgetsGroupsOrder] config is used for widget classes - MemberOrderingVisitor(this._groupsOrder, this._widgetsGroupsOrder); + MemberOrderingVisitor(this._rule, this._parameters); @override - List visitClassDeclaration(ClassDeclaration node) { + void visitClassDeclaration(ClassDeclaration node) { super.visitClassDeclaration(node); - _membersInfo.clear(); + final body = node.body; + if (body is! BlockClassBody) return; final type = node.extendsClause?.superclass.type; - final isFlutterWidget = - isWidgetOrSubclass(type) || isWidgetStateOrSubclass(type); - - for (final member in node.members) { - if (member is FieldDeclaration) { - _visitFieldDeclaration(member, isFlutterWidget); - } else if (member is ConstructorDeclaration) { - _visitConstructorDeclaration(member, isFlutterWidget); - } else if (member is MethodDeclaration) { - _visitMethodDeclaration(member, isFlutterWidget); - } - } - - return _membersInfo; - } - - void _visitFieldDeclaration( - FieldDeclaration declaration, - bool isFlutterWidget, - ) { - final group = FieldMemberGroup.parse(declaration); - final closestGroup = _getClosestGroup(group, isFlutterWidget); - - if (closestGroup != null) { - _membersInfo.add( - MemberInfo( - classMember: declaration, - memberOrder: _getOrder( - closestGroup, - declaration.fields.variables.first.name.lexeme, - declaration.fields.type?.type?.getDisplayString() ?? '_', - isFlutterWidget, - ), - ), - ); - } - } - - void _visitConstructorDeclaration( - ConstructorDeclaration declaration, - bool isFlutterWidget, - ) { - final group = ConstructorMemberGroup.parse(declaration); - final closestGroup = _getClosestGroup(group, isFlutterWidget); - - if (closestGroup != null) { - _membersInfo.add( - MemberInfo( - classMember: declaration, - memberOrder: _getOrder( - closestGroup, - declaration.name?.lexeme ?? '', - declaration.returnType.name, - isFlutterWidget, - ), - ), - ); - } - } - - void _visitMethodDeclaration( - MethodDeclaration declaration, - bool isFlutterWidget, - ) { - if (declaration.isGetter || declaration.isSetter) { - final group = GetSetMemberGroup.parse(declaration); - final closestGroup = _getClosestGroup(group, isFlutterWidget); - - if (closestGroup != null) { - _membersInfo.add( - MemberInfo( - classMember: declaration, - memberOrder: _getOrder( - closestGroup, - declaration.name.lexeme, - declaration.returnType?.type?.getDisplayString() ?? '_', - isFlutterWidget, - ), - ), - ); - } - } else { - final group = MethodMemberGroup.parse(declaration); - final closestGroup = _getClosestGroup(group, isFlutterWidget); - - if (closestGroup != null) { - _membersInfo.add( - MemberInfo( - classMember: declaration, - memberOrder: _getOrder( - closestGroup, - declaration.name.lexeme, - declaration.returnType?.type?.getDisplayString() ?? '_', - isFlutterWidget, - ), - ), - ); - } - } - } - - MemberGroup? _getClosestGroup( - MemberGroup parsedGroup, - bool isFlutterWidget, - ) { - final closestGroups = (isFlutterWidget ? _widgetsGroupsOrder : _groupsOrder) - .where( - (group) => - _isConstructorGroup(group, parsedGroup) || - _isFieldGroup(group, parsedGroup) || - _isGetSetGroup(group, parsedGroup) || - _isMethodGroup(group, parsedGroup), - ) - .sorted( - (a, b) => b.getSortingCoefficient() - a.getSortingCoefficient(), - ); - - return closestGroups.firstOrNull; - } - - MemberOrder _getOrder( - MemberGroup memberGroup, - String memberName, - String typeName, - bool isFlutterWidget, - ) { - if (_membersInfo.isNotEmpty) { - final lastMemberOrder = _membersInfo.last.memberOrder; - final hasSameGroup = lastMemberOrder.memberGroup == memberGroup; - - final previousMemberGroup = - hasSameGroup && lastMemberOrder.previousMemberGroup != null - ? lastMemberOrder.previousMemberGroup - : lastMemberOrder.memberGroup; - - final memberNames = MemberNames( - currentName: memberName, - previousName: lastMemberOrder.memberNames.currentName, - currentTypeName: typeName, - previousTypeName: lastMemberOrder.memberNames.currentTypeName, - ); - - return MemberOrder( - memberNames: memberNames, - isAlphabeticallyWrong: hasSameGroup && - memberNames.currentName.compareTo(memberNames.previousName!) < 0, - isByTypeWrong: hasSameGroup && - memberNames.currentTypeName - .toLowerCase() - .compareTo(memberNames.previousTypeName!.toLowerCase()) < - 0, - memberGroup: memberGroup, - previousMemberGroup: previousMemberGroup, - isWrong: (hasSameGroup && lastMemberOrder.isWrong) || - _isCurrentGroupBefore( - lastMemberOrder.memberGroup, - memberGroup, - isFlutterWidget, - ), - ); - } - - return MemberOrder( - memberNames: - MemberNames(currentName: memberName, currentTypeName: typeName), - isAlphabeticallyWrong: false, - isByTypeWrong: false, - memberGroup: memberGroup, - isWrong: false, + final declarationVisitor = DeclarationOrderingVisitor( + parameters: _parameters, + isFlutterWidget: + isWidgetOrSubclass(type) || isWidgetStateOrSubclass(type), ); - } - bool _isCurrentGroupBefore( - MemberGroup lastMemberGroup, - MemberGroup memberGroup, - bool isFlutterWidget, - ) { - final group = isFlutterWidget ? _widgetsGroupsOrder : _groupsOrder; + body.members.forEach(declarationVisitor.visit); - return group.indexOf(lastMemberGroup) > group.indexOf(memberGroup); + MemberOrderingReporter( + membersInfo: declarationVisitor.membersInfo, + rule: _rule, + ).report( + alphabetize: _parameters.alphabetize, + alphabetizeByType: _parameters.alphabetizeByType, + ); } - - bool _isConstructorGroup(MemberGroup group, MemberGroup parsedGroup) => - group is ConstructorMemberGroup && - parsedGroup is ConstructorMemberGroup && - (!group.isFactory || group.isFactory == parsedGroup.isFactory) && - (!group.isNamed || group.isNamed == parsedGroup.isNamed) && - (group.modifier == Modifier.unset || - group.modifier == parsedGroup.modifier) && - (group.annotation == Annotation.unset || - group.annotation == parsedGroup.annotation); - - bool _isMethodGroup(MemberGroup group, MemberGroup parsedGroup) => - group is MethodMemberGroup && - parsedGroup is MethodMemberGroup && - (!group.isStatic || group.isStatic == parsedGroup.isStatic) && - (!group.isNullable || group.isNullable == parsedGroup.isNullable) && - (group.name == null || group.name == parsedGroup.name) && - (group.modifier == Modifier.unset || - group.modifier == parsedGroup.modifier) && - (group.annotation == Annotation.unset || - group.annotation == parsedGroup.annotation); - - bool _isGetSetGroup(MemberGroup group, MemberGroup parsedGroup) => - group is GetSetMemberGroup && - parsedGroup is GetSetMemberGroup && - (group.memberType == parsedGroup.memberType || - (group.memberType == MemberType.getterAndSetter && - (parsedGroup.memberType == MemberType.getter || - parsedGroup.memberType == MemberType.setter))) && - (!group.isStatic || group.isStatic == parsedGroup.isStatic) && - (!group.isNullable || group.isNullable == parsedGroup.isNullable) && - (group.modifier == Modifier.unset || - group.modifier == parsedGroup.modifier) && - (group.annotation == Annotation.unset || - group.annotation == parsedGroup.annotation); - - bool _isFieldGroup(MemberGroup group, MemberGroup parsedGroup) => - group is FieldMemberGroup && - parsedGroup is FieldMemberGroup && - (!group.isLate || group.isLate == parsedGroup.isLate) && - (!group.isStatic || group.isStatic == parsedGroup.isStatic) && - (!group.isNullable || group.isNullable == parsedGroup.isNullable) && - (group.modifier == Modifier.unset || - group.modifier == parsedGroup.modifier) && - (group.keyword == FieldKeyword.unset || - group.keyword == parsedGroup.keyword) && - (group.annotation == Annotation.unset || - group.annotation == parsedGroup.annotation); -} - -/// Data class that holds AST class member and it's order info -class MemberInfo { - /// AST instance of an [ClassMember] - final ClassMember classMember; - - /// Class member order info - final MemberOrder memberOrder; - - /// Creates instance of an [MemberInfo] - const MemberInfo({ - required this.classMember, - required this.memberOrder, - }); -} - -/// Data class holds information about class member order info -class MemberOrder { - /// Indicates if order is wrong - final bool isWrong; - - /// Indicates if order is wrong alphabetically - final bool isAlphabeticallyWrong; - - /// Indicates if order is wrong alphabetically by type - final bool isByTypeWrong; - - /// Info about current and previous class member name - final MemberNames memberNames; - - /// Info about current member member group - final MemberGroup memberGroup; - - /// Info about previous member member group - final MemberGroup? previousMemberGroup; - - /// Creates instance of [MemberOrder] - const MemberOrder({ - required this.isWrong, - required this.isAlphabeticallyWrong, - required this.isByTypeWrong, - required this.memberNames, - required this.memberGroup, - this.previousMemberGroup, - }); -} - -/// Data class contains info about current and previous class member names -class MemberNames { - /// Name of current class member - final String currentName; - - /// Name of previous class member - final String? previousName; - - /// Type name of current class member - final String currentTypeName; - - /// Type name of previous class member - final String? previousTypeName; - - /// Crates instance of [MemberNames] - const MemberNames({ - required this.currentName, - required this.currentTypeName, - this.previousName, - this.previousTypeName, - }); } diff --git a/lib/src/models/rule_parameters_parser.dart b/lib/src/models/rule_parameters_parser.dart new file mode 100644 index 00000000..90d6b11e --- /dev/null +++ b/lib/src/models/rule_parameters_parser.dart @@ -0,0 +1,2 @@ +/// A function that parses the rule parameters from analysis options json. +typedef RuleParametersParser = T Function(Map); diff --git a/lib/src/models/solid_lint_rule.dart b/lib/src/models/solid_lint_rule.dart index 9f5bee56..b4bf288e 100644 --- a/lib/src/models/solid_lint_rule.dart +++ b/lib/src/models/solid_lint_rule.dart @@ -1,9 +1,7 @@ import 'package:analyzer/analysis_rule/analysis_rule.dart'; import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; - -/// A function that parses the rule parameters from analysis options json -typedef RuleParametersParser = T Function(Map); +import 'package:solid_lints/src/models/rule_parameters_parser.dart'; /// A base class for emitting information about /// issues with user's `.dart` files. diff --git a/lib/src/models/solid_multi_lint_rule.dart b/lib/src/models/solid_multi_lint_rule.dart new file mode 100644 index 00000000..244c9377 --- /dev/null +++ b/lib/src/models/solid_multi_lint_rule.dart @@ -0,0 +1,37 @@ +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/models/rule_parameters_parser.dart'; + +/// A base class for lint rules that report multiple diagnostic codes +/// and require configuration parameters from analysis options. +/// +/// Mirrors SolidLintRule but extends MultiAnalysisRule instead of +/// AnalysisRule, allowing rules to define multiple diagnostic codes. +abstract class SolidMultiLintRule + extends MultiAnalysisRule { + final AnalysisOptionsLoader _analysisOptionsLoader; + + final RuleParametersParser _parametersParser; + + /// Constructor for [SolidMultiLintRule] model with parameters. + SolidMultiLintRule({ + required AnalysisOptionsLoader analysisOptionsLoader, + required RuleParametersParser parametersParser, + required super.name, + required super.description, + super.state, + }) : _analysisOptionsLoader = analysisOptionsLoader, + _parametersParser = parametersParser; + + /// Reads the rule parameters from analysis options and parses them to [T]. + T? getParametersForContext(RuleContext context) { + _analysisOptionsLoader.loadRulesOptionsFromContext(context); + + final unparsedParameters = + _analysisOptionsLoader.getRuleOptions(context, name); + if (unparsedParameters == null) return null; + + return _parametersParser(unparsedParameters); + } +} diff --git a/lib/src/utils/implies.dart b/lib/src/utils/implies.dart new file mode 100644 index 00000000..94cf2617 --- /dev/null +++ b/lib/src/utils/implies.dart @@ -0,0 +1,19 @@ +/// Sugar syntax helper to write extensions analogous to bool implies. +/// +/// A truthy (!= [falsy]) value of [a] implies an equal value of [b]. +bool objectImplies(T a, T b, T falsy) => a == falsy || a == b; + +/// Logical implication operation for booleans. +extension BoolImplies on bool { + /// Logical implication operation. + /// + /// True implies that [other] is also true. + // ignore: avoid_positional_boolean_parameters + bool implies(bool other) => objectImplies(this, other, false); +} + +/// Logical implication operation for strings. +extension StringImplies on String? { + /// Logical implication operation. + bool implies(String? other) => objectImplies(this, other, null); +} diff --git a/lint_test/member_ordering_test.dart b/lint_test/member_ordering_test.dart deleted file mode 100644 index 3faf21f0..00000000 --- a/lint_test/member_ordering_test.dart +++ /dev/null @@ -1,232 +0,0 @@ -// ignore_for_file: unused_field, prefer_match_file_name, proper_super_calls -// ignore_for_file: unused_element -// ignore_for_file: no_empty_block - -import 'package:flutter/widgets.dart'; - -/// Check the `member_ordering` rule - -class AlphabeticalClass { - final b = 1; - - // expect_lint: member_ordering - final a = 1; - final c = 1; - - void bStuff() {} - - // expect_lint: member_ordering - void aStuff() {} - - void cStuff() {} - - void visitStatement() {} - - // expect_lint: member_ordering - void visitStanford() {} -} - -class CorrectOrder { - final publicField = 1; - int _privateField = 2; - - CorrectOrder(); - - int get privateFieldGetter => _privateField; - - void set privateFieldSetter(int value) { - _privateField = value; - } - - void publicDoStuff() {} - - void _privateDoStuff() {} - - void close() {} -} - -class WrongOrder { - void close() {} - - // expect_lint: member_ordering - void _privateDoStuff() {} - - // expect_lint: member_ordering - void publicDoStuff() {} - - // expect_lint: member_ordering - void set privateFieldSetter(int value) { - _privateField = value; - } - - // expect_lint: member_ordering - int get privateFieldGetter => _privateField; - - // expect_lint: member_ordering - WrongOrder(); - - // expect_lint: member_ordering - int _privateField = 2; - - // expect_lint: member_ordering - final publicField = 1; -} - -class PartiallyWrongOrder { - final publicField = 1; - - PartiallyWrongOrder(); - - // expect_lint: member_ordering - int _privateField = 2; - - int get privateFieldGetter => _privateField; - - void set privateFieldSetter(int value) { - _privateField = value; - } - - void _privateDoStuff() {} - - // expect_lint: member_ordering - void publicDoStuff() {} - - void close() {} -} - -class CorrectWidget extends StatefulWidget { - @override - State createState() => _CorrectWidgetState(); - - const CorrectWidget({super.key}); -} - -class _CorrectWidgetState extends State { - static const constField = 1; - static final staticField = 1; - - static void staticDoStuff() {} - - final publicField = 1; - final _privateField = 1; - - void publicDoStuff() {} - - void _privateDoStuff() {} - - _CorrectWidgetState(); - - @override - Widget build(BuildContext context) => throw UnimplementedError(); - - @override - void initState() => super.initState(); - - @override - void didChangeDependencies() => super.didChangeDependencies(); - - @override - void didUpdateWidget(covariant CorrectWidget oldWidget) => - super.didUpdateWidget(oldWidget); - - @override - void dispose() => super.dispose(); -} - -class WrongWidget extends StatefulWidget { - const WrongWidget({super.key}); - - // expect_lint: member_ordering - @override - State createState() => _WrongWidgetState(); -} - -class _WrongWidgetState extends State { - @override - void dispose() => super.dispose(); - - // expect_lint: member_ordering - @override - void didUpdateWidget(covariant WrongWidget oldWidget) => - super.didUpdateWidget(oldWidget); - - // expect_lint: member_ordering - @override - void didChangeDependencies() => super.didChangeDependencies(); - - // expect_lint: member_ordering - @override - void initState() => super.initState(); - - // expect_lint: member_ordering - @override - Widget build(BuildContext context) => throw UnimplementedError(); - - // expect_lint: member_ordering - _WrongWidgetState(); - - // expect_lint: member_ordering - void _privateDoStuff() {} - - // expect_lint: member_ordering - void publicDoStuff() {} - - // expect_lint: member_ordering - final _privateField = 1; - - // expect_lint: member_ordering - final publicField = 1; - - // expect_lint: member_ordering - static void staticDoStuff() {} - - // expect_lint: member_ordering - static final staticField = 1; - - // expect_lint: member_ordering - static const constField = 1; -} - -class PartiallyCorrectWidget extends StatefulWidget { - @override - State createState() => _PartiallyCorrectWidgetState(); - - const PartiallyCorrectWidget({super.key}); -} - -class _PartiallyCorrectWidgetState extends State { - static final staticField = 1; - - // expect_lint: member_ordering - static const constField = 1; - - static void staticDoStuff() {} - - final _privateField = 1; - - // expect_lint: member_ordering - final publicField = 1; - - void publicDoStuff() {} - - void _privateDoStuff() {} - - _PartiallyCorrectWidgetState(); - - @override - Widget build(BuildContext context) => throw UnimplementedError(); - - @override - void didChangeDependencies() => super.didChangeDependencies(); - - // expect_lint: member_ordering - @override - void initState() => super.initState(); - - @override - void didUpdateWidget(covariant PartiallyCorrectWidget oldWidget) => - super.didUpdateWidget(oldWidget); - - @override - void dispose() => super.dispose(); -} diff --git a/test/src/lints/member_ordering/member_ordering_rule_test.dart b/test/src/lints/member_ordering/member_ordering_rule_test.dart new file mode 100644 index 00000000..6617d460 --- /dev/null +++ b/test/src/lints/member_ordering/member_ordering_rule_test.dart @@ -0,0 +1,620 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/member_ordering/member_ordering_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(MemberOrderingRuleTest); + }); +} + +@reflectiveTest +class MemberOrderingRuleTest extends AnalysisRuleTest with AutoTestLintOffsets { + static const _defaultAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + member_ordering: {} + '''; + + static const _mockAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + member_ordering: + alphabetize: true + order: + - public_fields + - private_fields + - constructors + - getters + - setters + - public_methods + - private_methods + - close_method + widgets_order: + - const_fields + - static_fields + - static_methods + - public_fields + - private_fields + - public_methods + - private_methods + - constructors + - build_method + - init_state_method + - did_change_dependencies_method + - did_update_widget_method + - dispose_method + '''; + + static const _alphabetizeAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + member_ordering: + alphabetize: true + alphabetize_by_type: true + '''; + + static const _customOrderAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + member_ordering: + order: + - public_methods + - public_fields + '''; + + static const _customWidgetsOrderAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + member_ordering: + widgets_order: + - build_method + - init_state_method + '''; + + static const _factoryOrderAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + member_ordering: + order: + - constructors + - factory_constructors + - public_fields + '''; + + @override + void setUp() { + final flutter = newPackage('flutter'); + flutter.addFile('lib/src/widgets/framework.dart', r''' +class BuildContext {} +class Key {} +abstract class Widget { + const Widget({Key? key}); +} +abstract class StatefulWidget extends Widget { + const StatefulWidget({super.key}); + State createState(); +} +abstract class State { + void initState() {} + void didChangeDependencies() {} + void didUpdateWidget(T oldWidget) {} + void dispose() {} + Widget build(BuildContext context) => throw 0; +} +'''); + + rule = MemberOrderingRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_mockAnalysisOptionsContent), + ); + } + + @override + String get analysisRule => MemberOrderingRule.lintName; + + String _configWith(String pluginConfig) => + '''${analysisOptionsContent(rules: [rule.name])} +$pluginConfig'''; + + Future test_does_not_report_on_correct_order() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_defaultAnalysisOptionsContent), + ); + await assertNoDiagnostics(r''' +class CorrectOrder { + final publicField = 1; + + int get privateFieldGetter => 1; + + CorrectOrder(); + + void publicDoStuff() {} +} +'''); + } + + Future test_does_not_report_on_empty_class() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_defaultAnalysisOptionsContent), + ); + await assertNoDiagnostics(r''' +class EmptyClass {} +'''); + } + + Future test_does_not_report_on_single_member_class() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_defaultAnalysisOptionsContent), + ); + await assertNoDiagnostics(r''' +class SingleMember { + final a = 1; +} +'''); + } + + Future test_reports_on_wrong_order() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_defaultAnalysisOptionsContent), + ); + await assertAutoDiagnostics(''' +class WrongOrder { + void publicDoStuff() {} + ${expectLint('WrongOrder();')} +} +'''); + } + + Future + test_does_not_report_on_non_alphabetical_order_by_default() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_defaultAnalysisOptionsContent), + ); + await assertNoDiagnostics(r''' +class NonAlphabetical { + final b = 1; + final a = 1; +} +'''); + } + + Future test_does_not_report_on_correct_widget_order() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_defaultAnalysisOptionsContent), + ); + await assertNoDiagnostics(r''' +import 'package:flutter/src/widgets/framework.dart'; + +class CorrectWidgetState extends State { + CorrectWidgetState(); + + final finalField = 1; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } +} +'''); + } + + Future test_reports_on_wrong_widget_order() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_defaultAnalysisOptionsContent), + ); + await assertAutoDiagnostics(''' +import 'package:flutter/src/widgets/framework.dart'; + +class WrongWidgetState extends State { + @override + void initState() { + super.initState(); + } + + ${expectLint('WrongWidgetState();')} +} +'''); + } + + Future test_reports_on_non_alphabetical_order() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_alphabetizeAnalysisOptionsContent), + ); + + await assertAutoDiagnostics(''' +class AlphabeticalClass { + final b = 1; + ${expectLint('final a = 1;')} +} +'''); + } + + Future test_reports_on_non_alphabetical_by_type_order() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_alphabetizeAnalysisOptionsContent), + ); + + await assertAutoDiagnostics(''' +class AlphabeticalByTypeClass { + int b = 1; + ${expectLint('double a = 1.0;')} +} +'''); + } + + Future test_does_not_report_on_correct_custom_order() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_customOrderAnalysisOptionsContent), + ); + + await assertNoDiagnostics(r''' +class CorrectOrder { + void publicDoStuff() {} + final publicField = 1; +} +'''); + } + + Future test_reports_on_wrong_custom_order() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_customOrderAnalysisOptionsContent), + ); + + await assertAutoDiagnostics(''' +class WrongOrder { + final publicField = 1; + ${expectLint('void publicDoStuff() {}')} +} +'''); + } + + Future test_does_not_report_on_correct_custom_widgets_order() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_customWidgetsOrderAnalysisOptionsContent), + ); + + await assertNoDiagnostics(r''' +import 'package:flutter/src/widgets/framework.dart'; + +class CorrectWidgetState extends State { + @override + Widget build(BuildContext context) => throw 0; + + @override + void initState() { + super.initState(); + } +} +'''); + } + + Future test_reports_on_wrong_custom_widgets_order() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_customWidgetsOrderAnalysisOptionsContent), + ); + + await assertAutoDiagnostics(''' +import 'package:flutter/src/widgets/framework.dart'; + +class WrongWidgetState extends State { + @override + void initState() { + super.initState(); + } + + ${expectLint('@override Widget build(BuildContext context) => throw 0;')} +} +'''); + } + + Future test_reports_on_non_alphabetical_fields_and_methods_order() async { + await assertAutoDiagnostics(''' +class AlphabeticalClass { + final b = 1; + + ${expectLint('final a = 1;')} + final c = 1; + + void bStuff() {} + + ${expectLint('void aStuff() {}')} + + void cStuff() {} + + void visitStatement() {} + + ${expectLint('void visitStanford() {}')} +} +'''); + } + + Future test_reports_on_named_method_wrong_order() async { + await assertAutoDiagnostics(''' +class NamedMethodOrder { + void close() {} + + ${expectLint('void publicDoStuff() {}')} +} +'''); + } + + Future test_does_not_report_on_named_method_correct_order() async { + await assertNoDiagnostics(''' +class NamedMethodOrder { + void publicDoStuff() {} + + void close() {} +} +'''); + } + + Future test_does_not_report_on_correct_factory_order() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_factoryOrderAnalysisOptionsContent), + ); + await assertNoDiagnostics(r''' +class FactoryOrder { + FactoryOrder(); + + factory FactoryOrder.create() => FactoryOrder(); + + final field = 1; +} +'''); + } + + Future test_reports_on_wrong_factory_order() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + _configWith(_factoryOrderAnalysisOptionsContent), + ); + await assertAutoDiagnostics(''' +class FactoryOrder { + factory FactoryOrder.create() => FactoryOrder(); + + ${expectLint('FactoryOrder();')} + + final field = 1; +} +'''); + } + + Future test_does_not_report_on_correct_order_with_custom_config() async { + await assertNoDiagnostics(''' +class CorrectOrder { + final publicField = 1; + int _privateField = 2; + + CorrectOrder(); + + int get privateFieldGetter => _privateField; + + void set privateFieldSetter(int value) { + _privateField = value; + } + + void publicDoStuff() {} + + void _privateDoStuff() {} + + void close() {} +} +'''); + } + + Future test_reports_on_wrong_order_with_custom_config() async { + await assertAutoDiagnostics(''' +class WrongOrder { + void close() {} + + ${expectLint('void _privateDoStuff() {}')} + + ${expectLint('void publicDoStuff() {}')} + + ${expectLint('void set privateFieldSetter(int value) { _privateField = value; }')} + + ${expectLint('int get privateFieldGetter => _privateField;')} + + ${expectLint('WrongOrder();')} + + ${expectLint('int _privateField = 2;')} + + ${expectLint('final publicField = 1;')} +} +'''); + } + + Future test_reports_on_partially_wrong_order_with_custom_config() async { + await assertAutoDiagnostics(''' +class PartiallyWrongOrder { + final publicField = 1; + + PartiallyWrongOrder(); + + ${expectLint('int _privateField = 2;')} + + int get privateFieldGetter => _privateField; + + void set privateFieldSetter(int value) { + _privateField = value; + } + + void _privateDoStuff() {} + + ${expectLint('void publicDoStuff() {}')} + + void close() {} +} +'''); + } + + Future test_does_not_report_on_correct_widget_order_with_custom_config() async { + await assertNoDiagnostics(''' +import 'package:flutter/src/widgets/framework.dart'; + +class CorrectWidget extends StatefulWidget { + @override + State createState() => _CorrectWidgetState(); + + const CorrectWidget({super.key}); +} + +class _CorrectWidgetState extends State { + static const constField = 1; + static final staticField = 1; + + static void staticDoStuff() {} + + final publicField = 1; + final _privateField = 1; + + void publicDoStuff() {} + + void _privateDoStuff() {} + + _CorrectWidgetState(); + + @override + Widget build(BuildContext context) => throw 0; + + @override + void initState() => super.initState(); + + @override + void didChangeDependencies() => super.didChangeDependencies(); + + @override + void didUpdateWidget(covariant CorrectWidget oldWidget) => + super.didUpdateWidget(oldWidget); + + @override + void dispose() => super.dispose(); +} +'''); + } + + Future test_reports_on_wrong_widget_order_with_custom_config() async { + await assertAutoDiagnostics(''' +import 'package:flutter/src/widgets/framework.dart'; + +class WrongWidget extends StatefulWidget { + const WrongWidget({super.key}); + + ${expectLint('@override State createState() => _WrongWidgetState();')} +} + +class _WrongWidgetState extends State { + @override + void dispose() => super.dispose(); + + ${expectLint('@override void didUpdateWidget(covariant WrongWidget oldWidget) => super.didUpdateWidget(oldWidget);')} + + ${expectLint('@override void didChangeDependencies() => super.didChangeDependencies();')} + + ${expectLint('@override void initState() => super.initState();')} + + ${expectLint('@override Widget build(BuildContext context) => throw 0;')} + + ${expectLint('_WrongWidgetState();')} + + ${expectLint('void _privateDoStuff() {}')} + + ${expectLint('void publicDoStuff() {}')} + + ${expectLint('final _privateField = 1;')} + + ${expectLint('final publicField = 1;')} + + ${expectLint('static void staticDoStuff() {}')} + + ${expectLint('static final staticField = 1;')} + + ${expectLint('static const constField = 1;')} +} +'''); + } + + Future test_reports_on_partially_correct_widget_order_with_custom_config() async { + await assertAutoDiagnostics(''' +import 'package:flutter/src/widgets/framework.dart'; + +class PartiallyCorrectWidget extends StatefulWidget { + @override + State createState() => _PartiallyCorrectWidgetState(); + + const PartiallyCorrectWidget({super.key}); +} + +class _PartiallyCorrectWidgetState extends State { + static final staticField = 1; + + ${expectLint('static const constField = 1;')} + + static void staticDoStuff() {} + + final _privateField = 1; + + ${expectLint('final publicField = 1;')} + + void publicDoStuff() {} + + void _privateDoStuff() {} + + _PartiallyCorrectWidgetState(); + + @override + Widget build(BuildContext context) => throw 0; + + @override + void didChangeDependencies() => super.didChangeDependencies(); + + ${expectLint('@override void initState() => super.initState();')} + + @override + void didUpdateWidget(covariant PartiallyCorrectWidget oldWidget) => + super.didUpdateWidget(oldWidget); + + @override + void dispose() => super.dispose(); +} +'''); + } +} From e2db38756ea18f67bb9de5d9d2cae3545153f553 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 1 Jul 2026 14:46:14 +0300 Subject: [PATCH 32/49] refactor: migrate named_parameters_ordering (#298) * refactor: migrate named_parameters_ordering * refactor: improve named parameters ordering fix to preserve trailing comments and fix configuration parsing logic * fix: prevent incorrect parameter reordering when comments are present and ensure unique parameter types in config parser * fix: improve trailing comment detection by iterating through the entire comment chain in named parameters fix * refactor: extract correction and iterable utilities and optimize named parameters ordering fix logic * style: fix doc comment line length in correction_utils.dart * refactor: simplify pairwise implementation to use indexed element access * refactor: extract comment handling logic to correction utilities --------- Co-authored-by: Illia Aihistov --- lib/main.dart | 8 + .../analysis_options_loader.dart | 16 + .../config_parser.dart | 27 +- .../fixes/named_parameters_ordering_fix.dart | 195 ++++++++++ .../models/parameter_type.dart | 43 +++ .../named_parameters_ordering_rule.dart | 94 ++--- .../named_parameters_ordering_visitor.dart | 94 ++--- lib/src/utils/correction_utils.dart | 71 ++++ lib/src/utils/iterable_utils.dart | 11 + .../named_parameters_ordering_rule_test.dart | 352 ++++++++++++++++++ 10 files changed, 778 insertions(+), 133 deletions(-) create mode 100644 lib/src/lints/named_parameters_ordering/fixes/named_parameters_ordering_fix.dart create mode 100644 lib/src/utils/correction_utils.dart create mode 100644 lib/src/utils/iterable_utils.dart create mode 100644 test/src/lints/named_parameters_ordering/named_parameters_ordering_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index feae0050..df0f54cc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,8 @@ import 'package:solid_lints/src/lints/double_literal_format/double_literal_forma import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart'; import 'package:solid_lints/src/lints/member_ordering/member_ordering_rule.dart'; +import 'package:solid_lints/src/lints/named_parameters_ordering/fixes/named_parameters_ordering_fix.dart'; +import 'package:solid_lints/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart'; import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; import 'package:solid_lints/src/lints/no_magic_number/no_magic_number_rule.dart'; import 'package:solid_lints/src/lints/number_of_parameters/number_of_parameters_rule.dart'; @@ -58,6 +60,7 @@ class SolidLintsPlugin extends Plugin { AvoidDebugPrintInReleaseRule(), doubleLiteralFormatRule, ProperSuperCallsRule(), + NamedParametersOrderingRule(analysisOptionsLoader: analysisLoader), AvoidReturningWidgetsRule(analysisOptionsLoader: analysisLoader), MemberOrderingRule(analysisOptionsLoader: analysisLoader), AvoidUnusedParametersRule(analysisOptionsLoader: analysisLoader), @@ -106,5 +109,10 @@ class SolidLintsPlugin extends Plugin { preferLastRule.diagnosticCode, PreferLastFix.new, ); + + registry.registerFixForRule( + NamedParametersOrderingRule.code, + NamedParametersOrderingFix.new, + ); } } diff --git a/lib/src/common/parameter_parser/analysis_options_loader.dart b/lib/src/common/parameter_parser/analysis_options_loader.dart index ab22ecc9..f4fb74e8 100644 --- a/lib/src/common/parameter_parser/analysis_options_loader.dart +++ b/lib/src/common/parameter_parser/analysis_options_loader.dart @@ -21,6 +21,22 @@ class AnalysisOptionsLoader { (path) => _rulesCache[path]?.rules[ruleName], ); + /// Gets the options for a specific rule by looking up the nearest + /// `analysis_options.yaml` from the given [filePath]'s directory. + /// + /// Unlike [getRuleOptions], this method does not require a [RuleContext] + /// and can be used from quick fixes. + Map? getRuleOptionsForFile( + String filePath, + String ruleName, + ) { + final dirPath = _resourceProvider.pathContext.dirname(filePath); + final yamlPath = _findNearestAnalysisOptionsFilePath(dirPath); + if (yamlPath == null) return null; + _loadRulesOptionsIfNewer(yamlPath); + return _rulesCache[yamlPath]?.rules[ruleName]; + } + /// Loads lint rules from the analysis options file for all rules /// using the provided [RuleContext]. void loadRulesOptionsFromContext(RuleContext context) => diff --git a/lib/src/lints/named_parameters_ordering/config_parser.dart b/lib/src/lints/named_parameters_ordering/config_parser.dart index 5c5f9570..52d94922 100644 --- a/lib/src/lints/named_parameters_ordering/config_parser.dart +++ b/lib/src/lints/named_parameters_ordering/config_parser.dart @@ -23,22 +23,23 @@ import 'package:solid_lints/src/lints/named_parameters_ordering/models/parameter_type.dart'; -/// Helper class to parse member_ordering rule config +/// Helper class to parse named_parameters_ordering rule config class NamedParametersConfigParser { - static const _defaultOrderList = [ - 'required_super', - 'super', - 'required', - 'nullable', - 'default', - ]; - /// Parse rule config for regular class order rules static List parseOrder(Object? orderConfig) { - final order = orderConfig is Iterable - ? List.from(orderConfig) - : _defaultOrderList; + if (orderConfig is! Iterable) { + return ParameterType.defaultOrder; + } - return order.map(ParameterType.fromType).nonNulls.toList(); + final parsed = orderConfig + .whereType() + .map(ParameterType.fromType) + .nonNulls + .toSet() + .toList(); + final missing = ParameterType.defaultOrder.where( + (type) => !parsed.contains(type), + ); + return [...parsed, ...missing]; } } diff --git a/lib/src/lints/named_parameters_ordering/fixes/named_parameters_ordering_fix.dart b/lib/src/lints/named_parameters_ordering/fixes/named_parameters_ordering_fix.dart new file mode 100644 index 00000000..4349e8d0 --- /dev/null +++ b/lib/src/lints/named_parameters_ordering/fixes/named_parameters_ordering_fix.dart @@ -0,0 +1,195 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; +import 'package:collection/collection.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/named_parameters_ordering/models/named_parameters_ordering_parameters.dart'; +import 'package:solid_lints/src/lints/named_parameters_ordering/models/parameter_type.dart'; +import 'package:solid_lints/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart'; +import 'package:solid_lints/src/utils/correction_utils.dart'; + +/// A parameter block: the offsets of a parameter (including leading comments +/// and indentation) and an optional trailing comment on the same line. +typedef _ParamBlock = ({int blockStart, int blockEnd, String? trailingComment}); + +/// A Quick fix for [NamedParametersOrderingRule] rule. +class NamedParametersOrderingFix extends ResolvedCorrectionProducer { + static const _fixKind = FixKind( + 'solid_lints.fix.named_parameters_ordering', + DartFixKindPriority.standard, + "Sort named parameters", + ); + + /// Creates a new instance of [NamedParametersOrderingFix]. + NamedParametersOrderingFix({required super.context}); + + @override + FixKind get fixKind => _fixKind; + + @override + CorrectionApplicability get applicability => + CorrectionApplicability.automatically; + + @override + Future compute(ChangeBuilder builder) async { + final parameterList = node.thisOrAncestorOfType(); + if (parameterList == null) return; + + final namedParams = parameterList.parameters + .where((p) => p.isNamed) + .toList(); + if (namedParams.length < 2) return; + + final parametersOrder = _getParametersOrder(); + + final sortedNamedParams = namedParams.sortedBy( + (e) => parametersOrder.indexOf(ParameterType.fromParameter(e)), + ); + + // Check if the order is already correct (if sorting changed nothing) + final isChanged = !const ListEquality().equals( + namedParams, + sortedNamedParams, + ); + if (!isChanged) return; + + final isMultiline = utils + .getRangeText(parameterList.sourceRange) + .contains('\n'); + + final hasComments = namedParams.any( + (p) => p.beginToken.precedingComments != null, + ); + + final sourceStart = namedParams.first.offset; + final sourceEnd = namedParams.last.end; + + if (!isMultiline && !hasComments) { + // Single-line: no leading comments, simple text replacement + return builder.addDartFileEdit(file, (builder) { + builder.addSimpleReplacement( + utils.createRange(sourceStart, sourceEnd), + sortedNamedParams + .map((p) => utils.getRangeText(p.sourceRange)) + .join(', '), + ); + }); + } + + // Multiline: extract parameter blocks including leading and trailing + // comments + final blocks = _extractParamBlocks( + namedParams, + parameterList, + ); + + // Map sorted parameters to their corresponding blocks + final sortedBlocks = sortedNamedParams + .map((p) => blocks[namedParams.indexOf(p)]) + .toList(); + + // Determine if original had a trailing comma after the last param + final tokenAfterEnd = namedParams.last.endToken.next; + final hasTrailingComma = tokenAfterEnd?.lexeme == ','; + + // Build replacement text preserving trailing comments + final replacement = sortedBlocks.expandIndexed((i, e) { + final isLast = i == sortedBlocks.length - 1; + final trailingComment = e.trailingComment; + final text = utils.getTextRange(e.blockStart, e.blockEnd); + return [ + text, + if (!isLast || hasTrailingComma) ',', + if (trailingComment != null) ' $trailingComment', + if (!isLast) '\n', + ]; + }).join(); + + // Extend range to include the original trailing comma and any trailing + // comment on the original last parameter's line. + var rangeEnd = hasTrailingComma ? tokenAfterEnd!.end : sourceEnd; + final upperBound = + parameterList.rightDelimiter?.offset ?? + parameterList.rightParenthesis.offset; + if (rangeEnd < upperBound) { + final afterLast = utils.getTextRange(rangeEnd, upperBound); + final newlineIdx = afterLast.indexOf('\n'); + if (newlineIdx != -1) { + rangeEnd += newlineIdx; + } + } + + final firstBlockStart = blocks.first.blockStart; + final targetRange = utils.createRange(firstBlockStart, rangeEnd); + + await builder.addDartFileEdit(file, (builder) { + builder.addSimpleReplacement(targetRange, replacement); + }); + } + + /// Extracts text blocks for each named parameter, including any leading + /// comments that belong to that parameter, and detects trailing comments + /// on the same line. + /// + /// Trailing comments (e.g., `// comment` after a parameter on the same line) + /// are attributed to the parameter they follow, not the next parameter. + List<_ParamBlock> _extractParamBlocks( + List namedParams, + FormalParameterList parameterList, + ) { + final blocks = <_ParamBlock>[]; + + final lowerBound = + parameterList.leftDelimiter?.end ?? parameterList.leftParenthesis.end; + final upperBound = + parameterList.rightDelimiter?.offset ?? + parameterList.rightParenthesis.offset; + + for (int i = 0; i < namedParams.length; i++) { + final param = namedParams[i]; + final int minOffset = i == 0 ? lowerBound : namedParams[i - 1].end; + + final nextParamStart = i < namedParams.length - 1 + ? namedParams[i + 1].offset + : upperBound; + final previousParamEnd = i > 0 ? namedParams[i - 1].end : null; + + final leadingComment = utils.getLeadingComment( + node: param, + previousEnd: previousParamEnd, + ); + final blockStart = utils.getDeclarationStartOffset( + node: param, + leadingComment: leadingComment, + minOffset: minOffset, + ); + final trailingComment = utils.getTrailingComment( + node: param, + nextOffset: nextParamStart, + ); + + blocks.add(( + blockStart: blockStart, + blockEnd: param.end, + trailingComment: trailingComment, + )); + } + + return blocks; + } + + List _getParametersOrder() { + final loader = AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ); + final options = loader.getRuleOptionsForFile( + file, + NamedParametersOrderingRule.lintName, + ); + return options == null + ? ParameterType.defaultOrder + : NamedParametersOrderingParameters.fromJson(options).order; + } +} diff --git a/lib/src/lints/named_parameters_ordering/models/parameter_type.dart b/lib/src/lints/named_parameters_ordering/models/parameter_type.dart index d08901a2..93b29d47 100644 --- a/lib/src/lints/named_parameters_ordering/models/parameter_type.dart +++ b/lib/src/lints/named_parameters_ordering/models/parameter_type.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/dart/ast/ast.dart'; import 'package:collection/collection.dart'; /// Represents a function parameter type @@ -17,11 +18,53 @@ enum ParameterType { /// Default value parameter type (String parameterName = 'defaultValue') defaultValue('default'); + /// The default ordering of parameter types. + static const defaultOrder = [ + ParameterType.requiredInherited, + ParameterType.inherited, + ParameterType.required, + ParameterType.nullable, + ParameterType.defaultValue, + ]; + /// Returns [ParameterType] from type or null if not found static ParameterType? fromType(String type) { return values.firstWhereOrNull((o) => o.type == type); } + /// Classifies a [FormalParameter] into a [ParameterType]. + /// + /// Recursively unwraps [DefaultFormalParameter] wrappers to determine + /// the underlying parameter kind. + static ParameterType fromParameter( + FormalParameter parameter, { + bool hasDefaultValue = false, + }) { + if (parameter is DefaultFormalParameter && + parameter.parameter is! DefaultFormalParameter) { + return fromParameter( + parameter.parameter, + hasDefaultValue: parameter.defaultValue != null, + ); + } + + switch (parameter) { + case SuperFormalParameter(:final isRequired): + return isRequired + ? ParameterType.requiredInherited + : ParameterType.inherited; + + case DefaultFormalParameter(): + case _ when hasDefaultValue: + return ParameterType.defaultValue; + + case FieldFormalParameter(:final isRequired) || + FunctionTypedFormalParameter(:final isRequired) || + SimpleFormalParameter(:final isRequired): + return isRequired ? ParameterType.required : ParameterType.nullable; + } + } + /// String representation of the parameter type final String type; diff --git a/lib/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart b/lib/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart index 845a0c74..110ee5de 100644 --- a/lib/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart +++ b/lib/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart @@ -1,9 +1,9 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/named_parameters_ordering/models/named_parameters_ordering_parameters.dart'; -import 'package:solid_lints/src/lints/named_parameters_ordering/models/parameter_ordering_info.dart'; +import 'package:solid_lints/src/lints/named_parameters_ordering/models/parameter_type.dart'; import 'package:solid_lints/src/lints/named_parameters_ordering/visitors/named_parameters_ordering_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// A lint which allows to enforce a particular named parameter ordering @@ -28,15 +28,16 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Assuming config: /// /// ```yaml -/// custom_lint: -/// rules: -/// - named_parameters_ordering: -/// order: -/// - required -/// - required_super -/// - default -/// - nullable -/// - super +/// plugins: +/// solid_lints: +/// diagnostics: +/// named_parameters_ordering: +/// order: +/// - required +/// - required_super +/// - default +/// - nullable +/// - super /// ``` /// /// #### BAD: @@ -105,54 +106,41 @@ class NamedParametersOrderingRule /// The name of this lint rule. static const lintName = 'named_parameters_ordering'; - late final _visitor = NamedParametersOrderingVisitor(config.parameters.order); + /// The [LintCode] for this rule. + static const code = LintCode( + lintName, + '{0} named parameters should be before {1} named parameters.', + ); - NamedParametersOrderingRule._(super.config); - - /// Creates a new instance of [NamedParametersOrderingRule] - /// based on the lint configuration. - factory NamedParametersOrderingRule.createRule(CustomLintConfigs configs) { - final config = RuleConfig( - configs: configs, - name: lintName, - paramsParser: NamedParametersOrderingParameters.fromJson, - problemMessage: (_) => "Order of named parameter is wrong", - ); + @override + LintCode get diagnosticCode => code; - return NamedParametersOrderingRule._(config); - } + /// Creates a new instance of [NamedParametersOrderingRule]. + NamedParametersOrderingRule({ + required super.analysisOptionsLoader, + }) : super.withParameters( + name: lintName, + description: + 'A lint which allows to enforce a particular named parameter ' + 'ordering conventions.', + parametersParser: NamedParametersOrderingParameters.fromJson, + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addFormalParameterList((node) { - final parametersInfo = _visitor.visitFormalParameterList(node); + super.registerNodeProcessors(registry, context); - final wrongOrderParameters = parametersInfo.where( - (info) => info.parameterOrderingInfo.isWrong, - ); - - for (final parameterInfo in wrongOrderParameters) { - reporter.atNode( - parameterInfo.formalParameter, - _createWrongOrderLintCode(parameterInfo.parameterOrderingInfo), + final parameters = + getParametersForContext(context) ?? + const NamedParametersOrderingParameters( + order: ParameterType.defaultOrder, ); - } - }); - } - LintCode _createWrongOrderLintCode(ParameterOrderingInfo info) { - final parameterOrdering = info.parameterType; - final previousParameterOrdering = info.previousParameterType; + final visitor = NamedParametersOrderingVisitor(this, parameters.order); - return LintCode( - name: lintName, - problemMessage: "${parameterOrdering.displayName} named parameters" - " should be before " - "${previousParameterOrdering!.displayName} named parameters.", - ); + registry.addFormalParameterList(this, visitor); } } diff --git a/lib/src/lints/named_parameters_ordering/visitors/named_parameters_ordering_visitor.dart b/lib/src/lints/named_parameters_ordering/visitors/named_parameters_ordering_visitor.dart index 4ec4598c..44d2e26d 100644 --- a/lib/src/lints/named_parameters_ordering/visitors/named_parameters_ordering_visitor.dart +++ b/lib/src/lints/named_parameters_ordering/visitors/named_parameters_ordering_visitor.dart @@ -21,96 +21,56 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import 'package:analyzer/dart/ast/ast.dart' hide Annotation; +import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; -import 'package:solid_lints/src/lints/named_parameters_ordering/models/parameter_info.dart'; -import 'package:solid_lints/src/lints/named_parameters_ordering/models/parameter_ordering_info.dart'; import 'package:solid_lints/src/lints/named_parameters_ordering/models/parameter_type.dart'; +import 'package:solid_lints/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart'; +import 'package:solid_lints/src/utils/iterable_utils.dart'; /// AST Visitor which finds all methods, functions and constructor named /// parameters and checks if they are in order provided from rule config -/// or default config -class NamedParametersOrderingVisitor - extends RecursiveAstVisitor> { +/// or default config. +class NamedParametersOrderingVisitor extends SimpleAstVisitor { + final NamedParametersOrderingRule _rule; final List _parametersOrder; - final List _parametersInfo = []; - /// Creates instance of [NamedParametersOrderingVisitor] - NamedParametersOrderingVisitor(this._parametersOrder); + NamedParametersOrderingVisitor(this._rule, this._parametersOrder); @override - List visitFormalParameterList(FormalParameterList node) { - super.visitFormalParameterList(node); - - _parametersInfo.clear(); - - final namedParametersList = - node.parameters.where((p) => p.isNamed).toList(); + void visitFormalParameterList(FormalParameterList node) { + final namedParametersList = node.parameters + .where((p) => p.isNamed) + .toList(); if (namedParametersList.isEmpty) { - return _parametersInfo; + return; } - for (final parameter in namedParametersList) { - _parametersInfo.add( - ParameterInfo( - formalParameter: parameter, - parameterOrderingInfo: _getParameterOrderingInfo(parameter), - ), - ); - } - - return _parametersInfo; - } - - ParameterOrderingInfo _getParameterOrderingInfo(FormalParameter parameter) { - final parameterType = _getParameterType(parameter); - final previousParameterType = - _parametersInfo.lastOrNull?.parameterOrderingInfo.parameterType; + final parametersInfo = namedParametersList.map( + (p) => (parameter: p, type: ParameterType.fromParameter(p)), + ); - return ParameterOrderingInfo( - isWrong: _isOrderingWrong(parameterType, previousParameterType), - parameterType: parameterType, - previousParameterType: previousParameterType, + final badParameters = parametersInfo.pairwise().where( + (pair) => _isOrderingWrong(pair.$2.type, pair.$1.type), ); - } - ParameterType _getParameterType( - FormalParameter parameter, [ - bool hasDefaultValue = false, - ]) { - if (parameter is DefaultFormalParameter && - parameter.parameter is! DefaultFormalParameter) { - return _getParameterType( - parameter.parameter, - parameter.defaultValue != null, + for (final (previous, current) in badParameters) { + _rule.reportAtNode( + current.parameter, + arguments: [ + current.type.displayName, + previous.type.displayName, + ], ); } - - switch (parameter) { - case SuperFormalParameter(:final isRequired): - return isRequired - ? ParameterType.requiredInherited - : ParameterType.inherited; - - case DefaultFormalParameter(): - case _ when hasDefaultValue: - return ParameterType.defaultValue; - - case FieldFormalParameter(:final isRequired) || - FunctionTypedFormalParameter(:final isRequired) || - SimpleFormalParameter(:final isRequired): - return isRequired ? ParameterType.required : ParameterType.nullable; - } } bool _isOrderingWrong( ParameterType currentParameterType, - ParameterType? previousParameterType, + ParameterType previousParameterType, ) { - return previousParameterType != null && - _parametersOrder.indexOf(previousParameterType) > - _parametersOrder.indexOf(currentParameterType); + return _parametersOrder.indexOf(previousParameterType) > + _parametersOrder.indexOf(currentParameterType); } } diff --git a/lib/src/utils/correction_utils.dart b/lib/src/utils/correction_utils.dart new file mode 100644 index 00000000..80bfe87c --- /dev/null +++ b/lib/src/utils/correction_utils.dart @@ -0,0 +1,71 @@ +import 'package:analysis_server_plugin/edit/correction_utils.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/source/source_range.dart'; + +/// Extension on [CorrectionUtils] to provide utilities for creating +/// [SourceRange]s and parsing comments. +extension CorrectionUtilsExtension on CorrectionUtils { + /// Creates a [SourceRange] from [start] and [end] offsets. + SourceRange createRange(int start, int end) => + SourceRange(start, end - start); + + /// Returns the text of the range from [start] to [end]. + String getTextRange(int start, int end) => + getRangeText(createRange(start, end)); + + /// Finds trailing comment on the same line as this [node]. + String? getTrailingComment({ + required AstNode node, + required int nextOffset, + }) { + if (node.end >= nextOffset) return null; + + final sameLine = getTextRange(node.end, nextOffset) + .split('\n') + .first; + + final commentIdx = sameLine.indexOf('//'); + + return commentIdx != -1 ? sameLine.substring(commentIdx) : null; + } + + /// Finds leading comment, skipping any trailing comment that belongs + /// to the previous node (same line as previous node). + Token? getLeadingComment({ + required AstNode node, + int? previousEnd, + }) { + Token? comment = node.beginToken.precedingComments; + if (previousEnd == null) return comment; + + for (; comment != null; comment = comment.next) { + if (getTextRange(previousEnd, comment.offset).contains('\n')) { + return comment; + } + } + + return null; + } + + /// Computes the start offset of a [node]'s declaration, + /// including leading comments and indentation if applicable. + int getDeclarationStartOffset({ + required AstNode node, + required Token? leadingComment, + required int minOffset, + }) { + final hasValidComment = leadingComment != null && + leadingComment.offset >= minOffset && + leadingComment.offset < node.offset; + + final blockStart = hasValidComment ? leadingComment.offset : node.offset; + + final lineStart = getLineThis(blockStart); + final prefixText = getTextRange(lineStart, blockStart); + + final isLineStartWhitespace = prefixText.trim().isEmpty; + + return isLineStartWhitespace ? lineStart : blockStart; + } +} diff --git a/lib/src/utils/iterable_utils.dart b/lib/src/utils/iterable_utils.dart new file mode 100644 index 00000000..9f569e21 --- /dev/null +++ b/lib/src/utils/iterable_utils.dart @@ -0,0 +1,11 @@ +/// Extension on [Iterable] that provides a [pairwise] method for grouping +/// elements. +extension IterablePairwise on Iterable { + /// Returns an iterable of overlapping pairs of elements. + /// For example, `[1, 2, 3].pairwise()` returns `[(1, 2), (2, 3)]`. + Iterable<(T, T)> pairwise() sync* { + for (var i = 0; i + 1 < length; i++) { + yield (elementAt(i), elementAt(i + 1)); + } + } +} diff --git a/test/src/lints/named_parameters_ordering/named_parameters_ordering_rule_test.dart b/test/src/lints/named_parameters_ordering/named_parameters_ordering_rule_test.dart new file mode 100644 index 00000000..7a1e82cd --- /dev/null +++ b/test/src/lints/named_parameters_ordering/named_parameters_ordering_rule_test.dart @@ -0,0 +1,352 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineRefSuite(); +} + +void defineRefSuite() { + defineReflectiveSuite(() { + defineReflectiveTests(NamedParametersOrderingRuleTest); + }); +} + +@reflectiveTest +class NamedParametersOrderingRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + static const _customAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + named_parameters_ordering: + order: + - required + - required_super + - default + - nullable + - super +'''; + + @override + void setUp() { + rule = NamedParametersOrderingRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + super.setUp(); + newAnalysisOptionsYamlFile( + testPackageRootPath, + analysisOptionsContent(rules: [rule.name]), + ); + } + + @override + String get analysisRule => NamedParametersOrderingRule.lintName; + + Future test_does_not_report_correct_constructor_ordering() async { + await assertNoDiagnostics(r''' +class Base { + final String accountType; + final String? userId; + + Base({ + required this.accountType, + this.userId, + }); +} + +class User extends Base { + final String name; + final String email; + final String? age; + final String? country; + final bool isActive; + + User({ + required super.accountType, + super.userId, + required this.name, + required this.email, + this.age, + this.country, + this.isActive = true, + }); +} +'''); + } + + Future test_reports_incorrect_constructor_ordering() async { + await assertAutoDiagnostics(''' +class User { + final String accountType; + final String? userId; + + User({ + this.userId, + ${expectLint('required this.accountType')}, + }); +} +'''); + } + + Future test_reports_incorrect_constructor_ordering_complex() async { + await assertAutoDiagnostics(''' +class Base { + final String accountType; + final String? userId; + + Base({ + required this.accountType, + this.userId, + }); +} + +class User extends Base { + final String name; + final String email; + final String? age; + final bool isActive; + + User({ + required super.accountType, + this.age, + ${expectLint('super.userId')}, + required this.name, + this.isActive = true, + ${expectLint('required this.email')}, + }); +} +'''); + } + + Future test_does_not_report_correct_method_ordering() async { + await assertNoDiagnostics(r''' +class UserProfile { + void orderedMethod({ + required String name, + required String email, + int? age, + bool isActive = true, + }) { + return; + } +} +'''); + } + + Future test_reports_incorrect_method_ordering() async { + await assertAutoDiagnostics(''' +class UserProfile { + void partiallyOrderedMethod({ + required String name, + int? age, + ${expectLint('required String email')}, + bool isActive = true, + }) { + return; + } +} +'''); + } + + Future test_reports_incorrect_function_ordering() async { + await assertAutoDiagnostics(''' +void functionExample({ + required String name, + bool isActive = true, + ${expectLint('int? age')}, + ${expectLint('required String email')}, +}) { + return; +} +'''); + } + + Future test_reports_mixed_positional_and_named_parameters() async { + await assertAutoDiagnostics(''' +void mixedParameters( + String accountType, + String? userId, { + int? age, + ${expectLint('required String email')}, + bool isActive = true, + ${expectLint('required String name')}, +}) { + return; +} +'''); + } + + Future test_reports_incorrect_ordering_with_custom_config() async { + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +$_customAnalysisOptionsContent'''); + await assertAutoDiagnostics(''' +class User { + final String accountType; + final String? userId; + + User({ + this.userId, + ${expectLint('required this.accountType')}, + }); +} +'''); + } + + Future + test_reports_incorrect_ordering_with_custom_config_super() async { + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +$_customAnalysisOptionsContent'''); + await assertAutoDiagnostics(''' +class Base { + final String? name; + Base({this.name}); +} + +class User extends Base { + final String? email; + User({ + super.name, + ${expectLint('required this.email')}, + }); +} +'''); + } + + Future test_reports_incorrect_ordering_with_callback_parameter() async { + await assertAutoDiagnostics(''' +void example({ + int? age, + ${expectLint('required void Function() onTap')}, +}) { + return; +} +'''); + } + + Future test_does_not_report_correct_ordering_with_callback() async { + await assertNoDiagnostics(r''' +void example({ + required void Function() onTap, + int? age, +}) { + return; +} +'''); + } + + Future test_reports_incorrect_ordering_with_comments() async { + await assertAutoDiagnostics(''' +void example({ + /* Whether active */ + bool isActive = true, + // Email comment + ${expectLint('required String email')}, + /// The age of the user. + /// Can be null if unknown. + int? age, +}) { + return; +} +'''); + } + + Future + test_reports_incorrect_ordering_with_trailing_comments() async { + await assertAutoDiagnostics(''' +void example({ + int? age, // the age + ${expectLint('required String name')}, // the name + bool isActive = true, // active flag +}) { + return; +} +'''); + } + + Future + test_reports_incorrect_ordering_with_mixed_comments() async { + await assertAutoDiagnostics(''' +void example({ + /// The age of the user. + /// Can be null if unknown. + int? age, // optional + // The user's name + ${expectLint('required String name')}, // must not be empty + bool isActive = true, +}) { + return; +} +'''); + } + + Future test_reports_incorrect_ordering_with_complex_defaults() async { + await assertAutoDiagnostics(''' +void example({ + List items = const [], + int count = 1 + 2, + ${expectLint('required String name')}, +}) { + return; +} +'''); + } + + Future + test_does_not_report_with_partial_custom_config() async { + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + named_parameters_ordering: + order: + - required + - nullable +'''); + // 'default' is omitted from config but should not cause false positives. + // It should be appended after 'nullable' automatically. + await assertNoDiagnostics(r''' +void example({ + required String name, + int? age, + bool isActive = true, +}) { + return; +} +'''); + } + + Future + test_reports_incorrect_ordering_with_partial_custom_config() async { + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + named_parameters_ordering: + order: + - required + - nullable +'''); + // Order is [required, nullable, default]. + // 'default' is appended automatically after 'nullable'. + await assertAutoDiagnostics(''' +void example({ + int? age, + ${expectLint('required String name')}, + bool isActive = true, +}) { + return; +} +'''); + } +} From 0f1e2028472858f458aab532622f888ad4cd8a0a Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 1 Jul 2026 14:51:22 +0300 Subject: [PATCH 33/49] refactor: migrate prefer_conditional_expressions (#303) * refactor: migrate prefer_conditional_expressions * fix: add parentheses to inverted conditional expressions for complex conditions and update tests * refactor: extract logic from prefer_conditional_expressions_visitor into separate model and visitor classes --------- Co-authored-by: Illia Aihistov --- lib/main.dart | 11 + .../prefer_conditional_expressions_fix.dart | 115 +++++------ ...er_conditional_expressions_parameters.dart | 13 +- .../models/statement_info.dart | 117 +++++++++++ .../prefer_conditional_expressions_rule.dart | 80 ++++---- .../visitors/conditionals_visitor.dart | 36 ++++ ...refer_conditional_expressions_visitor.dart | 122 ++---------- .../analysis_options.yaml | 8 - ...tional_expressions_ignore_nested_test.dart | 17 -- .../prefer_conditional_expressions_test.dart | 47 ----- ...fer_conditional_expressions_rule_test.dart | 188 ++++++++++++++++++ 11 files changed, 465 insertions(+), 289 deletions(-) create mode 100644 lib/src/lints/prefer_conditional_expressions/models/statement_info.dart create mode 100644 lib/src/lints/prefer_conditional_expressions/visitors/conditionals_visitor.dart delete mode 100644 lint_test/prefer_conditional_expressions_ignore_nested_test/analysis_options.yaml delete mode 100644 lint_test/prefer_conditional_expressions_ignore_nested_test/prefer_conditional_expressions_ignore_nested_test.dart delete mode 100644 lint_test/prefer_conditional_expressions_test.dart create mode 100644 test/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index df0f54cc..cecaeb70 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,6 +20,8 @@ import 'package:solid_lints/src/lints/named_parameters_ordering/named_parameters import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; import 'package:solid_lints/src/lints/no_magic_number/no_magic_number_rule.dart'; import 'package:solid_lints/src/lints/number_of_parameters/number_of_parameters_rule.dart'; +import 'package:solid_lints/src/lints/prefer_conditional_expressions/fixes/prefer_conditional_expressions_fix.dart'; +import 'package:solid_lints/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart'; import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; import 'package:solid_lints/src/lints/prefer_last/fixes/prefer_last_fix.dart'; @@ -50,6 +52,9 @@ class SolidLintsPlugin extends Plugin { AvoidUnnecessaryTypeAssertionsRule(); final doubleLiteralFormatRule = DoubleLiteralFormatRule(); final preferFirstRule = PreferFirstRule(); + final preferConditionalExpressionsRule = PreferConditionalExpressionsRule( + analysisOptionsLoader: analysisLoader, + ); final preferLastRule = PreferLastRule(); final lintRules = [ @@ -71,6 +76,7 @@ class SolidLintsPlugin extends Plugin { UseNearestContextRule(), NoMagicNumberRule(analysisOptionsLoader: analysisLoader), preferFirstRule, + preferConditionalExpressionsRule, preferLastRule, // TODO: Add more lint rules and use analysisLoader // for rules that need parameters @@ -110,6 +116,11 @@ class SolidLintsPlugin extends Plugin { PreferLastFix.new, ); + registry.registerFixForRule( + preferConditionalExpressionsRule.diagnosticCode, + PreferConditionalExpressionsFix.new, + ); + registry.registerFixForRule( NamedParametersOrderingRule.code, NamedParametersOrderingFix.new, diff --git a/lib/src/lints/prefer_conditional_expressions/fixes/prefer_conditional_expressions_fix.dart b/lib/src/lints/prefer_conditional_expressions/fixes/prefer_conditional_expressions_fix.dart index 8cbf3033..76503be7 100644 --- a/lib/src/lints/prefer_conditional_expressions/fixes/prefer_conditional_expressions_fix.dart +++ b/lib/src/lints/prefer_conditional_expressions/fixes/prefer_conditional_expressions_fix.dart @@ -1,56 +1,55 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/ast/token.dart'; -import 'package:analyzer/diagnostic/diagnostic.dart'; -import 'package:analyzer/source/source_range.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/lints/prefer_conditional_expressions/visitors/prefer_conditional_expressions_visitor.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; +import 'package:solid_lints/src/lints/prefer_conditional_expressions/models/statement_info.dart'; +import 'package:solid_lints/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart'; /// A Quick fix for `prefer_conditional_expressions` rule -/// Suggests to remove unnecessary assertions -class PreferConditionalExpressionsFix extends DartFix { - final Expando _diagnosticsInfoExpando; +/// Suggests to convert simple if statements to conditional expressions +class PreferConditionalExpressionsFix extends ResolvedCorrectionProducer { + static const _fixComment = "Convert to conditional expression."; - /// A Quick fix for `prefer_conditional_expressions` rule - /// Suggests to remove unnecessary assertions - PreferConditionalExpressionsFix(this._diagnosticsInfoExpando); + /// Creates a new instance of [PreferConditionalExpressionsFix] + PreferConditionalExpressionsFix({required super.context}); @override - void run( - CustomLintResolver resolver, - ChangeReporter reporter, - CustomLintContext context, - Diagnostic diagnostic, - List others, - ) { - context.registry.addIfStatement((node) { - if (!diagnostic.sourceRange.intersects(node.sourceRange)) return; + FixKind get fixKind => const FixKind( + 'solid_lints.fix.${PreferConditionalExpressionsRule.lintName}', + DartFixKindPriority.standard, + _fixComment, + ); - final statementInfo = _diagnosticsInfoExpando[diagnostic]; - if (statementInfo == null) return; + @override + FixKind get multiFixKind => const FixKind( + 'solid_lints.fix.multi.${PreferConditionalExpressionsRule.lintName}', + DartFixKindPriority.standard, + '$_fixComment across files', + ); - final correction = _createCorrection(statementInfo); - if (correction == null) return; + @override + CorrectionApplicability get applicability => + CorrectionApplicability.automatically; - _addReplacement(reporter, statementInfo.statement, correction); - }); - } + @override + Future compute(ChangeBuilder builder) async { + final statement = node.thisOrAncestorOfType(); + if (statement == null) return; - void _addReplacement( - ChangeReporter reporter, - IfStatement node, - String correction, - ) { - final changeBuilder = reporter.createChangeBuilder( - message: "Convert to conditional expression.", - priority: 1, - ); + final statementInfo = StatementInfo.fromIfStatement(statement); + if (statementInfo == null) return; - changeBuilder.addDartFileEdit((builder) { - builder.addSimpleReplacement( - SourceRange(node.offset, node.length), + final correction = _createCorrection(statementInfo); + if (correction == null) return; + + await builder.addDartFileEdit( + file, + (builder) => builder.addSimpleReplacement( + statement.sourceRange, correction, - ); - }); + ), + ); } String? _createCorrection(StatementInfo info) { @@ -64,20 +63,7 @@ class PreferConditionalExpressionsFix extends DartFix { final target = thenStatement.leftHandSide; final firstExpression = thenStatement.rightHandSide; final secondExpression = elseStatement.rightHandSide; - - final thenStatementOperator = thenStatement.operator.type; - final elseStatementOperator = elseStatement.operator.type; - - if (_isAssignmentOperatorNotEq(thenStatementOperator) && - _isAssignmentOperatorNotEq(elseStatementOperator)) { - final prefix = thenStatement.leftHandSide; - final thenPart = - '$prefix ${thenStatementOperator.stringValue} $firstExpression'; - final elsePart = - '$prefix ${elseStatementOperator.stringValue} $secondExpression;'; - - return '$condition ? $thenPart : $elsePart'; - } + final op = thenStatement.operator.lexeme; final correctionForLiterals = _createCorrectionForLiterals( condition, @@ -85,7 +71,7 @@ class PreferConditionalExpressionsFix extends DartFix { secondExpression, ); - return '$target = $correctionForLiterals'; + return '$target $op $correctionForLiterals'; } if (thenStatement is ReturnStatement && elseStatement is ReturnStatement) { @@ -110,14 +96,23 @@ class PreferConditionalExpressionsFix extends DartFix { ) { if (firstExpression is BooleanLiteral && secondExpression is BooleanLiteral) { + if (firstExpression.value == secondExpression.value) { + return '${firstExpression.value};'; + } final isInverted = !firstExpression.value && secondExpression.value; + if (isInverted) { + final useParentheses = + condition is! Identifier && + condition is! PropertyAccess && + condition is! MethodInvocation && + condition is! IndexExpression && + condition is! ParenthesizedExpression; + return '${useParentheses ? '!($condition)' : '!$condition'};'; + } - return '${isInverted ? "!" : ""}$condition;'; + return '$condition;'; } return '$condition ? $firstExpression : $secondExpression;'; } - - bool _isAssignmentOperatorNotEq(TokenType token) => - token.isAssignmentOperator && token != TokenType.EQ; } diff --git a/lib/src/lints/prefer_conditional_expressions/models/prefer_conditional_expressions_parameters.dart b/lib/src/lints/prefer_conditional_expressions/models/prefer_conditional_expressions_parameters.dart index 4524e700..0f0982ac 100644 --- a/lib/src/lints/prefer_conditional_expressions/models/prefer_conditional_expressions_parameters.dart +++ b/lib/src/lints/prefer_conditional_expressions/models/prefer_conditional_expressions_parameters.dart @@ -20,11 +20,16 @@ class PreferConditionalExpressionsParameters { required this.ignoreNested, }); + /// Empty [PreferConditionalExpressionsParameters] model. + factory PreferConditionalExpressionsParameters.empty() => + const PreferConditionalExpressionsParameters( + ignoreNested: false, + ); + /// Method for creating from json data factory PreferConditionalExpressionsParameters.fromJson( Map json, - ) => - PreferConditionalExpressionsParameters( - ignoreNested: json[_ignoreNestedConfig] as bool? ?? false, - ); + ) => PreferConditionalExpressionsParameters( + ignoreNested: json[_ignoreNestedConfig] as bool? ?? false, + ); } diff --git a/lib/src/lints/prefer_conditional_expressions/models/statement_info.dart b/lib/src/lints/prefer_conditional_expressions/models/statement_info.dart new file mode 100644 index 00000000..145670d1 --- /dev/null +++ b/lib/src/lints/prefer_conditional_expressions/models/statement_info.dart @@ -0,0 +1,117 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart'; + +/// Data class contains info required for fix +class StatementInfo { + /// If statement node + final IfStatement statement; + + /// Contents of if block + final AstNode unwrappedThenStatement; + + /// Contents of else block + final AstNode unwrappedElseStatement; + + /// Creates instance of an [StatementInfo] + const StatementInfo({ + required this.statement, + required this.unwrappedThenStatement, + required this.unwrappedElseStatement, + }); + + /// Factory constructor to create [StatementInfo] from [IfStatement] if it + /// can be simplified. + static StatementInfo? fromIfStatement(IfStatement statement) { + if (statement.parent is IfStatement || + statement.elseStatement == null || + statement.elseStatement is IfStatement) { + return null; + } + + final thenAssignment = _getAssignmentExpression(statement.thenStatement); + final elseAssignment = _getAssignmentExpression(statement.elseStatement); + + if (thenAssignment != null && + elseAssignment != null && + thenAssignment.operator.type == elseAssignment.operator.type && + _haveEqualNames(thenAssignment, elseAssignment)) { + return StatementInfo( + statement: statement, + unwrappedThenStatement: thenAssignment, + unwrappedElseStatement: elseAssignment, + ); + } + + final thenReturn = _getReturnStatement(statement.thenStatement); + final elseReturn = _getReturnStatement(statement.elseStatement); + + if (thenReturn != null && + elseReturn != null && + thenReturn.expression != null && + elseReturn.expression != null) { + return StatementInfo( + statement: statement, + unwrappedThenStatement: thenReturn, + unwrappedElseStatement: elseReturn, + ); + } + + return null; + } + + static AssignmentExpression? _getAssignmentExpression(Statement? statement) { + if (statement is ExpressionStatement && + statement.expression is AssignmentExpression) { + return statement.expression as AssignmentExpression; + } + + if (statement is Block && statement.statements.length == 1) { + return _getAssignmentExpression(statement.statements.first); + } + + return null; + } + + static bool _haveEqualNames( + AssignmentExpression thenAssignment, + AssignmentExpression elseAssignment, + ) => + thenAssignment.leftHandSide is Identifier && + elseAssignment.leftHandSide is Identifier && + (thenAssignment.leftHandSide as Identifier).name == + (elseAssignment.leftHandSide as Identifier).name; + + static ReturnStatement? _getReturnStatement(Statement? statement) { + if (statement is ReturnStatement) { + return statement; + } + + if (statement is Block && statement.statements.length == 1) { + return _getReturnStatement(statement.statements.first); + } + + return null; + } +} diff --git a/lib/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart b/lib/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart index 134f2250..5f501972 100644 --- a/lib/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart +++ b/lib/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart @@ -1,9 +1,8 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/lints/prefer_conditional_expressions/fixes/prefer_conditional_expressions_fix.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/prefer_conditional_expressions/models/prefer_conditional_expressions_parameters.dart'; import 'package:solid_lints/src/lints/prefer_conditional_expressions/visitors/prefer_conditional_expressions_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; // Inspired by TSLint (https://palantir.github.io/tslint/rules/prefer-conditional-expression/) @@ -14,10 +13,11 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// ### Example config: /// /// ```yaml -/// custom_lint: -/// rules: -/// - prefer_conditional_expressions: -/// ignore_nested: true +/// plugins: +/// solid_lints: +/// diagnostics: +/// prefer_conditional_expressions: +/// ignore_nested: true /// ``` /// /// ### Example @@ -57,51 +57,43 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// ``` class PreferConditionalExpressionsRule extends SolidLintRule { - /// This lint rule represents the error if number of - /// parameters reaches the maximum value. + /// This lint rule represents the error when an if-else statement + /// can be simplified to a conditional expression. static const lintName = 'prefer_conditional_expressions'; - final _diagnosticsInfoExpando = Expando(); + static const _code = LintCode( + lintName, + 'Prefer conditional expression.', + ); - PreferConditionalExpressionsRule._(super.config); + @override + LintCode get diagnosticCode => _code; /// Creates a new instance of [PreferConditionalExpressionsRule] - /// based on the lint configuration. - factory PreferConditionalExpressionsRule.createRule( - CustomLintConfigs configs, - ) { - final config = RuleConfig( - configs: configs, - name: lintName, - paramsParser: PreferConditionalExpressionsParameters.fromJson, - problemMessage: (value) => 'Prefer conditional expression.', - ); - - return PreferConditionalExpressionsRule._(config); - } + PreferConditionalExpressionsRule({ + required super.analysisOptionsLoader, + }) : super.withParameters( + name: _code.lowerCaseName, + description: _code.problemMessage, + parametersParser: PreferConditionalExpressionsParameters.fromJson, + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addCompilationUnit((node) { - final visitor = PreferConditionalExpressionsVisitor( - ignoreNested: config.parameters.ignoreNested, - ); - node.accept(visitor); + super.registerNodeProcessors(registry, context); - for (final element in visitor.statementsInfo) { - final diagnostic = reporter.atNode(element.statement, code); + final parameters = + getParametersForContext(context) ?? + PreferConditionalExpressionsParameters.empty(); - _diagnosticsInfoExpando[diagnostic] = element; - } - }); - } + final visitor = PreferConditionalExpressionsVisitor( + rule: this, + ignoreNested: parameters.ignoreNested, + ); - @override - List getFixes() => [ - PreferConditionalExpressionsFix(_diagnosticsInfoExpando), - ]; + registry.addCompilationUnit(this, visitor); + } } diff --git a/lib/src/lints/prefer_conditional_expressions/visitors/conditionals_visitor.dart b/lib/src/lints/prefer_conditional_expressions/visitors/conditionals_visitor.dart new file mode 100644 index 00000000..866f5776 --- /dev/null +++ b/lib/src/lints/prefer_conditional_expressions/visitors/conditionals_visitor.dart @@ -0,0 +1,36 @@ +// MIT License +// +// Copyright (c) 2020-2021 Dart Code Checker team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +/// A visitor that checks if a node contains nested conditional expressions. +class ConditionalsVisitor extends RecursiveAstVisitor { + /// Whether the visited nodes contain a conditional expression. + bool hasInnerConditionals = false; + + @override + void visitConditionalExpression(ConditionalExpression node) { + hasInnerConditionals = true; + } +} diff --git a/lib/src/lints/prefer_conditional_expressions/visitors/prefer_conditional_expressions_visitor.dart b/lib/src/lints/prefer_conditional_expressions/visitors/prefer_conditional_expressions_visitor.dart index c4df3ff4..d9441907 100644 --- a/lib/src/lints/prefer_conditional_expressions/visitors/prefer_conditional_expressions_visitor.dart +++ b/lib/src/lints/prefer_conditional_expressions/visitors/prefer_conditional_expressions_visitor.dart @@ -23,136 +23,40 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/prefer_conditional_expressions/models/statement_info.dart'; +import 'package:solid_lints/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart'; +import 'package:solid_lints/src/lints/prefer_conditional_expressions/visitors/conditionals_visitor.dart'; /// The AST visitor that will collect all if statements that can be simplified /// into conditional expressions. class PreferConditionalExpressionsVisitor extends RecursiveAstVisitor { - final _statementsInfo = []; - + final PreferConditionalExpressionsRule _rule; final bool _ignoreNested; - /// List of statement info that represents all simple if statements - Iterable get statementsInfo => _statementsInfo; - /// Creates instance of [PreferConditionalExpressionsVisitor] PreferConditionalExpressionsVisitor({ + required PreferConditionalExpressionsRule rule, required bool ignoreNested, - }) : _ignoreNested = ignoreNested; + }) : _rule = rule, + _ignoreNested = ignoreNested; @override void visitIfStatement(IfStatement node) { super.visitIfStatement(node); if (_ignoreNested) { - final visitor = _ConditionalsVisitor(); - node.visitChildren(visitor); + final visitor = ConditionalsVisitor(); + node.thenStatement.accept(visitor); + node.elseStatement?.accept(visitor); if (visitor.hasInnerConditionals) { return; } } - if (node.parent is! IfStatement && - node.elseStatement != null && - node.elseStatement is! IfStatement) { - _checkBothAssignment(node); - _checkBothReturn(node); - } - } - - void _checkBothAssignment(IfStatement statement) { - final thenAssignment = _getAssignmentExpression(statement.thenStatement); - final elseAssignment = _getAssignmentExpression(statement.elseStatement); - - if (thenAssignment != null && - elseAssignment != null && - _haveEqualNames(thenAssignment, elseAssignment)) { - _statementsInfo.add( - StatementInfo( - statement: statement, - unwrappedThenStatement: thenAssignment, - unwrappedElseStatement: elseAssignment, - ), - ); - } - } - - AssignmentExpression? _getAssignmentExpression(Statement? statement) { - if (statement is ExpressionStatement && - statement.expression is AssignmentExpression) { - return statement.expression as AssignmentExpression; - } - - if (statement is Block && statement.statements.length == 1) { - return _getAssignmentExpression(statement.statements.first); - } - - return null; - } - - bool _haveEqualNames( - AssignmentExpression thenAssignment, - AssignmentExpression elseAssignment, - ) => - thenAssignment.leftHandSide is Identifier && - elseAssignment.leftHandSide is Identifier && - (thenAssignment.leftHandSide as Identifier).name == - (elseAssignment.leftHandSide as Identifier).name; - - void _checkBothReturn(IfStatement statement) { - final thenReturn = _getReturnStatement(statement.thenStatement); - final elseReturn = _getReturnStatement(statement.elseStatement); - - if (thenReturn != null && elseReturn != null) { - _statementsInfo.add( - StatementInfo( - statement: statement, - unwrappedThenStatement: thenReturn, - unwrappedElseStatement: elseReturn, - ), - ); - } - } - - ReturnStatement? _getReturnStatement(Statement? statement) { - if (statement is ReturnStatement) { - return statement; - } - - if (statement is Block && statement.statements.length == 1) { - return _getReturnStatement(statement.statements.first); + final info = StatementInfo.fromIfStatement(node); + if (info != null) { + _rule.reportAtNode(node); } - - return null; } } - -class _ConditionalsVisitor extends RecursiveAstVisitor { - bool hasInnerConditionals = false; - - @override - void visitConditionalExpression(ConditionalExpression node) { - hasInnerConditionals = true; - - super.visitConditionalExpression(node); - } -} - -/// Data class contains info required for fix -class StatementInfo { - /// If statement node - final IfStatement statement; - - /// Contents of if block - final AstNode unwrappedThenStatement; - - /// Contents of else block - final AstNode unwrappedElseStatement; - - /// Creates instance of an [StatementInfo] - const StatementInfo({ - required this.statement, - required this.unwrappedThenStatement, - required this.unwrappedElseStatement, - }); -} diff --git a/lint_test/prefer_conditional_expressions_ignore_nested_test/analysis_options.yaml b/lint_test/prefer_conditional_expressions_ignore_nested_test/analysis_options.yaml deleted file mode 100644 index 750be4b4..00000000 --- a/lint_test/prefer_conditional_expressions_ignore_nested_test/analysis_options.yaml +++ /dev/null @@ -1,8 +0,0 @@ -analyzer: - plugins: - - ../custom_lint - -custom_lint: - rules: - - prefer_conditional_expressions: - ignore_nested: true diff --git a/lint_test/prefer_conditional_expressions_ignore_nested_test/prefer_conditional_expressions_ignore_nested_test.dart b/lint_test/prefer_conditional_expressions_ignore_nested_test/prefer_conditional_expressions_ignore_nested_test.dart deleted file mode 100644 index 150b9e4b..00000000 --- a/lint_test/prefer_conditional_expressions_ignore_nested_test/prefer_conditional_expressions_ignore_nested_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -// ignore_for_file: unused_local_variable -// ignore_for_file: cyclomatic_complexity -// ignore_for_file: no_equal_then_else - -/// Check the `prefer_conditional_expressions` rule ignore_nested option -void fun() { - int _result = 0; - - // Allowed because ignore_nested flag is enabled - if (1 > 0) { - if (true) { - _result = 1 > 0 ? 0 : 1; - } - } else { - _result = 0; - } -} diff --git a/lint_test/prefer_conditional_expressions_test.dart b/lint_test/prefer_conditional_expressions_test.dart deleted file mode 100644 index ee60f237..00000000 --- a/lint_test/prefer_conditional_expressions_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -// ignore_for_file: unused_local_variable -// ignore_for_file: cyclomatic_complexity -// ignore_for_file: no_equal_then_else -// ignore_for_file: dead_code -// ignore_for_file: no_magic_number - -/// Check the `prefer_conditional_expressions` rule -void fun() { - int _result = 0; - - // expect_lint: prefer_conditional_expressions - if (true) { - _result = 1; - } else { - _result = 2; - } - - // expect_lint: prefer_conditional_expressions - if (1 > 0) - _result = 1; - else - _result = 2; - - // expect_lint: prefer_conditional_expressions - if (1 > 0) { - _result = 1 > 2 ? 2 : 1; - } else { - _result = 0; - } -} - -int someFun() { - // expect_lint: prefer_conditional_expressions - if (1 == 1) { - return 0; - } else { - return 1; - } -} - -int anotherFun() { - // expect_lint: prefer_conditional_expressions - if (1 > 0) - return 1; - else - return 2; -} diff --git a/test/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule_test.dart b/test/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule_test.dart new file mode 100644 index 00000000..724e0747 --- /dev/null +++ b/test/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule_test.dart @@ -0,0 +1,188 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferConditionalExpressionsRuleTest); + }); +} + +@reflectiveTest +class PreferConditionalExpressionsRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + @override + void setUp() { + rule = PreferConditionalExpressionsRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + super.setUp(); + } + + Future test_reports_on_simple_assignment() async { + await assertAutoDiagnostics(''' +void main() { + var x = 0; + ${expectLint('''if (x > 0) { + x = 1; + } else { + x = 2; + }''')} +} +'''); + } + + Future test_reports_on_simple_return() async { + await assertAutoDiagnostics(''' +int getVal(int x) { + ${expectLint('''if (x > 0) { + return 1; + } else { + return 2; + }''')} +} +'''); + } + + Future test_does_not_report_when_different_variables_assigned() async { + await assertNoDiagnostics(''' +void main() { + var x = 0; + var y = 0; + if (x > 0) { + x = 1; + } else { + y = 2; + } +} +'''); + } + + Future test_does_not_report_when_no_else_clause() async { + await assertNoDiagnostics(''' +void main() { + var x = 0; + if (x > 0) { + x = 1; + } +} +'''); + } + + Future test_reports_on_same_compound_assignment() async { + await assertAutoDiagnostics(''' +void main() { + var x = 0; + ${expectLint('''if (x > 0) { + x += 1; + } else { + x += 2; + }''')} +} +'''); + } + + Future test_does_not_report_on_different_compound_assignment() async { + await assertNoDiagnostics(''' +void main() { + var x = 0; + if (x > 0) { + x += 1; + } else { + x -= 2; + } +} +'''); + } + + Future test_does_not_report_on_mixed_assignment_operators() async { + await assertNoDiagnostics(''' +void main() { + var x = 0; + if (x > 0) { + x += 1; + } else { + x = 2; + } +} +'''); + } + + Future test_does_not_report_on_empty_returns() async { + await assertNoDiagnostics(''' +void test(bool c) { + if (c) { + return; + } else { + return; + } +} +'''); + } + + Future test_does_not_report_on_mixed_empty_return() async { + await assertNoDiagnostics(''' +dynamic test(bool c) { + if (c) { + return 1; + } else { + return; + } +} +'''); + } + + Future test_reports_on_nested_by_default() async { + await assertAutoDiagnostics(''' +void main() { + var x = 0; + ${expectLint('''if (x > 0) { + x = x > 2 ? 3 : 4; + } else { + x = 2; + }''')} +} +'''); + } + + Future test_does_not_report_on_nested_when_ignore_nested_true() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + prefer_conditional_expressions: + ignore_nested: true +''', + ); + await assertNoDiagnostics(''' +void main() { + var x = 0; + if (x > 0) { + x = x > 2 ? 3 : 4; + } else { + x = 2; + } +} +'''); + } + + Future test_reports_on_complex_condition_inversion() async { + await assertAutoDiagnostics(''' +bool test(int x, int y) { + ${expectLint('''if (x > 0 || y < 1) { + return false; + } else { + return true; + }''')} +} +'''); + } +} From 1b70d997923ea2d6448bd390a2cdf91085215272 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Mon, 6 Jul 2026 17:49:58 +0300 Subject: [PATCH 34/49] test: add test for type promotion in avoid_unnecessary_return_variable (#308) * test: add test for type promotion in avoid_unnecessary_return_variable * test: refactor test name and remove whitespace in avoid_unnecessary_return_variable rule test * test: add test for avoid_unnecessary_return_variable rule when variable is used in string interpolation * test: update test cases for avoid_unnecessary_return_variable lint to improve type safety and parameter usage --------- Co-authored-by: Illia Aihistov --- ...unnecessary_return_variable_rule_test.dart | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart b/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart index 704cc16d..c112a617 100644 --- a/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart +++ b/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart @@ -213,7 +213,7 @@ int test() { ); } - void test_does_not_report_if_return_is_cached_and_used_after_return_nested_block() async { + void test_does_not_report_if_cached_and_used_after_nested_block() async { await assertNoDiagnostics(r''' int test(bool b) { final a = 3; @@ -224,5 +224,33 @@ int test(bool b) { } '''); } + + void test_does_not_report_if_type_promoted() async { + await assertNoDiagnostics(r''' +class Test { + final Map _map = {}; + + T get(String key) { + final value = _map[key]; + if (value is T) { + // local variable is promoted to T + return value; + } + + throw Exception('value is not of type $T'); + } } +'''); + } + void test_does_not_report_if_used_in_other_return_statement() async { + await assertNoDiagnostics(r''' +String test(bool someCondition) { + final a = 'test'; + if (someCondition) return a; + + return 'something $a'; +} +'''); + } +} From 824547f5d542a6943170f88c6ab76e3a47cf7559 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Mon, 6 Jul 2026 17:53:12 +0300 Subject: [PATCH 35/49] fix: resolve rules configuration being ignored in nested directories (#310) * fix: resolve rules configuration being ignored in nested directories * refactor: use pattern matching for yaml parsing to improve robustness and prevent crashes on invalid configurations * fix: improve robustness of analysis options loader by filtering non-string keys and fixing infinite loop in path traversal * refactor: move file reading inside the try-catch block in analysis_options_loader --------- Co-authored-by: Illia Aihistov --- lib/analysis_options.yaml | 58 +++++---- lib/analysis_options_test.yaml | 12 +- .../analysis_options_loader.dart | 60 +++++---- .../analysis_options_loader_test.dart | 122 ++++++++++++++++-- 4 files changed, 184 insertions(+), 68 deletions(-) diff --git a/lib/analysis_options.yaml b/lib/analysis_options.yaml index 21857abc..f615faea 100644 --- a/lib/analysis_options.yaml +++ b/lib/analysis_options.yaml @@ -35,34 +35,34 @@ analyzer: parameter_assignments: error unnecessary_cast: warning -custom_lint: - rules: - - avoid_global_state +solid_lints: + diagnostics: + avoid_global_state: true - - avoid_late_keyword: + avoid_late_keyword: allow_initialized: true ignored_types: - AnimationController - - avoid_non_null_assertion - - avoid_returning_widgets - - avoid_unnecessary_return_variable - - avoid_unnecessary_setstate - - avoid_unnecessary_type_assertions - - avoid_unrelated_type_assertions - - avoid_unused_parameters - - avoid_debug_print_in_release - - avoid_final_with_getter + avoid_non_null_assertion: true + avoid_returning_widgets: true + avoid_unnecessary_return_variable: true + avoid_unnecessary_setstate: true + avoid_unnecessary_type_assertions: true + avoid_unrelated_type_assertions: true + avoid_unused_parameters: true + avoid_debug_print_in_release: true + avoid_final_with_getter: true - - cyclomatic_complexity: + cyclomatic_complexity: max_complexity: 10 - - double_literal_format + double_literal_format: true - - function_lines_of_code: + function_lines_of_code: max_lines: 200 - - member_ordering: + member_ordering: order: - fields - getters_setters @@ -76,26 +76,28 @@ custom_lint: - deactivate_method - dispose_method - - newline_before_return - - no_empty_block - - no_equal_then_else - - prefer_early_return + newline_before_return: true + no_empty_block: true + no_equal_then_else: true + prefer_early_return: true - - no_magic_number: + no_magic_number: allowed_in_widget_params: true - - number_of_parameters: + number_of_parameters: max_parameters: 7 exclude: - method_name: copyWith - - prefer_conditional_expressions: + prefer_conditional_expressions: ignore_nested: true - - prefer_first - - prefer_last - - prefer_match_file_name - - proper_super_calls + prefer_first: true + prefer_last: true + prefer_match_file_name: true + proper_super_calls: true + named_parameters_ordering: true + use_nearest_context: true linter: rules: diff --git a/lib/analysis_options_test.yaml b/lib/analysis_options_test.yaml index 5db9e6b7..19ddb7ec 100644 --- a/lib/analysis_options_test.yaml +++ b/lib/analysis_options_test.yaml @@ -1,18 +1,18 @@ include: package:solid_lints/analysis_options.yaml -custom_lint: - rules: +solid_lints: + diagnostics: # Tests usually organized in one large main() function making this rule not applicable. # Given the quite large threshold configured for this metric we considered extracting # test body into separate function, but that means that we'll have to either pass # Test Context that contains all defined variables in main to every function call # or moving them to the Global State. # Both options didn't look right, so we decided that tests are ok to be long. - - function_lines_of_code: false + function_lines_of_code: false # Since we're not using the source-lines-of-code rule, `main()` function in test can # have high cyclomatic complexity. # For rationale against splitting up `main()` in tests, see `source-lines-of-code` comments. - - cyclomatic_complexity: false + cyclomatic_complexity: false # Late keyword is allowed in tests in order to enable the use of custom mocks and # fakes. @@ -48,6 +48,6 @@ custom_lint: # - In terms of behavior similar to `late`, but requires using the operator in # many places inside the test code, adding uninformative visual noise. # - The use of this operator is also discouraged by the main rule set. - - avoid_late_keyword: false + avoid_late_keyword: false # It's acceptable to include stubs or other helper classes into the test file. - - prefer_match_file_name: false + prefer_match_file_name: false diff --git a/lib/src/common/parameter_parser/analysis_options_loader.dart b/lib/src/common/parameter_parser/analysis_options_loader.dart index f4fb74e8..5b0a016e 100644 --- a/lib/src/common/parameter_parser/analysis_options_loader.dart +++ b/lib/src/common/parameter_parser/analysis_options_loader.dart @@ -11,8 +11,7 @@ class AnalysisOptionsLoader { /// Creates an instance of [AnalysisOptionsLoader] AnalysisOptionsLoader({ResourceProvider? resourceProvider}) - : _resourceProvider = - resourceProvider ?? PhysicalResourceProvider.INSTANCE; + : _resourceProvider = resourceProvider ?? PhysicalResourceProvider.INSTANCE; /// Gets the options for a specific rule by its name. Map? getRuleOptions(RuleContext context, String ruleName) => @@ -49,10 +48,10 @@ class AnalysisOptionsLoader { RuleContext context, T Function(String) f, ) { - final packageRootPath = context.package?.root.path; - if (packageRootPath == null) return null; + final filePath = context.definingUnit.file.path; + final dirPath = _resourceProvider.pathContext.dirname(filePath); + final yamlPath = _findNearestAnalysisOptionsFilePath(dirPath); - final yamlPath = _findNearestAnalysisOptionsFilePath(packageRootPath); if (yamlPath == null) return null; return f(yamlPath); @@ -74,13 +73,15 @@ class AnalysisOptionsLoader { ); } - String? _findNearestAnalysisOptionsFilePath(String packageRootPath) { + String? _findNearestAnalysisOptionsFilePath(String startDirectoryPath) { final pathContext = _resourceProvider.pathContext; - String currentDirectoryPath = packageRootPath; + String currentDirectoryPath = startDirectoryPath; - while (pathContext.dirname(currentDirectoryPath) != currentDirectoryPath) { - final candidatePath = - pathContext.join(currentDirectoryPath, 'analysis_options.yaml'); + while (true) { + final candidatePath = pathContext.join( + currentDirectoryPath, + 'analysis_options.yaml', + ); final candidateFile = _resourceProvider.getFile(candidatePath); if (candidateFile.exists) { @@ -88,6 +89,9 @@ class AnalysisOptionsLoader { } final parentDir = pathContext.dirname(currentDirectoryPath); + if (parentDir == currentDirectoryPath) { + break; + } currentDirectoryPath = parentDir; } @@ -99,27 +103,33 @@ class AnalysisOptionsLoader { return {}; } - final optionsString = analysisOptionsFile.readAsStringSync(); Object? yaml; try { - yaml = loadYaml(optionsString) as Object?; - } catch (err) { + final optionsString = analysisOptionsFile.readAsStringSync(); + yaml = loadYaml(optionsString); + } catch (_) { return {}; } - if (yaml - case {'plugins': {'solid_lints': {'diagnostics': final diagnostics?}}} - when diagnostics is Map) { - return Map.fromEntries( - diagnostics.entries.where((e) => e.key is String && e.value is Map).map( - (e) => MapEntry( - e.key as String, - Map.from(e.value as Map), - ), - ), - ); + Object? rawDiagnostics; + if (yaml case {'solid_lints': {'diagnostics': final diagnostics}}) { + rawDiagnostics = diagnostics; + } else if (yaml case { + 'plugins': {'solid_lints': {'diagnostics': final diagnostics}}, + }) { + rawDiagnostics = diagnostics; } - return {}; + if (rawDiagnostics is! Map) return {}; + + return { + for (final entry in rawDiagnostics.entries) + if (entry.key is String && entry.value is Map) + entry.key as String: { + for (final optionEntry in (entry.value as Map).entries) + if (optionEntry.key is String) + optionEntry.key as String: optionEntry.value, + }, + }; } } diff --git a/test/src/common/parameter_parser/analysis_options_loader_test.dart b/test/src/common/parameter_parser/analysis_options_loader_test.dart index a727136f..67fffb6a 100644 --- a/test/src/common/parameter_parser/analysis_options_loader_test.dart +++ b/test/src/common/parameter_parser/analysis_options_loader_test.dart @@ -25,7 +25,7 @@ plugins: solid_lints: diagnostics: $_mockRuleThatNeedsConfigName: - abc: def + some_parameter: root_value $_mockRule2Name: foo: bar exclude: @@ -43,7 +43,7 @@ plugins: solid_lints: diagnostics: $_mockRuleThatNeedsConfigName: - abc: ghi + some_parameter: nested_value $_mockRule2Name: foo: baz exclude: @@ -128,7 +128,7 @@ plugins: ); expect(mockRuleThatNeedsConfigOptions, isNotNull); - expect(mockRuleThatNeedsConfigOptions, {'abc': 'def'}); + expect(mockRuleThatNeedsConfigOptions, {'some_parameter': 'root_value'}); expect(mockRule2Options, isNotNull); expect(mockRule2Options, { @@ -165,8 +165,8 @@ plugins: _mockRuleThatNeedsConfigName, ); - expect(initialOptions, {'abc': 'def'}); - expect(updatedOptions, {'abc': 'ghi'}); + expect(initialOptions, {'some_parameter': 'root_value'}); + expect(updatedOptions, {'some_parameter': 'nested_value'}); expect(updatedOptions, isNot(same(initialOptions))); } @@ -179,7 +179,58 @@ plugins: ); expect(options, isNotNull); - expect(options, {'abc': 'def'}); + } + + void test_does_not_crash_when_plugins_is_list() { + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +plugins: + - solid_lints +''', + ); + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + final options = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + expect(options, isNull); + } + + void test_does_not_crash_when_solid_lints_is_boolean() { + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +solid_lints: true +''', + ); + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + final options = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + expect(options, isNull); + } + + void test_does_not_crash_when_rule_option_has_non_string_key() { + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +plugins: + solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: + 123: true + some_parameter: root_value +''', + ); + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + final options = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + expect(options, isNotNull); + expect(options, {'some_parameter': 'root_value'}); } void test_returns_cached_response_for_same_rule_name() { @@ -197,9 +248,46 @@ plugins: expect(secondOptions, same(firstOptions)); } - RuleContext _createMockContextForPackage(String packageRootPath) { + void test_resolves_nested_analysis_options_for_nested_files() { + final nestedDirPath = '$testPackageRootPath/test'; + final nestedFilePath = '$nestedDirPath/some_test.dart'; + + newFolder(nestedDirPath); + newFile(nestedFilePath, 'void main() {}'); + + newAnalysisOptionsYamlFile( + nestedDirPath, + _mockDifferentAnalysisOptionsContent, + ); + + final nestedFile = getFile(nestedFilePath); + final mockNestedContext = _createMockContextForPackage( + testPackageRootPath, + definingUnit: _TestRuleContextUnit(nestedFile), + ); + + analysisOptionsLoader.loadRulesOptionsFromContext(mockNestedContext); + + final options = analysisOptionsLoader.getRuleOptions( + mockNestedContext, + _mockRuleThatNeedsConfigName, + ); + + expect(options, isNotNull); + expect(options, {'some_parameter': 'nested_value'}); + } + + RuleContext _createMockContextForPackage( + String packageRootPath, { + RuleContextUnit? definingUnit, + }) { + final rootFolder = getFolder(packageRootPath); return _TestRuleContext( - _TestWorkspacePackage(getFolder(packageRootPath)), + _TestWorkspacePackage(rootFolder), + definingUnit: definingUnit ?? + _TestRuleContextUnit( + rootFolder.getChildAssumingFile('lib/dummy.dart'), + ), ); } } @@ -208,7 +296,23 @@ class _TestRuleContext implements RuleContext { @override final WorkspacePackage? package; - _TestRuleContext(this.package); + @override + final RuleContextUnit definingUnit; + + _TestRuleContext( + this.package, { + required this.definingUnit, + }); + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _TestRuleContextUnit implements RuleContextUnit { + @override + final File file; + + _TestRuleContextUnit(this.file); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); From bbd9ea46417c8d887fd8920714049ed81205fc93 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Mon, 6 Jul 2026 17:58:53 +0300 Subject: [PATCH 36/49] refactor: use AutoTestLintOffsets in remaining AnalysisRuleTest suites (#311) * refactor: use AutoTestLintOffsets in remaining AnalysisRuleTest suites * refactor: revert test files movement back to test/lints --------- Co-authored-by: Illia Aihistov --- lib/main.dart | 12 + test/lints/auto_test_lint_offsets.dart | 41 +- ...void_debug_print_in_release_rule_test.dart | 120 +++--- .../avoid_global_state_rule_test.dart | 37 +- .../avoid_non_null_assertion_rule_test.dart | 31 +- ...unnecessary_return_variable_rule_test.dart | 92 ++--- ...avoid_unnecessary_set_state_rule_test.dart | 118 ++---- ...unnecessary_type_assertions_rule_test.dart | 3 - ...d_unrelated_type_assertions_rule_test.dart | 99 ++--- .../avoid_unused_parameters_rule_test.dart | 110 ++---- .../double_literal_format_rule_test.dart | 112 ++---- .../newline_before_return_rule_test.dart | 104 +++--- .../no_equal_then_else_rule_test.dart | 36 +- .../prefer_early_return_rule_test.dart | 353 +++++++----------- .../proper_super_calls_rule_test.dart | 114 +++--- .../avoid_final_with_getter_rule_test.dart | 52 +-- .../avoid_returning_widgets_rule_test.dart | 84 ++--- .../cyclomatic_complexity_rule_test.dart | 4 - .../member_ordering_rule_test.dart | 18 +- .../named_parameters_ordering_rule_test.dart | 12 +- .../no_empty_block_rule_test.dart | 6 +- .../no_magic_number_rule_test.dart | 15 +- .../number_of_parameters_rule_test.dart | 3 - test/src/utils/auto_lint_data.dart | 17 + test/utils/fake_analysis_options_loader.dart | 7 + 25 files changed, 627 insertions(+), 973 deletions(-) create mode 100644 test/src/utils/auto_lint_data.dart diff --git a/lib/main.dart b/lib/main.dart index cecaeb70..4ab02252 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,8 +7,11 @@ import 'package:solid_lints/src/lints/avoid_final_with_getter/fixes/avoid_final_ import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart'; +import 'package:solid_lints/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart'; import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart'; import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; @@ -17,11 +20,14 @@ import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_c import 'package:solid_lints/src/lints/member_ordering/member_ordering_rule.dart'; import 'package:solid_lints/src/lints/named_parameters_ordering/fixes/named_parameters_ordering_fix.dart'; import 'package:solid_lints/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart'; +import 'package:solid_lints/src/lints/newline_before_return/newline_before_return_rule.dart'; import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; +import 'package:solid_lints/src/lints/no_equal_then_else/no_equal_then_else_rule.dart'; import 'package:solid_lints/src/lints/no_magic_number/no_magic_number_rule.dart'; import 'package:solid_lints/src/lints/number_of_parameters/number_of_parameters_rule.dart'; import 'package:solid_lints/src/lints/prefer_conditional_expressions/fixes/prefer_conditional_expressions_fix.dart'; import 'package:solid_lints/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart'; +import 'package:solid_lints/src/lints/prefer_early_return/prefer_early_return_rule.dart'; import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; import 'package:solid_lints/src/lints/prefer_last/fixes/prefer_last_fix.dart'; @@ -81,6 +87,12 @@ class SolidLintsPlugin extends Plugin { // TODO: Add more lint rules and use analysisLoader // for rules that need parameters // For example: `CyclomaticComplexityRule(analysisLoader)` + AvoidUnnecessaryReturnVariableRule(), + AvoidUnnecessarySetStateRule(), + AvoidUnrelatedTypeAssertionsRule(), + NewlineBeforeReturnRule(), + NoEqualThenElseRule(), + PreferEarlyReturnRule(), ]; for (final lintRule in lintRules) { diff --git a/test/lints/auto_test_lint_offsets.dart b/test/lints/auto_test_lint_offsets.dart index a9b4b298..b4b41fbb 100644 --- a/test/lints/auto_test_lint_offsets.dart +++ b/test/lints/auto_test_lint_offsets.dart @@ -2,17 +2,19 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart'; import 'package:collection/collection.dart'; +import '../src/utils/auto_lint_data.dart'; + mixin AutoTestLintOffsets on AnalysisRuleTest { int _nextPlaceholderId = 0; - final Map _placeholderToCode = {}; + final Map _placeholderToData = {}; Future assertAutoDiagnostics(String source) async { try { - final placeholders = _placeholderToCode.entries + final placeholders = _placeholderToData.entries .map( (entry) => ( placeholder: entry.key, - code: entry.value, + data: entry.value, index: source.indexOf(entry.key), ), ) @@ -29,27 +31,46 @@ mixin AutoTestLintOffsets on AnalysisRuleTest { } expectedDiagnostics.add( - lint(match.index + replacedPlaceholdersDelta, match.code.length), + lint( + match.index + replacedPlaceholdersDelta, + match.data.code.length, + correctionContains: match.data.correctionContains, + messageContainsAll: match.data.messageContainsAll, + name: match.data.name, + contextMessages: match.data.contextMessages, + ), ); replacedPlaceholdersDelta += - match.code.length - match.placeholder.length; + match.data.code.length - match.placeholder.length; } - final resolvedSource = _placeholderToCode.entries.fold( + final resolvedSource = _placeholderToData.entries.fold( source, - (resolved, entry) => resolved.replaceFirst(entry.key, entry.value), + (resolved, entry) => resolved.replaceFirst(entry.key, entry.value.code), ); await assertDiagnostics(resolvedSource, expectedDiagnostics); } finally { _nextPlaceholderId = 0; - _placeholderToCode.clear(); + _placeholderToData.clear(); } } - String expectLint(String code) { + String expectLint( + String code, { + Pattern? correctionContains, + List messageContainsAll = const [], + String? name, + List? contextMessages, + }) { final placeholder = '__AUTO_TEST_LINT_${_nextPlaceholderId++}__'; - _placeholderToCode[placeholder] = code; + _placeholderToData[placeholder] = AutoLintData( + code: code, + correctionContains: correctionContains, + messageContainsAll: messageContainsAll, + name: name, + contextMessages: contextMessages, + ); return placeholder; } } diff --git a/test/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart b/test/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart index 74c6554b..1848496e 100644 --- a/test/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart +++ b/test/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart @@ -2,6 +2,8 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(AvoidDebugPrintInReleaseRuleTest); @@ -9,7 +11,8 @@ void main() { } @reflectiveTest -class AvoidDebugPrintInReleaseRuleTest extends AnalysisRuleTest { +class AvoidDebugPrintInReleaseRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { @override void setUp() { final flutter = newPackage('flutter'); @@ -33,81 +36,62 @@ export 'package:flutter/foundation.dart'; super.setUp(); } - @override - String get analysisRule => AvoidDebugPrintInReleaseRule.lintName; - - void test_reports_debug_print_with_package_import() async { - await assertDiagnostics( - r''' + Future test_reports_debug_print_with_package_import() async { + await assertAutoDiagnostics(''' import 'package:flutter/foundation.dart'; void test() { - debugPrint('This should be flagged'); + ${expectLint('debugPrint')}('This should be flagged'); } -''', - [lint(59, 10)], - ); +'''); } - void test_reports_aliased_debug_print_from_package() async { - await assertDiagnostics( - r''' + Future test_reports_aliased_debug_print_from_package() async { + await assertAutoDiagnostics(''' import 'package:flutter/foundation.dart' as f; void test() { - f.debugPrint('This should be flagged'); + f.${expectLint('debugPrint')}('This should be flagged'); } -''', - [lint(66, 10)], - ); +'''); } - void test_reports_debug_print_as_callback() async { - await assertDiagnostics( - r''' + Future test_reports_debug_print_as_callback() async { + await assertAutoDiagnostics(''' import 'package:flutter/foundation.dart'; void test() { - ['a'].forEach(debugPrint); + ['a'].forEach(${expectLint('debugPrint')}); } -''', - [lint(73, 10)], - ); +'''); } - void test_reports_inside_kReleaseMode_guard() async { - await assertDiagnostics( - r''' + Future test_reports_inside_kReleaseMode_guard() async { + await assertAutoDiagnostics(''' import 'package:flutter/foundation.dart'; void test() { if (kReleaseMode) { - debugPrint('This should be flagged'); + ${expectLint('debugPrint')}('This should be flagged'); } } -''', - [lint(83, 10)], - ); +'''); } - void test_reports_inside_not_kDebugMode_guard() async { - await assertDiagnostics( - r''' + Future test_reports_inside_not_kDebugMode_guard() async { + await assertAutoDiagnostics(''' import 'package:flutter/foundation.dart'; void test() { if (!kDebugMode) { - debugPrint('Should still be flagged'); + ${expectLint('debugPrint')}('Should still be flagged'); } } -''', - [lint(82, 10)], - ); +'''); } - void test_does_not_report_inside_not_kReleaseMode() async { - await assertNoDiagnostics( - r''' + Future test_does_not_report_inside_not_kReleaseMode() async { + await assertNoDiagnostics(r''' import 'package:flutter/foundation.dart'; void test() { @@ -115,13 +99,11 @@ void test() { debugPrint('Safe'); } } -''', - ); +'''); } - void test_does_not_report_inside_kDebugMode() async { - await assertNoDiagnostics( - r''' + Future test_does_not_report_inside_kDebugMode() async { + await assertNoDiagnostics(r''' import 'package:flutter/foundation.dart'; void test() { @@ -129,58 +111,46 @@ void test() { debugPrint('Safe'); } } -''', - ); +'''); } - void test_no_report_when_debugPrint_is_not_from_foundation() async { - await assertNoDiagnostics( - r''' + Future test_no_report_when_debugPrint_is_not_from_foundation() async { + await assertNoDiagnostics(r''' void debugPrint(String message) {} void test() { debugPrint('Not a flutter call'); } -''', - ); +'''); } - void test_reports_when_imported_via_material() async { - await assertDiagnostics( - r''' + Future test_reports_when_imported_via_material() async { + await assertAutoDiagnostics(''' import 'package:flutter/material.dart'; void test() { - debugPrint('Flagged via material'); + ${expectLint('debugPrint')}('Flagged via material'); } -''', - [lint(57, 10)], - ); +'''); } - void test_reports_when_imported_via_cupertino() async { - await assertDiagnostics( - r''' + Future test_reports_when_imported_via_cupertino() async { + await assertAutoDiagnostics(''' import 'package:flutter/cupertino.dart'; void test() { - debugPrint('Flagged via cupertino'); + ${expectLint('debugPrint')}('Flagged via cupertino'); } -''', - [lint(58, 10)], - ); +'''); } - void test_reports_debug_print_call_method() async { - await assertDiagnostics( - r''' + Future test_reports_debug_print_call_method() async { + await assertAutoDiagnostics(''' import 'package:flutter/foundation.dart'; void test() { - debugPrint.call('This should be flagged'); + ${expectLint('debugPrint')}.call('This should be flagged'); } -''', - [lint(59, 10)], - ); +'''); } } diff --git a/test/lints/avoid_global_state/avoid_global_state_rule_test.dart b/test/lints/avoid_global_state/avoid_global_state_rule_test.dart index 29616804..2ba14682 100644 --- a/test/lints/avoid_global_state/avoid_global_state_rule_test.dart +++ b/test/lints/avoid_global_state/avoid_global_state_rule_test.dart @@ -2,6 +2,8 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(AvoidGlobalStateRuleTest); @@ -9,47 +11,42 @@ void main() { } @reflectiveTest -class AvoidGlobalStateRuleTest extends AnalysisRuleTest { +class AvoidGlobalStateRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { @override void setUp() { rule = AvoidGlobalStateRule(); super.setUp(); } - void test_reports_mutable_top_level_variable() async { - await assertDiagnostics( - r''' -var globalMutable = 0; -''', - [lint(4, 17)], - ); + Future test_reports_mutable_top_level_variable() async { + await assertAutoDiagnostics(''' +var ${expectLint('globalMutable = 0')}; +'''); } - void test_reports_mutable_static_field() async { - await assertDiagnostics( - r''' + Future test_reports_mutable_static_field() async { + await assertAutoDiagnostics(''' class Test { - static int staticMutable = 0; + static int ${expectLint('staticMutable = 0')}; } -''', - [lint(26, 17)], - ); +'''); } - void test_does_not_report_global_immutable_variables() async { + Future test_does_not_report_global_immutable_variables() async { await assertNoDiagnostics(r''' final globalFinal = 1; const globalConst = 1; '''); } - void test_does_not_report_global_private_variables() async { + Future test_does_not_report_global_private_variables() async { await assertNoDiagnostics(r''' var _privateTopLevel = 0; '''); } - void test_does_not_report_class_level_immutable_variables() async { + Future test_does_not_report_class_level_immutable_variables() async { await assertNoDiagnostics(r''' class Test { static final int staticFinal = 1; @@ -58,7 +55,7 @@ class Test { '''); } - void test_does_not_report_class_level_private_variables() async { + Future test_does_not_report_class_level_private_variables() async { await assertNoDiagnostics(r''' class Test { static int _staticPrivate = 0; @@ -66,7 +63,7 @@ class Test { '''); } - void test_does_not_report_local_variables() async { + Future test_does_not_report_local_variables() async { await assertNoDiagnostics(r''' void m() { int localMutable = 0; diff --git a/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart b/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart index 8d054cda..10cf6ced 100644 --- a/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart +++ b/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart @@ -2,6 +2,8 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(AvoidNonNullAssertionRuleTest); @@ -9,36 +11,31 @@ void main() { } @reflectiveTest -class AvoidNonNullAssertionRuleTest extends AnalysisRuleTest { +class AvoidNonNullAssertionRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { @override void setUp() { rule = AvoidNonNullAssertionRule(); super.setUp(); } - void test_reports_non_null_assertion_on_nullable_value() async { - await assertDiagnostics( - r''' + Future test_reports_non_null_assertion_on_nullable_value() async { + await assertAutoDiagnostics(''' void m(int? number) { - final value = number!; + final value = ${expectLint('number!')}; } -''', - [lint(38, 7)], - ); +'''); } - void test_reports_non_null_assertion_on_method_call() async { - await assertDiagnostics( - r''' + Future test_reports_non_null_assertion_on_method_call() async { + await assertAutoDiagnostics(''' void m(Object? object) { - object!.toString(); + ${expectLint('object!')}.toString(); } -''', - [lint(27, 7)], - ); +'''); } - void test_does_not_report_map_access() async { + Future test_does_not_report_map_access() async { await assertNoDiagnostics(r''' void m() { final map = {'key': 'value'}; @@ -47,7 +44,7 @@ void m() { '''); } - void test_does_not_report_safe_null_check() async { + Future test_does_not_report_safe_null_check() async { await assertNoDiagnostics(r''' void m(int? number) { if (number != null) { diff --git a/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart b/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart index c112a617..051c61c0 100644 --- a/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart +++ b/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart @@ -2,6 +2,8 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(AvoidUnnecessaryReturnVariableTest); @@ -9,25 +11,23 @@ void main() { } @reflectiveTest -class AvoidUnnecessaryReturnVariableTest extends AnalysisRuleTest { +class AvoidUnnecessaryReturnVariableTest extends AnalysisRuleTest + with AutoTestLintOffsets { @override void setUp() { rule = AvoidUnnecessaryReturnVariableRule(); super.setUp(); } - @override - String get analysisRule => rule.name; - - void test_does_not_report_if_return_good_trivial() async { + Future test_does_not_report_if_return_good_trivial() async { await assertNoDiagnostics(r''' int returnVarTestTrivial() { return 1; } - '''); +'''); } - void test_does_not_report_if_return_is_mutable() async { + Future test_does_not_report_if_return_is_mutable() async { await assertNoDiagnostics(r''' int returnVarTestMutable() { var a = 1; @@ -35,18 +35,18 @@ int returnVarTestMutable() { return a; } - '''); +'''); } - void test_does_not_report_if_returns_parameter() async { + Future test_does_not_report_if_returns_parameter() async { await assertNoDiagnostics(r''' int returnVarTestReturnParameter(int param) { return param; } - '''); +'''); } - void test_does_not_report_if_return_is_cached_mutable() async { + Future test_does_not_report_if_return_is_cached_mutable() async { await assertNoDiagnostics(r''' int returnVarTestCachedMutable() { var a = 1; @@ -57,26 +57,24 @@ int returnVarTestCachedMutable() { } void _doNothing() {} - '''); +'''); } - void test_reports_if_return_follows_declaration() async { - await assertDiagnostics( - r''' + Future test_reports_if_return_follows_declaration() async { + await assertAutoDiagnostics(''' int returnVarTestReturnFollowsDeclaration() { var a = 1; final result = a; //Some comment here - return result; + ${expectLint('return result;')} } - ''', - [lint(105, 14)], - ); +'''); } - void test_does_not_report_if_return_is_cached_another_method_result() async { + Future + test_does_not_report_if_return_is_cached_another_method_result() async { await assertNoDiagnostics(r''' int returnVarTestCachedAnotherMethodResult() { var a = 1; @@ -94,7 +92,7 @@ void _doNothing() {} '''); } - void test_does_not_report_if_return_is_cached_object_field() async { + Future test_does_not_report_if_return_is_cached_object_field() async { await assertNoDiagnostics(r''' int returnVarTestCachedObjectField() { final obj = _TestClass(); @@ -116,7 +114,7 @@ void _doNothing() {} '''); } - void test_does_not_report_if_return_used_variable() async { + Future test_does_not_report_if_return_used_variable() async { await assertNoDiagnostics(r''' int returnVarTestUsedVariable() { var a = 1; @@ -128,22 +126,18 @@ int returnVarTestUsedVariable() { '''); } - void test_reports_if_return_is_bad_trivial() async { - await assertDiagnostics( - r''' + Future test_reports_if_return_is_bad_trivial() async { + await assertAutoDiagnostics(''' int returnVarTestBadTrivial() { final result = 1; - return result; + ${expectLint('return result;')} } -''', - [lint(55, 14)], - ); +'''); } - void test_reports_if_return_is_bad_immutable_expression() async { - await assertDiagnostics( - r''' + Future test_reports_if_return_is_bad_immutable_expression() async { + await assertAutoDiagnostics(''' int returnVarTestBadImmutableExpression() { const constLocal = 1; final finalLocal = 1; @@ -156,7 +150,7 @@ int returnVarTestBadImmutableExpression() { testObj.finalField; _doNothing(); - return result; + ${expectLint('return result;')} } class _TestClass { @@ -168,12 +162,10 @@ class _TestClass { } void _doNothing() {} -''', - [lint(304, 14)], - ); +'''); } - void test_does_not_report_if_return_is_cached_nested_block() async { + Future test_does_not_report_if_return_is_cached_nested_block() async { await assertNoDiagnostics(r''' Future testAvoidUnnecessaryReturnVariableNestedBlock() async { final cached = 'cached'; @@ -186,34 +178,30 @@ Future testAvoidUnnecessaryReturnVariableNestedBlock() async { '''); } - void test_reports_if_return_is_cached_and_only_returned_nested_block() async { - await assertDiagnostics( - r''' + Future + test_reports_if_return_is_cached_and_only_returned_nested_block() async { + await assertAutoDiagnostics(''' int test(bool b) { final a = 3; if (b) { - return a; + ${expectLint('return a;')} } return 0; } -''', - [lint(49, 9)], - ); +'''); } - void test_reports_if_return_in_parentheses() async { - await assertDiagnostics( - r''' + Future test_reports_if_return_in_parentheses() async { + await assertAutoDiagnostics(''' int test() { final a = 3; - return (a); + ${expectLint('return (a);')} } -''', - [lint(30, 11)], - ); +'''); } - void test_does_not_report_if_cached_and_used_after_nested_block() async { + Future + test_does_not_report_if_cached_and_used_after_nested_block() async { await assertNoDiagnostics(r''' int test(bool b) { final a = 3; diff --git a/test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart b/test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart index 0e160ae0..a7d308f0 100644 --- a/test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart +++ b/test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart @@ -25,6 +25,8 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(AvoidUnnecessarySetStateRuleTest); @@ -32,14 +34,13 @@ void main() { } @reflectiveTest -class AvoidUnnecessarySetStateRuleTest extends AnalysisRuleTest { +class AvoidUnnecessarySetStateRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { @override void setUp() { final flutter = newPackage('flutter'); - flutter.addFile( - 'lib/src/widgets/framework.dart', - r''' + flutter.addFile('lib/src/widgets/framework.dart', r''' abstract class Widget {} abstract class StatefulWidget extends Widget {} @@ -66,19 +67,14 @@ class ElevatedButton extends Widget { ElevatedButton({this.onPressed, this.onLongPress, this.child}); } -''', - ); +'''); rule = AvoidUnnecessarySetStateRule(); super.setUp(); } - @override - String get analysisRule => rule.name; - - void test_reports_set_state_in_init_state() async { - await assertDiagnostics( - r''' + Future test_reports_set_state_in_init_state() async { + await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; class _MyWidgetState extends State { @@ -88,9 +84,7 @@ class _MyWidgetState extends State { void initState() { super.initState(); - setState(() { - _myString = "Hello"; - }); + ${expectLint('setState(() {\n _myString = "Hello";\n })')}; } @override @@ -100,14 +94,11 @@ class _MyWidgetState extends State { ); } } - ''', - [lint(194, 47)], - ); +'''); } - void test_reports_set_state_in_init_state_with_condition() async { - await assertDiagnostics( - r''' + Future test_reports_set_state_in_init_state_with_condition() async { + await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; class _MyWidgetState extends State { @@ -119,9 +110,7 @@ class _MyWidgetState extends State { super.initState(); if (_condition) { - setState(() { - _myString = "Hello"; - }); + ${expectLint('setState(() {\n _myString = "Hello";\n })')}; } } @@ -132,14 +121,11 @@ class _MyWidgetState extends State { ); } } - ''', - [lint(250, 51)], - ); +'''); } - void test_reports_set_state_in_init_state_through_method() async { - await assertDiagnostics( - r''' + Future test_reports_set_state_in_init_state_through_method() async { + await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; class _MyWidgetState extends State { @@ -150,7 +136,7 @@ class _MyWidgetState extends State { void initState() { super.initState(); - myStateUpdateMethod(); + ${expectLint('myStateUpdateMethod()')}; } void myStateUpdateMethod() { @@ -166,14 +152,11 @@ class _MyWidgetState extends State { ); } } - ''', - [lint(226, 21)], - ); +'''); } - void test_reports_set_state_in_did_update_widget() async { - await assertDiagnostics( - r''' + Future test_reports_set_state_in_did_update_widget() async { + await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; class _MyWidgetState extends State { @@ -182,9 +165,7 @@ class _MyWidgetState extends State { @override void didUpdateWidget(StatefulWidget oldWidget) { super.didUpdateWidget(oldWidget); - setState(() { - _myString = "Hello"; - }); + ${expectLint('setState(() {\n _myString = "Hello";\n })')}; } @override @@ -194,14 +175,11 @@ class _MyWidgetState extends State { ); } } - ''', - [lint(238, 47)], - ); +'''); } - void test_reports_set_state_in_build_method() async { - await assertDiagnostics( - r''' + Future test_reports_set_state_in_build_method() async { + await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; class _MyWidgetState extends State { @@ -209,23 +187,18 @@ class _MyWidgetState extends State { @override Widget build(BuildContext context) { - setState(() { - _myString = "Hello"; - }); + ${expectLint('setState(() {\n _myString = "Hello";\n })')}; return ElevatedButton( child: Text(_myString), ); } } - ''', - [lint(188, 47)], - ); +'''); } - void test_reports_set_state_in_build_method_with_condition() async { - await assertDiagnostics( - r''' + Future test_reports_set_state_in_build_method_with_condition() async { + await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; class _MyWidgetState extends State { @@ -235,9 +208,7 @@ class _MyWidgetState extends State { @override Widget build(BuildContext context) { if (_condition) { - setState(() { - _myString = "Hello"; - }); + ${expectLint('setState(() {\n _myString = "Hello";\n })')}; } return ElevatedButton( @@ -245,14 +216,11 @@ class _MyWidgetState extends State { ); } } - ''', - [lint(244, 51)], - ); +'''); } - void test_reports_set_state_in_build_method_through_method() async { - await assertDiagnostics( - r''' + Future test_reports_set_state_in_build_method_through_method() async { + await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; class _MyWidgetState extends State { @@ -266,21 +234,18 @@ class _MyWidgetState extends State { @override Widget build(BuildContext context) { - myStateUpdateMethod(); + ${expectLint('myStateUpdateMethod()')}; return ElevatedButton( child: Text(_myString), ); } } - ''', - [lint(277, 21)], - ); +'''); } - void test_does_not_report_set_state_in_button_on_pressed() async { - await assertNoDiagnostics( - r''' + Future test_does_not_report_set_state_in_button_on_pressed() async { + await assertNoDiagnostics(r''' import 'package:flutter/src/widgets/framework.dart'; class _MyWidgetState extends State { @@ -300,13 +265,11 @@ class _MyWidgetState extends State { ); } } - ''', - ); +'''); } - void test_does_not_report_set_state_in_button_on_long_press() async { - await assertNoDiagnostics( - r''' + Future test_does_not_report_set_state_in_button_on_long_press() async { + await assertNoDiagnostics(r''' import 'package:flutter/src/widgets/framework.dart'; class _MyWidgetState extends State { @@ -324,7 +287,6 @@ class _MyWidgetState extends State { ); } } - ''', - ); +'''); } } diff --git a/test/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart b/test/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart index 602e60ba..a383bf07 100644 --- a/test/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart +++ b/test/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart @@ -19,9 +19,6 @@ class AvoidUnnecessaryTypeAssertionsRuleTest extends AnalysisRuleTest super.setUp(); } - @override - String get analysisRule => AvoidUnnecessaryTypeAssertionsRule.lintName; - Future test_does_not_report_if_is_expression_checks_nullable_source() async { await assertNoDiagnostics(r''' diff --git a/test/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart b/test/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart index a2260724..c41bb3ad 100644 --- a/test/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart +++ b/test/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart @@ -2,6 +2,8 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(AvoidUnrelatedTypeAssertionsRuleTest); @@ -9,64 +11,46 @@ void main() { } @reflectiveTest -class AvoidUnrelatedTypeAssertionsRuleTest extends AnalysisRuleTest { +class AvoidUnrelatedTypeAssertionsRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { @override void setUp() { rule = AvoidUnrelatedTypeAssertionsRule(); super.setUp(); } - @override - String get analysisRule => rule.name; - - void test_reports_unrelated_string_is_int() async { - await assertDiagnostics( - r''' + Future test_reports_unrelated_string_is_int() async { + await assertAutoDiagnostics(''' void fun() { final testString = ''; - final result = testString is int; + final result = ${expectLint('testString is int', messageContainsAll: ['false'])}; } -''', - [ - lint(56, 17, messageContainsAll: ['false']), - ], - ); +'''); } - void test_reports_unrelated_int_list_is_string_list() async { - await assertDiagnostics( - r''' + Future test_reports_unrelated_int_list_is_string_list() async { + await assertAutoDiagnostics(''' void fun() { final testList = [1, 2, 3]; - final result = testList is List; + final result = ${expectLint('testList is List', messageContainsAll: ['false'])}; } -''', - [ - lint(61, 24, messageContainsAll: ['false']), - ], - ); +'''); } - void test_reports_unrelated_string_map_is_double_map() async { - await assertDiagnostics( - r''' + Future test_reports_unrelated_string_map_is_double_map() async { + await assertAutoDiagnostics(''' void fun() { final testMap = {'A': 'B'}; - final result = testMap['A'] is double; + final result = ${expectLint("testMap['A'] is double", messageContainsAll: ['false'])}; } -''', - [ - lint(61, 22, messageContainsAll: ['false']), - ], - ); +'''); } - void test_reports_unrelated_class_is_another_class() async { - await assertDiagnostics( - r''' + Future test_reports_unrelated_class_is_another_class() async { + await assertAutoDiagnostics(''' class Foo {} class Bar {} @@ -74,18 +58,13 @@ class Bar {} void fun() { final Foo foo = Foo(); - final result = foo is Bar; + final result = ${expectLint('foo is Bar', messageContainsAll: ['false'])}; } -''', - [ - lint(84, 10, messageContainsAll: ['false']), - ], - ); +'''); } - void test_reports_unrelated_child_class_is_another_class() async { - await assertDiagnostics( - r''' + Future test_reports_unrelated_child_class_is_another_class() async { + await assertAutoDiagnostics(''' class Foo {} class Bar {} @@ -95,18 +74,13 @@ class ChildFoo extends Foo {} void fun() { final childFoo = ChildFoo(); - final result = childFoo is Bar; + final result = ${expectLint('childFoo is Bar', messageContainsAll: ['false'])}; } -''', - [ - lint(121, 15, messageContainsAll: ['false']), - ], - ); +'''); } - void test_reports_unrelated_is_condition() async { - await assertDiagnostics( - r''' + Future test_reports_unrelated_is_condition() async { + await assertAutoDiagnostics(''' class _A {} class _B extends _A {} @@ -116,18 +90,13 @@ class _C {} void lint() { final _A a = _B(); - if (a is _C) return; + if (${expectLint('a is _C', messageContainsAll: ['false'])}) return; } -''', - [ - lint(92, 7, messageContainsAll: ['false']), - ], - ); +'''); } - void test_reports_unrelated_is_not_condition() async { - await assertDiagnostics( - r''' + Future test_reports_unrelated_is_not_condition() async { + await assertAutoDiagnostics(''' class _A {} class _B extends _A {} @@ -137,12 +106,8 @@ class _C {} void lint() { final _A a = _B(); - if (a is! _C) return; + if (${expectLint('a is! _C', messageContainsAll: ['true'])}) return; } -''', - [ - lint(92, 8, messageContainsAll: ['true']), - ], - ); +'''); } } diff --git a/test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart b/test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart index 440ee2e4..3ea643ce 100644 --- a/test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart +++ b/test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart @@ -3,6 +3,7 @@ import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_param import 'package:test_reflective_loader/test_reflective_loader.dart'; import '../../utils/fake_analysis_options_loader.dart'; +import '../auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { @@ -11,7 +12,8 @@ void main() { } @reflectiveTest -class AvoidUnusedParametersRuleTest extends AnalysisRuleTest { +class AvoidUnusedParametersRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { static const _importFlutterMaterial = "import 'package:flutter/material.dart';"; @@ -62,90 +64,72 @@ class Placeholder extends StatelessWidget { } Future test_reports_on_unused_function_expression_parameters() async { - await assertDiagnostics( - r''' + await assertAutoDiagnostics(''' typedef MaxFun = int Function(int a, int b); -final MaxFun bad = (int a, int b) => 1; +final MaxFun bad = (${expectLint('int a')}, ${expectLint('int b')}) => 1; -final MaxFun tetsFun = (int a, int b) { +final MaxFun tetsFun = (${expectLint('int a')}, ${expectLint('int b')}) { return 4; }; -var c = (String g) { +var c = (${expectLint('String g')}) { return '0'; }; -final MaxFun maxFunInstance = (int a, int b) => 1; -''', - [ - lint(66, 5), - lint(73, 5), - lint(111, 5), - lint(118, 5), - lint(152, 8), - lint(213, 5), - lint(220, 5), - ], - ); +final MaxFun maxFunInstance = (${expectLint('int a')}, ${expectLint('int b')}) => 1; +'''); } Future test_reports_on_unused_optional_and_named_parameters() async { - await assertDiagnostics( - r''' -final optional = (int a, [int b = 0]) { + await assertAutoDiagnostics(''' +final optional = (int a, [${expectLint('int b = 0')}]) { return a; }; -final named = (int a, {required int b, int c = 0}) { +final named = (${expectLint('int a')}, {required int b, int c = 0}) { return c; }; -''', - [lint(26, 9), lint(71, 5)], - ); +'''); } Future test_reports_on_unused_top_level_functions() async { - await assertDiagnostics( - r''' -void fun(String s) { + await assertAutoDiagnostics(''' +void fun(${expectLint('String s')}) { return; } -void fun2(String s) { +void fun2(${expectLint('String s')}) { return; } -void closure(int a) { +void closure(${expectLint('int a')}) { void internal(int a) { print(a); } } -''', - [lint(9, 8), lint(44, 8), lint(82, 5)], - ); +'''); } Future test_reports_on_unused_parameters_in_methods() async { - await assertDiagnostics( - r''' + await assertAutoDiagnostics(''' class TestClass { - static void staticMethod(int a) {} + static void staticMethod(${expectLint('int a')}) {} - void method(String s) { + void method(${expectLint('String s')}) { return; } } class SomeOtherClass { - final MaxFun maxFunLint = (int a, int b) => 1; + final MaxFun maxFunLint = (${expectLint('int a')}, ${expectLint('int b')}) => 1; // Good final MaxFun good = (int a, int b) { return a * b; }; - void method(String s) { + void method(${expectLint('String s')}) { return; } } @@ -158,55 +142,42 @@ class SomeOtherAnotherClass { return; } - void anonymousCallback(Function(int a) cb) {} + void anonymousCallback(${expectLint('Function(int a) cb')}) {} } -''', - [ - lint(45, 5), - lint(70, 8), - lint(153, 5), - lint(160, 5), - lint(261, 8), - lint(450, 18), - ], - ); +'''); } Future test_reports_on_unused_parameters_in_constructors() async { - await assertDiagnostics( - r''' + await assertAutoDiagnostics(''' class Foo { final int a; final int? b; - Foo.another({required int c}) + Foo.another({${expectLint('required int c')}}) : a = 1, b = 0; - factory Foo.aOnly(int a) { + factory Foo.aOnly(${expectLint('int a')}) { return Foo._(1, null); } Foo._(this.a, this.b); } -''', - [lint(59, 14), lint(127, 5)], - ); +'''); } Future test_reports_on_unused_widget_constructor_parameters() async { - await assertDiagnostics( - ''' + await assertAutoDiagnostics(''' $_importFlutterMaterial class TestWidget extends StatelessWidget { const TestWidget({ super.key, - int a = 1, - String k = '', + ${expectLint('int a = 1')}, + ${expectLint("String k = ''")}, }); - factory TestWidget.a([int b = 0]) { + factory TestWidget.a([${expectLint('int b = 0')}]) { return const TestWidget(); } @@ -215,9 +186,7 @@ class TestWidget extends StatelessWidget { return const Placeholder(); } } -''', - [lint(124, 9), lint(139, 13), lint(185, 9)], - ); +'''); } Future test_does_not_report_on_used_parameters() async { @@ -364,21 +333,18 @@ class SimpleClassName { } Future test_reports_on_non_matching_excluded_declarations() async { - await assertDiagnostics( - r''' + await assertAutoDiagnostics(''' class Exclude { - void excludeMethod2(String s) { + void excludeMethod2(${expectLint('String s')}) { return; } } class SimpleClassName { - void simpleMethodName2(String s) { + void simpleMethodName2(${expectLint('String s')}) { return; } } -''', - [lint(38, 8), lint(118, 8)], - ); +'''); } } diff --git a/test/lints/double_literal_format/double_literal_format_rule_test.dart b/test/lints/double_literal_format/double_literal_format_rule_test.dart index d3c8c2a7..43e01e78 100644 --- a/test/lints/double_literal_format/double_literal_format_rule_test.dart +++ b/test/lints/double_literal_format/double_literal_format_rule_test.dart @@ -2,6 +2,8 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(DoubleLiteralFormatRuleTest); @@ -9,7 +11,8 @@ void main() { } @reflectiveTest -class DoubleLiteralFormatRuleTest extends AnalysisRuleTest { +class DoubleLiteralFormatRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { @override void setUp() { rule = DoubleLiteralFormatRule(); @@ -17,99 +20,58 @@ class DoubleLiteralFormatRuleTest extends AnalysisRuleTest { } Future test_reports_on_leading_zeros() async { - await assertDiagnostics( - r''' -var badA = 05.23; -double badB = -01.2; -double badC = -001.2; -double badExpr = 5.23 + 05.23; + await assertAutoDiagnostics(''' +var badA = ${expectLint('05.23')}; +double badB = -${expectLint('01.2')}; +double badC = -${expectLint('001.2')}; +double badExpr = 5.23 + ${expectLint('05.23')}; class Test { - var badA = 05.23; - double badB = -01.2; - double badC = -001.2; - double badExpr = 5.23 + 05.23; + var badA = ${expectLint('05.23')}; + double badB = -${expectLint('01.2')}; + double badC = -${expectLint('001.2')}; + double badExpr = 5.23 + ${expectLint('05.23')}; } -''', - [ - lint(11, 5), - lint(33, 4), - lint(54, 5), - lint(85, 5), - - lint(119, 5), - lint(143, 4), - lint(166, 5), - lint(199, 5), - ], - ); +'''); } Future test_reports_on_trailing_zeros() async { - await assertDiagnostics( - r''' + await assertAutoDiagnostics(''' class Test { - var badA = 5.230; - final badB = -1.20; - double get badC => -1.200; - double badExpr = 5.23 + 5.230; - var badD = -0.400e-5; + var badA = ${expectLint('5.230')}; + final badB = -${expectLint('1.20')}; + double get badC => -${expectLint('1.200')}; + double badExpr = 5.23 + ${expectLint('5.230')}; + var badD = -${expectLint('0.400e-5')}; void someMethod() { - var badA = 5.230; - double badB = -1.20; - double badC = -1.200; - double badExpr = 5.23 + 5.230; - var badD = -0.400E-5; + var badA = ${expectLint('5.230')}; + double badB = -${expectLint('1.20')}; + double badC = -${expectLint('1.200')}; + double badExpr = 5.23 + ${expectLint('5.230')}; + var badD = -${expectLint('0.400E-5')}; } } -''', - [ - lint(26, 5), - lint(49, 4), - lint(77, 5), - lint(110, 5), - lint(131, 8), - - lint(179, 5), - lint(205, 4), - lint(230, 5), - lint(265, 5), - lint(288, 8), - ], - ); +'''); } Future test_reports_on_leading_decimal_point() async { - await assertDiagnostics( - r''' -var badA = .23; -double badB = -.2; -double badExpr = 5.23 + .23; -var badD = .4e-5; + await assertAutoDiagnostics(''' +var badA = ${expectLint('.23')}; +double badB = -${expectLint('.2')}; +double badExpr = 5.23 + ${expectLint('.23')}; +var badD = ${expectLint('.4e-5')}; class Test { - var badA = .23; - double badB = -.2; - double get badExpr => 5.23 + .23; - double get badD => -.4E-5; + var badA = ${expectLint('.23')}; + double badB = -${expectLint('.2')}; + double get badExpr => 5.23 + ${expectLint('.23')}; + double get badD => -${expectLint('.4E-5')}; } -''', - [ - lint(11, 3), - lint(31, 2), - lint(59, 3), - lint(75, 5), - - lint(109, 3), - lint(131, 2), - lint(166, 3), - lint(193, 5), - ], - ); +'''); } - void test_does_not_report_on_non_double_literals() async { + Future test_does_not_report_on_non_double_literals() async { await assertNoDiagnostics(r''' var badA = '05.23'; var stringA = '5.23'; diff --git a/test/lints/newline_before_return/newline_before_return_rule_test.dart b/test/lints/newline_before_return/newline_before_return_rule_test.dart index db805301..a8ea4f19 100644 --- a/test/lints/newline_before_return/newline_before_return_rule_test.dart +++ b/test/lints/newline_before_return/newline_before_return_rule_test.dart @@ -2,6 +2,8 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/newline_before_return/newline_before_return_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(NewlineBeforeReturnRuleTest); @@ -9,68 +11,55 @@ void main() { } @reflectiveTest -class NewlineBeforeReturnRuleTest extends AnalysisRuleTest { +class NewlineBeforeReturnRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { @override void setUp() { rule = NewlineBeforeReturnRule(); super.setUp(); } - @override - String get analysisRule => rule.name; - - void test_reports_no_newline_before_return_value() async { - await assertDiagnostics( - r''' + Future test_reports_no_newline_before_return_value() async { + await assertAutoDiagnostics(''' int method() { final a = 0; - return 1; + ${expectLint('return 1;')} } - ''', - [lint(32, 9)], - ); +'''); } - void test_reports_no_newline_before_return() async { - await assertDiagnostics( - r''' + Future test_reports_no_newline_before_return() async { + await assertAutoDiagnostics(''' void method() { final a = 0; - return; + ${expectLint('return;')} } - ''', - [lint(33, 7)], - ); +'''); } - void test_reports_no_newline_before_return_with_comment() async { - await assertDiagnostics( - r''' + Future test_reports_no_newline_before_return_with_comment() async { + await assertAutoDiagnostics(''' void method() { final a = 0; // Comment - return; + ${expectLint('return;')} } - ''', - [lint(46, 7)], - ); +'''); } - void test_reports_no_newline_before_return_with_multiple_comments() async { - await assertDiagnostics( - r''' + Future + test_reports_no_newline_before_return_with_multiple_comments() async { + await assertAutoDiagnostics(''' void method() { final a = 0; // Comment 1 // Comment 2 - return; + ${expectLint('return;')} } - ''', - [lint(63, 7)], - ); +'''); } - void test_does_not_report_newline_before_return_with_comment() async { + Future test_does_not_report_newline_before_return_with_comment() async { await assertNoDiagnostics(r''' void method() { final a = 0; @@ -78,40 +67,41 @@ void method() { // Comment return; } - '''); +'''); } - void test_does_not_report_no_newline_before_single_statement_return() async { + Future + test_does_not_report_no_newline_before_single_statement_return() async { await assertNoDiagnostics(r''' void method() { return; } - '''); +'''); } - void test_does_not_report_newline_before_return() async { + Future test_does_not_report_newline_before_return() async { await assertNoDiagnostics(r''' void method() { final a = 0; return; } - '''); +'''); } - void + Future test_does_not_report_no_newline_before_single_statement_return_value() async { await assertNoDiagnostics(r''' int method() { return 1; } - '''); +'''); } - void + Future test_does_not_report_no_newline_before_single_statement_nested_return() async { await assertNoDiagnostics(r''' -class Foo{ +class Foo { void bar(void Function()) { return; } @@ -123,13 +113,12 @@ void fun() { return; }); } - '''); +'''); } - void test_reports_no_newline_before_return_nested() async { - await assertDiagnostics( - r''' -class Foo{ + Future test_reports_no_newline_before_return_nested() async { + await assertAutoDiagnostics(''' +class Foo { void bar(void Function()) { return; } @@ -139,18 +128,15 @@ void fun() { final foo = Foo(); foo.bar(() { final a = 1; - return; + ${expectLint('return;')} }); } - ''', - [lint(130, 7)], - ); +'''); } - void test_reports_no_newline_before_two_return_nested() async { - await assertDiagnostics( - r''' -class Foo{ + Future test_reports_no_newline_before_two_return_nested() async { + await assertAutoDiagnostics(''' +class Foo { void bar(void Function()) { return; } @@ -160,12 +146,10 @@ void fun() { final foo = Foo(); foo.bar(() { final a = 1; - return; + ${expectLint('return;')} }); - return; + ${expectLint('return;')} } - ''', - [lint(130, 7), lint(146, 7)], - ); +'''); } } diff --git a/test/lints/no_equal_then_else/no_equal_then_else_rule_test.dart b/test/lints/no_equal_then_else/no_equal_then_else_rule_test.dart index 8eecbb3f..99059d62 100644 --- a/test/lints/no_equal_then_else/no_equal_then_else_rule_test.dart +++ b/test/lints/no_equal_then_else/no_equal_then_else_rule_test.dart @@ -2,6 +2,8 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/no_equal_then_else/no_equal_then_else_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(NoEqualThenElseRuleTest); @@ -9,37 +11,32 @@ void main() { } @reflectiveTest -class NoEqualThenElseRuleTest extends AnalysisRuleTest { +class NoEqualThenElseRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { @override void setUp() { rule = NoEqualThenElseRule(); super.setUp(); } - @override - String get analysisRule => rule.name; - - void test_reports_then_and_else_equal() async { - await assertDiagnostics( - r''' + Future test_reports_then_and_else_equal() async { + await assertAutoDiagnostics(''' void fun() { final _valueA = 1; final _valueB = 2; int _result = 0; - if (_valueA == 1) { + ${expectLint('''if (_valueA == 1) { _result = _valueA; } else { _result = _valueA; - } + }''')} } -''', - [lint(78, 80)], - ); +'''); } - void test_does_not_report_then_and_else_different() async { + Future test_does_not_report_then_and_else_different() async { await assertNoDiagnostics(r''' void fun() { final _valueA = 1; @@ -56,23 +53,20 @@ void fun() { '''); } - void test_reports_conditional_expression_then_and_else_equal() async { - await assertDiagnostics( - r''' + Future test_reports_conditional_expression_then_and_else_equal() async { + await assertAutoDiagnostics(''' void fun() { final _valueA = 1; final _valueB = 2; int _result = 0; - _result = _valueA == 2 ? _valueA : _valueA; + _result = ${expectLint('_valueA == 2 ? _valueA : _valueA')}; } -''', - [lint(88, 32)], - ); +'''); } - void + Future test_does_not_report_conditional_expression_then_and_else_different() async { await assertNoDiagnostics(r''' void fun() { diff --git a/test/lints/prefer_early_return/prefer_early_return_rule_test.dart b/test/lints/prefer_early_return/prefer_early_return_rule_test.dart index 2e52493a..4b3cfa2e 100644 --- a/test/lints/prefer_early_return/prefer_early_return_rule_test.dart +++ b/test/lints/prefer_early_return/prefer_early_return_rule_test.dart @@ -2,6 +2,8 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/prefer_early_return/prefer_early_return_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(PreferEarlyReturnRuleTest); @@ -9,47 +11,38 @@ void main() { } @reflectiveTest -class PreferEarlyReturnRuleTest extends AnalysisRuleTest { +class PreferEarlyReturnRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { @override void setUp() { rule = PreferEarlyReturnRule(); super.setUp(); } - @override - String get analysisRule => PreferEarlyReturnRule.lintName; - - void test_reports_if_as_only_statement_in_function() async { - await assertDiagnostics( - r''' + Future test_reports_if_as_only_statement_in_function() async { + await assertAutoDiagnostics(''' void test(bool a) { - if (a) { + ${expectLint('''if (a) { print('hello'); - } + }''')} } -''', - [lint(22, 32)], - ); +'''); } - void test_reports_if_with_return() async { - await assertDiagnostics( - r''' + Future test_reports_if_with_return() async { + await assertAutoDiagnostics(''' void test(bool a) { - if (a) { + ${expectLint('''if (a) { print('hello'); - } + }''')} return; } -''', - [lint(22, 32)], - ); +'''); } - void test_does_not_report_if_with_return_value() async { - await assertNoDiagnostics( - r''' + Future test_does_not_report_if_with_return_value() async { + await assertNoDiagnostics(r''' int test(bool a) { if (a) { print('hello'); @@ -57,30 +50,23 @@ int test(bool a) { return 1; } -''', - ); +'''); } - void test_reports_nested_if_as_only_statement() async { - await assertDiagnostics( - r''' + Future test_reports_nested_if_as_only_statement() async { + await assertAutoDiagnostics(''' void test(bool a, bool b) { - if (a) { + ${expectLint('''if (a) { if (b) { print('nested'); } - } + }''')} } -''', - [ - lint(30, 54), - ], - ); +'''); } - void test_does_not_report_nested_if_with_return_value() async { - await assertNoDiagnostics( - r''' + Future test_does_not_report_nested_if_with_return_value() async { + await assertNoDiagnostics(r''' int test(bool a, bool b) { if (a) { if (b) { @@ -90,52 +76,40 @@ int test(bool a, bool b) { return 1; } -''', - ); +'''); } - void test_reports_nested_3_if_as_only_statement() async { - await assertDiagnostics( - r''' + Future test_reports_nested_3_if_as_only_statement() async { + await assertAutoDiagnostics(''' void test(bool a, bool b, bool c) { - if (a) { + ${expectLint('''if (a) { if (b) { if (c){ print('nested'); } } - } + }''')} } -''', - [ - lint(38, 78), - ], - ); +'''); } - void test_reports_nested_3_with_return() async { - await assertDiagnostics( - r''' + Future test_reports_nested_3_with_return() async { + await assertAutoDiagnostics(''' void test(bool a, bool b, bool c) { - if (a) { + ${expectLint('''if (a) { if (b) { if (c){ print('nested'); } } - } + }''')} return; } -''', - [ - lint(38, 78), - ], - ); +'''); } - void test_does_not_report_if_else() async { - await assertNoDiagnostics( - r''' + Future test_does_not_report_if_else() async { + await assertNoDiagnostics(r''' void test(bool a) { if (a) { print('hello'); @@ -143,13 +117,11 @@ void test(bool a) { print('hello'); } } -''', - ); +'''); } - void test_does_not_report_if_else_return() async { - await assertNoDiagnostics( - r''' + Future test_does_not_report_if_else_return() async { + await assertNoDiagnostics(r''' void test(bool a) { if (a) { print('hello'); @@ -157,13 +129,11 @@ void test(bool a) { return; } } -''', - ); +'''); } - void test_does_not_report_nested_if_else() async { - await assertNoDiagnostics( - r''' + Future test_does_not_report_nested_if_else() async { + await assertNoDiagnostics(r''' void test(bool a, bool b) { if (a) { if(b){ @@ -173,49 +143,39 @@ void test(bool a, bool b) { print('hello'); } } -''', - ); +'''); } - void test_reports_inner_if_else() async { - await assertDiagnostics( - r''' + Future test_reports_inner_if_else() async { + await assertAutoDiagnostics(''' void test(bool a, bool b) { - if (a) { + ${expectLint('''if (a) { if(b){ print('hello'); } else { print('hello'); } - } + }''')} } -''', - [ - lint(30, 90), - ], - ); +'''); } - void test_reports_on_three_if() async { - await assertDiagnostics( - r''' + Future test_reports_on_three_if() async { + await assertAutoDiagnostics(''' void threeIf(bool a, bool b, bool c) { - if (a) { + ${expectLint('''if (a) { if (b) { if (c) { print('hello'); } } - } -}''', - [lint(41, 74)], - ); + }''')} +}'''); } - void test_does_not_report_nested_3_with_else_1() async { - await assertNoDiagnostics( - r''' + Future test_does_not_report_nested_3_with_else_1() async { + await assertNoDiagnostics(r''' void test(bool a, bool b, bool c) { if (a) { if (b) { @@ -227,15 +187,13 @@ void test(bool a, bool b, bool c) { print('hello'); } } -''', - ); +'''); } - void test_reports_nested_3_with_else_2() async { - await assertDiagnostics( - r''' + Future test_reports_nested_3_with_else_2() async { + await assertAutoDiagnostics(''' void test(bool a, bool b, bool c) { - if (a) { + ${expectLint('''if (a) { if (b) { if (c) { print('nested'); @@ -243,20 +201,15 @@ void test(bool a, bool b, bool c) { } else { print('nested'); } - } + }''')} } -''', - [ - lint(38, 115), - ], - ); +'''); } - void test_reports_nested_3_with_else_3() async { - await assertDiagnostics( - r''' + Future test_reports_nested_3_with_else_3() async { + await assertAutoDiagnostics(''' void test(bool a, bool b, bool c) { - if (a) { + ${expectLint('''if (a) { if (b) { if (c) { print('nested'); @@ -265,65 +218,49 @@ void test(bool a, bool b, bool c) { print('nested'); } } - } + }''')} } -''', - [ - lint(38, 126), - ], - ); +'''); } - void test_reports_2_sequencial_if() async { - await assertDiagnostics( - r''' + Future test_reports_2_sequencial_if() async { + await assertAutoDiagnostics(''' void test(bool a, bool b) { if (a) return; - if (b) { + ${expectLint('''if (b) { print('gello'); - } + }''')} } -''', - [ - lint(47, 32), - ], - ); +'''); } - void test_does_not_report_2_sequencial_if_with_return() async { - await assertNoDiagnostics( - r''' + Future test_does_not_report_2_sequencial_if_with_return() async { + await assertNoDiagnostics(r''' void test(bool a, bool b) { if (a) return; if (b) return; return; } -''', - ); +'''); } - void test_reports_2_sequencial_if_with_return_2() async { - await assertDiagnostics( - r''' + Future test_reports_2_sequencial_if_with_return_2() async { + await assertAutoDiagnostics(''' void test(bool a, bool b) { if (a) return; - if (b) { + ${expectLint('''if (b) { print('hello'); - } + }''')} return; } -''', - [ - lint(47, 32), - ], - ); +'''); } - void test_does_not_report_2_sequencial_with_following_statement() async { - await assertNoDiagnostics( - r''' + Future + test_does_not_report_2_sequencial_with_following_statement() async { + await assertNoDiagnostics(r''' void test(bool a, bool b) { if (a) return; if (b) { @@ -332,69 +269,52 @@ void test(bool a, bool b) { print('after'); } -''', - ); +'''); } - void test_reports_2_sequencial_if_with_something() async { - await assertDiagnostics( - r''' + Future test_reports_2_sequencial_if_with_something() async { + await assertAutoDiagnostics(''' void test(bool a, bool b) { if (a) { print('hello'); } - if (b) { + ${expectLint('''if (b) { print('hello'); - } + }''')} } -''', - [ - lint(65, 32), - ], - ); +'''); } - void test_reports_3_sequencial_if_with_return() async { - await assertDiagnostics( - r''' + Future test_reports_3_sequencial_if_with_return() async { + await assertAutoDiagnostics(''' void test(bool a, bool b, bool c) { if (a) return; if (b) return; - if (c) { + ${expectLint('''if (c) { print('hello'); - } + }''')} return; } -''', - [ - lint(72, 32), - ], - ); +'''); } - void test_reports_3_sequencial_if_with_something_2() async { - await assertDiagnostics( - r''' + Future test_reports_3_sequencial_if_with_something_2() async { + await assertAutoDiagnostics(''' void test(bool a, bool b, bool c) { if (a) return; if (b) { print('hello'); } - if (c) { + ${expectLint('''if (c) { print('hello'); - } + }''')} } -''', - [ - lint(90, 32), - ], - ); +'''); } - void test_does_not_report_if_throw_with_return() async { - await assertNoDiagnostics( - r''' + Future test_does_not_report_if_throw_with_return() async { + await assertNoDiagnostics(r''' void test(bool a) { if (a) { throw ''; @@ -402,13 +322,11 @@ void test(bool a) { return; } -''', - ); +'''); } - void test_does_not_report_if_else_throw() async { - await assertNoDiagnostics( - r''' + Future test_does_not_report_if_else_throw() async { + await assertNoDiagnostics(r''' void test(bool a) { if (a) { print('hello'); @@ -416,79 +334,58 @@ void test(bool a) { throw ''; } } -''', - ); +'''); } - void test_reports_2_sequencial_if_throw() async { - await assertDiagnostics( - r''' + Future test_reports_2_sequencial_if_throw() async { + await assertAutoDiagnostics(''' void test(bool a, bool b) { if (a) throw ''; - if (b) { + ${expectLint('''if (b) { print('hello'); - } + }''')} } -''', - [ - lint(49, 32), - ], - ); +'''); } - void test_reports_2_sequencial_if_throw_with_return() async { - await assertDiagnostics( - r''' + Future test_reports_2_sequencial_if_throw_with_return() async { + await assertAutoDiagnostics(''' void test(bool a, bool b) { if (a) throw ''; - if (b) { + ${expectLint('''if (b) { print('hello'); - } + }''')} return; } -''', - [ - lint(49, 32), - ], - ); +'''); } - void test_reports_3_sequencial_if_throw_with_return() async { - await assertDiagnostics( - r''' + Future test_reports_3_sequencial_if_throw_with_return() async { + await assertAutoDiagnostics(''' void test(bool a, bool b, bool c) { if (a) throw ''; if (b) throw ''; - if (c) { + ${expectLint('''if (c) { print('hello'); - } + }''')} return; } -''', - [ - lint(76, 32), - ], - ); +'''); } - void test_reports_3_sequencial_if_throw_with_something() async { - await assertDiagnostics( - r''' + Future test_reports_3_sequencial_if_throw_with_something() async { + await assertAutoDiagnostics(''' void test(bool a, bool b, bool c) { if (a) throw ''; if (b) { print('hello'); } - if (c) { + ${expectLint('''if (c) { print('hello'); - } + }''')} } -''', - [ - lint(92, 32), - ], - ); +'''); } } diff --git a/test/lints/proper_super_calls/proper_super_calls_rule_test.dart b/test/lints/proper_super_calls/proper_super_calls_rule_test.dart index c043945c..6d73d246 100644 --- a/test/lints/proper_super_calls/proper_super_calls_rule_test.dart +++ b/test/lints/proper_super_calls/proper_super_calls_rule_test.dart @@ -2,6 +2,8 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(ProperSuperCallsRuleTest); @@ -9,132 +11,108 @@ void main() { } @reflectiveTest -class ProperSuperCallsRuleTest extends AnalysisRuleTest { +class ProperSuperCallsRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { @override void setUp() { final flutter = newPackage('flutter'); - flutter.addFile( - 'lib/src/widgets/framework.dart', - r''' + flutter.addFile('lib/src/widgets/framework.dart', r''' abstract class StatefulWidget {} abstract class State { void initState() {} void dispose() {} } -''', - ); +'''); rule = ProperSuperCallsRule(); super.setUp(); } - @override - String get analysisRule => ProperSuperCallsRule.lintName; - - void test_initState_reports_when_super_not_first() async { - await assertDiagnostics( - r''' + Future test_initState_reports_when_super_not_first() async { + await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; class MyWidgetState extends State { @override - void initState() { + void ${expectLint('initState')}() { print('Bad'); super.initState(); } } -''', - [lint(125, 9)], - ); +'''); } - void test_dispose_reports_when_super_not_last() async { - await assertDiagnostics( - r''' + Future test_dispose_reports_when_super_not_last() async { + await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; class MyWidgetState extends State { @override - void dispose() { + void ${expectLint('dispose')}() { super.dispose(); print('Bad'); } } -''', - [lint(125, 7)], - ); +'''); } - void test_reports_even_without_explicit_override_annotation() async { - await assertDiagnostics( - r''' + Future test_reports_even_without_explicit_override_annotation() async { + await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; class MyWidgetState extends State { - void initState() { + void ${expectLint('initState')}() { print('Bad'); super.initState(); } } -''', - [lint(113, 9)], - ); +'''); } - void test_reports_empty_body_missing_super() async { - await assertDiagnostics( - r''' + Future test_reports_empty_body_missing_super() async { + await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; class MyWidgetState extends State { @override - void initState() {} + void ${expectLint('initState')}() {} } -''', - [lint(125, 9)], - ); +'''); } - void test_reports_when_wrong_super_method_is_called() async { - await assertDiagnostics( - r''' + Future test_reports_when_wrong_super_method_is_called() async { + await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; class MyWidgetState extends State { @override - void initState() { + void ${expectLint('initState')}() { super.dispose(); print('Bad'); } } -''', - [lint(125, 9)], - ); +'''); } - void test_reports_in_deep_inheritance() async { - await assertDiagnostics( - r''' + Future test_reports_in_deep_inheritance() async { + await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; abstract class BaseState extends State {} class MyWidgetState extends BaseState { @override - void dispose() { + void ${expectLint('dispose')}() { super.dispose(); print('Bad'); } } -''', - [lint(172, 7)], - ); +'''); } - void test_no_report_for_correct_usage() async { - await assertNoDiagnostics( - r''' + Future test_no_report_for_correct_usage() async { + await assertNoDiagnostics(r''' import 'package:flutter/src/widgets/framework.dart'; class MyWidgetState extends State { @@ -150,13 +128,11 @@ class MyWidgetState extends State { super.dispose(); } } -''', - ); +'''); } - void test_no_report_for_other_methods() async { - await assertNoDiagnostics( - r''' + Future test_no_report_for_other_methods() async { + await assertNoDiagnostics(r''' import 'package:flutter/src/widgets/framework.dart'; class MyWidgetState extends State { @@ -164,13 +140,11 @@ class MyWidgetState extends State { super.initState(); } } -''', - ); +'''); } - void test_no_report_for_async_correct_usage() async { - await assertNoDiagnostics( - r''' + Future test_no_report_for_async_correct_usage() async { + await assertNoDiagnostics(r''' import 'package:flutter/src/widgets/framework.dart'; class MyWidgetState extends State { @@ -188,13 +162,12 @@ class MyWidgetState extends State { await super.dispose(); } } -''', - ); +'''); } - void test_no_report_for_parenthesized_and_async_correct_usage() async { - await assertNoDiagnostics( - r''' + Future + test_no_report_for_parenthesized_and_async_correct_usage() async { + await assertNoDiagnostics(r''' import 'package:flutter/src/widgets/framework.dart'; class MyWidgetState extends State { @@ -212,7 +185,6 @@ class MyWidgetState extends State { (await super.dispose()); } } -''', - ); +'''); } } diff --git a/test/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule_test.dart b/test/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule_test.dart index 2149a1c0..af66db59 100644 --- a/test/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule_test.dart +++ b/test/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule_test.dart @@ -2,6 +2,8 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../../../lints/auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(AvoidFinalWithGetterRuleTest); @@ -9,7 +11,8 @@ void main() { } @reflectiveTest -class AvoidFinalWithGetterRuleTest extends AnalysisRuleTest { +class AvoidFinalWithGetterRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { @override void setUp() { rule = AvoidFinalWithGetterRule(); @@ -17,71 +20,56 @@ class AvoidFinalWithGetterRuleTest extends AnalysisRuleTest { } Future test_reports_on_getter_with_same_name_as_field() async { - await assertDiagnostics( - r''' + await assertAutoDiagnostics(''' class Test { final int _myField = 0; - int get myField => _myField; + ${expectLint('int get myField => _myField;')} } -''', - [lint(44, 28)], - ); +'''); } Future test_reports_on_getter_with_different_name_from_field() async { - await assertDiagnostics( - r''' + await assertAutoDiagnostics(''' class Test { final int _myField = 0; - int get myFieldInt => _myField; + ${expectLint('int get myFieldInt => _myField;')} } -''', - [lint(44, 31)], - ); +'''); } Future test_reports_on_static_getter_with_private_field() async { - await assertDiagnostics( - r''' + await assertAutoDiagnostics(''' class Test { static final int _myField = 0; - static int get myField => _myField; + ${expectLint('static int get myField => _myField;')} } -''', - [lint(51, 35)], - ); +'''); } Future test_reports_on_getter_with_this_property_access() async { - await assertDiagnostics( - r''' + await assertAutoDiagnostics(''' class Test { final int _myField = 0; - int get myField => this._myField; + ${expectLint('int get myField => this._myField;')} } -''', - [lint(44, 33)], - ); +'''); } Future test_reports_on_block_body_getter_returning_private_field() async { - await assertDiagnostics( - r''' + await assertAutoDiagnostics(''' class Test { final int _myField = 0; - int get myField { + ${expectLint('''int get myField { return _myField; - } + }''')} } -''', - [lint(44, 42)], - ); +'''); } Future test_does_not_report_on_getter_that_contains_logic() async { diff --git a/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart b/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart index b5286057..dba8fd06 100644 --- a/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart +++ b/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart @@ -4,6 +4,8 @@ import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader. import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; +import '../../../lints/auto_test_lint_offsets.dart'; + void main() { defineReflectiveSuite(() { defineReflectiveTests(AvoidReturningWidgetsRuleTest); @@ -11,7 +13,8 @@ void main() { } @reflectiveTest -class AvoidReturningWidgetsRuleTest extends AnalysisRuleTest { +class AvoidReturningWidgetsRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { static const _importFlutterWidgets = "import 'package:flutter/widgets.dart';"; static const _mockFlutterWidgetsContent = ''' abstract class Widget { @@ -115,76 +118,66 @@ $_mockAnalysisOptionsContent''', } Future test_reports_on_static_function() async { - await assertDiagnostics( - ''' + await assertAutoDiagnostics(''' $_importFlutterWidgets -Widget avoidReturningWidgets() => const SizedBox(); +${expectLint('Widget avoidReturningWidgets() => const SizedBox();')} -Widget build() { +${expectLint('''Widget build() { return SizedBox(); -} -''', - [lint(40, 51), lint(93, 39)], - ); +}''')} +'''); } Future test_reports_on_methods() async { - await assertDiagnostics( - ''' + await assertAutoDiagnostics(''' $_importFlutterWidgets class BaseWidget extends StatelessWidget { const BaseWidget({super.key}); - Widget decoratedBox() { + ${expectLint('''Widget decoratedBox() { return DecoratedBox(decoration: BoxDecoration()); - } + }''')} } -''', - [lint(119, 81)], - ); +'''); } Future test_reports_on_getters_but_not_setters() async { - await assertDiagnostics( - ''' + await assertAutoDiagnostics(''' $_importFlutterWidgets class BaseWidget extends StatelessWidget { const BaseWidget({super.key}); - Widget get box => SizedBox(); + ${expectLint('Widget get box => SizedBox();')} set box(Widget value) { throw 'unimplemented'; } } -''', - [lint(119, 29)], - ); +'''); } Future test_reports_on_private_members() async { _addBaseWidgetFile(); - await assertDiagnostics( - ''' + await assertAutoDiagnostics(''' $_importFlutterWidgets import 'base_widget.dart'; class MyWidget extends BaseWidget { const MyWidget({super.key}); - Widget _test1() => const SizedBox(); + ${expectLint('Widget _test1() => const SizedBox();')} - Widget _test2() { + ${expectLint('''Widget _test2() { return const SizedBox( child: SizedBox(), ); - } + }''')} - Widget get _test3 => const SizedBox(); + ${expectLint('Widget get _test3 => const SizedBox();')} @override Widget decoratedBox() { @@ -199,21 +192,14 @@ class MyWidget extends BaseWidget { return const SizedBox(); } } -''', - [ - lint(137, 36), - lint(177, 80), - lint(261, 38), - ], - ); +'''); } Future test_does_not_report_on_overridden_members() async { _addBaseWidgetFile(); // Shouldn't report even if not annotated with @override - await assertNoDiagnostics( - ''' + await assertNoDiagnostics(''' $_importFlutterWidgets import 'base_widget.dart'; @@ -230,13 +216,11 @@ class MyWidget extends BaseWidget { return const SizedBox(); } } -''', - ); +'''); } Future test_does_not_report_on_excluded() async { - await assertNoDiagnostics( - ''' + await assertNoDiagnostics(''' $_importFlutterWidgets SizedBox excludeMethod() => const SizedBox(); @@ -251,14 +235,11 @@ class ExcludeWidget extends StatelessWidget { Widget excludeWidgetMethod() => const SizedBox(); } - -''', - ); +'''); } Future test_reports_on_non_matching_excluded() async { - await assertDiagnostics( - ''' + await assertAutoDiagnostics(''' $_importFlutterWidgets SizedBox excludeMethod() => const SizedBox(); @@ -271,7 +252,7 @@ class ExcludeWidget extends StatelessWidget { return const Placeholder(); } - Widget notExcludeWidgetMethod() => const Placeholder(); + ${expectLint('Widget notExcludeWidgetMethod() => const Placeholder();')} } class NotExcludeWidget extends StatelessWidget { @@ -282,13 +263,8 @@ class NotExcludeWidget extends StatelessWidget { return const Placeholder(); } - Widget excludeWidgetMethod() => const SizedBox(); + ${expectLint('Widget excludeWidgetMethod() => const SizedBox();')} } -''', - [ - lint(260, 55), - lint(498, 49), - ], - ); +'''); } } diff --git a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart index 4226997d..4b5c09c1 100644 --- a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart +++ b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart @@ -43,9 +43,6 @@ $_mockAnalysisOptionsContent''', ); } - @override - String get analysisRule => CyclomaticComplexityRule.lintName; - Future test_reports_when_complexity_exceeds_threshold() async { await assertAutoDiagnostics(''' void cyclomaticComplexity() ${expectLint(r'''{ @@ -281,7 +278,6 @@ void test(List l1, List l2, List l3, List l4) ${expectLint(r '''); } - Future test_reports_when_complexity_exceeds_threshold_due_to_logical_and_pattern() async { await assertAutoDiagnostics(''' diff --git a/test/src/lints/member_ordering/member_ordering_rule_test.dart b/test/src/lints/member_ordering/member_ordering_rule_test.dart index 6617d460..30ff152a 100644 --- a/test/src/lints/member_ordering/member_ordering_rule_test.dart +++ b/test/src/lints/member_ordering/member_ordering_rule_test.dart @@ -128,9 +128,6 @@ abstract class State { ); } - @override - String get analysisRule => MemberOrderingRule.lintName; - String _configWith(String pluginConfig) => '''${analysisOptionsContent(rules: [rule.name])} $pluginConfig'''; @@ -344,7 +341,8 @@ class WrongWidgetState extends State { '''); } - Future test_reports_on_non_alphabetical_fields_and_methods_order() async { + Future + test_reports_on_non_alphabetical_fields_and_methods_order() async { await assertAutoDiagnostics(''' class AlphabeticalClass { final b = 1; @@ -417,7 +415,8 @@ class FactoryOrder { '''); } - Future test_does_not_report_on_correct_order_with_custom_config() async { + Future + test_does_not_report_on_correct_order_with_custom_config() async { await assertNoDiagnostics(''' class CorrectOrder { final publicField = 1; @@ -462,7 +461,8 @@ class WrongOrder { '''); } - Future test_reports_on_partially_wrong_order_with_custom_config() async { + Future + test_reports_on_partially_wrong_order_with_custom_config() async { await assertAutoDiagnostics(''' class PartiallyWrongOrder { final publicField = 1; @@ -486,7 +486,8 @@ class PartiallyWrongOrder { '''); } - Future test_does_not_report_on_correct_widget_order_with_custom_config() async { + Future + test_does_not_report_on_correct_widget_order_with_custom_config() async { await assertNoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; @@ -572,7 +573,8 @@ class _WrongWidgetState extends State { '''); } - Future test_reports_on_partially_correct_widget_order_with_custom_config() async { + Future + test_reports_on_partially_correct_widget_order_with_custom_config() async { await assertAutoDiagnostics(''' import 'package:flutter/src/widgets/framework.dart'; diff --git a/test/src/lints/named_parameters_ordering/named_parameters_ordering_rule_test.dart b/test/src/lints/named_parameters_ordering/named_parameters_ordering_rule_test.dart index 7a1e82cd..9ed5e32b 100644 --- a/test/src/lints/named_parameters_ordering/named_parameters_ordering_rule_test.dart +++ b/test/src/lints/named_parameters_ordering/named_parameters_ordering_rule_test.dart @@ -46,9 +46,6 @@ plugins: ); } - @override - String get analysisRule => NamedParametersOrderingRule.lintName; - Future test_does_not_report_correct_constructor_ordering() async { await assertNoDiagnostics(r''' class Base { @@ -259,8 +256,7 @@ void example({ '''); } - Future - test_reports_incorrect_ordering_with_trailing_comments() async { + Future test_reports_incorrect_ordering_with_trailing_comments() async { await assertAutoDiagnostics(''' void example({ int? age, // the age @@ -272,8 +268,7 @@ void example({ '''); } - Future - test_reports_incorrect_ordering_with_mixed_comments() async { + Future test_reports_incorrect_ordering_with_mixed_comments() async { await assertAutoDiagnostics(''' void example({ /// The age of the user. @@ -300,8 +295,7 @@ void example({ '''); } - Future - test_does_not_report_with_partial_custom_config() async { + Future test_does_not_report_with_partial_custom_config() async { newAnalysisOptionsYamlFile(testPackageRootPath, ''' ${analysisOptionsContent(rules: [rule.name])} plugins: diff --git a/test/src/lints/no_empty_block/no_empty_block_rule_test.dart b/test/src/lints/no_empty_block/no_empty_block_rule_test.dart index f71a29e1..0199ea81 100644 --- a/test/src/lints/no_empty_block/no_empty_block_rule_test.dart +++ b/test/src/lints/no_empty_block/no_empty_block_rule_test.dart @@ -24,9 +24,6 @@ class NoEmptyBlockRuleTest extends AnalysisRuleTest with AutoTestLintOffsets { super.setUp(); } - @override - String get analysisRule => NoEmptyBlockRule.lintName; - Future test_reports_on_empty_function() async { await assertAutoDiagnostics(''' void fun() ${expectLint('{}')} @@ -60,7 +57,8 @@ void fun() { '''); } - Future test_does_not_report_on_todo_comment_with_other_comments() async { + Future + test_does_not_report_on_todo_comment_with_other_comments() async { await assertNoDiagnostics(r''' // ignore_for_file: todo void fun() { diff --git a/test/src/lints/no_magic_number/no_magic_number_rule_test.dart b/test/src/lints/no_magic_number/no_magic_number_rule_test.dart index 59df9c7f..46d7b1a8 100644 --- a/test/src/lints/no_magic_number/no_magic_number_rule_test.dart +++ b/test/src/lints/no_magic_number/no_magic_number_rule_test.dart @@ -47,8 +47,7 @@ int fn(int b) { '''); } - Future - test_reports_magic_numbers_in_ternary_comparison() async { + Future test_reports_magic_numbers_in_ternary_comparison() async { await assertAutoDiagnostics(''' bool canDrive(int age, {bool isUSA = false}) { return isUSA ? age >= ${expectLint('16')} : age > ${expectLint('18')}; @@ -308,8 +307,7 @@ void fn() { '''); } - Future - test_does_not_report_anonymous_function_default_param() async { + Future test_does_not_report_anonymous_function_default_param() async { await assertNoDiagnostics(r''' void fn() { ({int value = 7}) {}; @@ -347,8 +345,7 @@ void fn() { '''); } - Future - test_does_not_report_var_with_const_constructor_call() async { + Future test_does_not_report_var_with_const_constructor_call() async { await assertNoDiagnostics(r''' class TestOperation { final double res; @@ -361,8 +358,7 @@ void fn() { '''); } - Future - test_reports_magic_number_in_constructor_inside_list() async { + Future test_reports_magic_number_in_constructor_inside_list() async { await assertAutoDiagnostics(''' class TestOperation { final double res; @@ -376,8 +372,7 @@ void fn() { '''); } - Future - test_does_not_report_const_constructor_inside_list() async { + Future test_does_not_report_const_constructor_inside_list() async { await assertNoDiagnostics(r''' class TestOperation { final double res; diff --git a/test/src/lints/number_of_parameters/number_of_parameters_rule_test.dart b/test/src/lints/number_of_parameters/number_of_parameters_rule_test.dart index 04dd4197..2b7dd7d9 100644 --- a/test/src/lints/number_of_parameters/number_of_parameters_rule_test.dart +++ b/test/src/lints/number_of_parameters/number_of_parameters_rule_test.dart @@ -43,9 +43,6 @@ $_mockAnalysisOptionsContent''', ); } - @override - String get analysisRule => NumberOfParametersRule.lintName; - Future test_reports_on_exceeding_max_parameters() async { await assertAutoDiagnostics(''' void fun${expectLint(r'(int a, int b, int c)')} {} diff --git a/test/src/utils/auto_lint_data.dart b/test/src/utils/auto_lint_data.dart new file mode 100644 index 00000000..f0aa562b --- /dev/null +++ b/test/src/utils/auto_lint_data.dart @@ -0,0 +1,17 @@ +import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart'; + +class AutoLintData { + final String code; + final Pattern? correctionContains; + final List messageContainsAll; + final String? name; + final List? contextMessages; + + const AutoLintData({ + required this.code, + this.correctionContains, + this.messageContainsAll = const [], + this.name, + this.contextMessages, + }); +} diff --git a/test/utils/fake_analysis_options_loader.dart b/test/utils/fake_analysis_options_loader.dart index c8683879..777abb87 100644 --- a/test/utils/fake_analysis_options_loader.dart +++ b/test/utils/fake_analysis_options_loader.dart @@ -10,6 +10,13 @@ class FakeAnalysisOptionsLoader implements AnalysisOptionsLoader { Map? getRuleOptions(RuleContext context, String ruleName) => ruleOptions; + @override + Map? getRuleOptionsForFile( + String filePath, + String ruleName, + ) => + ruleOptions; + @override void loadRulesOptionsFromContext(RuleContext context) {} } From 0376fd6dfb15f679437e3becf4eca4194221106f Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Mon, 6 Jul 2026 18:22:08 +0300 Subject: [PATCH 37/49] feat: add exclude_annotation support to avoid_unused_parameters (#312) * feat: add exclude_annotation support to avoid_unused_parameters lint to ignore generated classes * feat: allow configuration parameters to accept single strings in addition to lists and add associated parsing tests * feat: support checking inherited annotations for all declaration types in avoid_unused_parameters lint * refactor: simplify excluded annotations check by iterating through all declarations * feat: allow map values in exclude parameters, generalize Iterable parsing, and fix operator precedence in unused parameter visitor * test: simplify freezed class definitions in avoid_unused_parameters test cases * fix: allow excluded annotations to be matched by full qualified name --------- Co-authored-by: Illia Aihistov --- lib/analysis_options.yaml | 6 +- .../excluded_annotations_list_parameter.dart | 58 +++++++++++++ .../excluded_entities_list_parameter.dart | 8 +- .../excluded_identifiers_list_parameter.dart | 11 ++- .../avoid_unused_parameters_rule.dart | 7 ++ .../avoid_unused_parameters_parameters.dart | 9 ++ .../avoid_unused_parameters_visitor.dart | 9 +- .../avoid_unused_parameters_rule_test.dart | 64 ++++++++++++++ .../parameters/parameters_parsing_test.dart | 87 +++++++++++++++++++ 9 files changed, 249 insertions(+), 10 deletions(-) create mode 100644 lib/src/common/parameters/excluded_annotations_list_parameter.dart create mode 100644 test/src/common/parameters/parameters_parsing_test.dart diff --git a/lib/analysis_options.yaml b/lib/analysis_options.yaml index f615faea..269a6441 100644 --- a/lib/analysis_options.yaml +++ b/lib/analysis_options.yaml @@ -43,14 +43,16 @@ solid_lints: allow_initialized: true ignored_types: - AnimationController - avoid_non_null_assertion: true avoid_returning_widgets: true avoid_unnecessary_return_variable: true avoid_unnecessary_setstate: true avoid_unnecessary_type_assertions: true avoid_unrelated_type_assertions: true - avoid_unused_parameters: true + avoid_unused_parameters: + exclude_annotation: + - freezed + - unfreezed avoid_debug_print_in_release: true avoid_final_with_getter: true diff --git a/lib/src/common/parameters/excluded_annotations_list_parameter.dart b/lib/src/common/parameters/excluded_annotations_list_parameter.dart new file mode 100644 index 00000000..2f740d92 --- /dev/null +++ b/lib/src/common/parameters/excluded_annotations_list_parameter.dart @@ -0,0 +1,58 @@ +import 'package:analyzer/dart/ast/ast.dart'; + +/// A parameter model representing excluded annotations for linting. +/// It defines class-level annotations that indicate when class members +/// should be ignored during analysis. +class ExcludedAnnotationsListParameter { + /// The set of excluded annotation names. + final Set excludedAnnotations; + + /// A common parameter key for analysis_options.yaml + static const String excludeAnnotationKey = 'exclude_annotation'; + + /// Constructor for [ExcludedAnnotationsListParameter] class + ExcludedAnnotationsListParameter({ + required this.excludedAnnotations, + }); + + /// Method for creating from json data + factory ExcludedAnnotationsListParameter.fromJson(Map json) { + final raw = json[excludeAnnotationKey]; + if (raw is Iterable) { + return ExcludedAnnotationsListParameter( + excludedAnnotations: Set.from(raw.whereType()), + ); + } else if (raw is String) { + return ExcludedAnnotationsListParameter( + excludedAnnotations: {raw}, + ); + } + + return ExcludedAnnotationsListParameter( + excludedAnnotations: {}, + ); + } + + /// Returns whether the target node should be ignored during analysis because + /// its enclosing declaration is annotated with one of the excluded + /// annotations. + bool shouldIgnore(Declaration node) { + if (excludedAnnotations.isEmpty) return false; + + AstNode? current = node; + while (current != null) { + if (current is Declaration) { + final hasAnnotation = current.metadata.any((annotation) { + final name = annotation.name.name; + final simpleName = name.split('.').last; + return excludedAnnotations.contains(name) || + excludedAnnotations.contains(simpleName); + }); + if (hasAnnotation) return true; + } + current = current.parent; + } + + return false; + } +} diff --git a/lib/src/common/parameters/excluded_entities_list_parameter.dart b/lib/src/common/parameters/excluded_entities_list_parameter.dart index 34d4277c..c10b6793 100644 --- a/lib/src/common/parameters/excluded_entities_list_parameter.dart +++ b/lib/src/common/parameters/excluded_entities_list_parameter.dart @@ -22,9 +22,13 @@ class ExcludedEntitiesListParameter { /// Method for creating from json data factory ExcludedEntitiesListParameter.fromJson(Map json) { final raw = json['exclude_entity']; - if (raw is List) { + if (raw is Iterable) { return ExcludedEntitiesListParameter( - excludedEntityNames: Set.from(raw), + excludedEntityNames: Set.from(raw.whereType()), + ); + } else if (raw is String) { + return ExcludedEntitiesListParameter( + excludedEntityNames: {raw}, ); } diff --git a/lib/src/common/parameters/excluded_identifiers_list_parameter.dart b/lib/src/common/parameters/excluded_identifiers_list_parameter.dart index 526201a4..484c49cc 100644 --- a/lib/src/common/parameters/excluded_identifiers_list_parameter.dart +++ b/lib/src/common/parameters/excluded_identifiers_list_parameter.dart @@ -43,10 +43,13 @@ class ExcludedIdentifiersListParameter { factory ExcludedIdentifiersListParameter.defaultFromJson( Map json, ) { - final excludeList = - json[ExcludedIdentifiersListParameter.excludeParameterName] - as Iterable? ?? - []; + final raw = json[ExcludedIdentifiersListParameter.excludeParameterName]; + + final excludeList = switch (raw) { + Iterable() => raw, + Map() || String() => [raw], + _ => const [], + }; return ExcludedIdentifiersListParameter.fromJson( excludeList: excludeList, diff --git a/lib/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart b/lib/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart index ab9d7e9c..a1858429 100644 --- a/lib/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart +++ b/lib/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart @@ -11,6 +11,13 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Parameters whose names consist only of underscores are also ignored. /// Overridden methods and methods used as tear-offs are skipped. /// +/// ### Parameters +/// +/// #### exclude_annotation +/// A list of class annotations (such as `freezed` or `unfreezed`) whose +/// constructor parameters should be ignored by this rule (useful for code +/// generation). +/// /// {@template solid_lints.avoid_unused_parameters.example} /// ### Example /// diff --git a/lib/src/lints/avoid_unused_parameters/models/avoid_unused_parameters_parameters.dart b/lib/src/lints/avoid_unused_parameters/models/avoid_unused_parameters_parameters.dart index a6a20cbd..67e5d326 100644 --- a/lib/src/lints/avoid_unused_parameters/models/avoid_unused_parameters_parameters.dart +++ b/lib/src/lints/avoid_unused_parameters/models/avoid_unused_parameters_parameters.dart @@ -1,3 +1,4 @@ +import 'package:solid_lints/src/common/parameters/excluded_annotations_list_parameter.dart'; import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart'; /// A data model class that represents the `avoid_unused_parameters` input @@ -6,15 +7,22 @@ class AvoidUnusedParametersParameters { /// A list of methods that should be excluded from the lint. final ExcludedIdentifiersListParameter exclude; + /// A list of annotations that should be ignored during class check. + final ExcludedAnnotationsListParameter excludeAnnotation; + /// Constructor for [AvoidUnusedParametersParameters] model. AvoidUnusedParametersParameters({ required this.exclude, + required this.excludeAnnotation, }); /// Empty [AvoidUnusedParametersParameters] model, excludes nothing. factory AvoidUnusedParametersParameters.empty() { return AvoidUnusedParametersParameters( exclude: ExcludedIdentifiersListParameter(exclude: []), + excludeAnnotation: ExcludedAnnotationsListParameter( + excludedAnnotations: {}, + ), ); } @@ -22,6 +30,7 @@ class AvoidUnusedParametersParameters { factory AvoidUnusedParametersParameters.fromJson(Map json) { return AvoidUnusedParametersParameters( exclude: ExcludedIdentifiersListParameter.defaultFromJson(json), + excludeAnnotation: ExcludedAnnotationsListParameter.fromJson(json), ); } } diff --git a/lib/src/lints/avoid_unused_parameters/visitors/avoid_unused_parameters_visitor.dart b/lib/src/lints/avoid_unused_parameters/visitors/avoid_unused_parameters_visitor.dart index e10fb310..31cb12ad 100644 --- a/lib/src/lints/avoid_unused_parameters/visitors/avoid_unused_parameters_visitor.dart +++ b/lib/src/lints/avoid_unused_parameters/visitors/avoid_unused_parameters_visitor.dart @@ -45,8 +45,10 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor { final parent = node.parent; final parameters = node.parameters; - if (parent is ClassDeclaration && parent.abstractKeyword != null || + if ((parent is ClassDeclaration && parent.abstractKeyword != null) || node.externalKeyword != null || + node.redirectedConstructor != null || + _hasExcludedAnnotationClass(node) || parameters.parameters.isEmpty) { return; } @@ -71,7 +73,7 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor { final parent = node.parent; final parameters = node.parameters; - if (parent is ClassDeclaration && parent.abstractKeyword != null || + if ((parent is ClassDeclaration && parent.abstractKeyword != null) || node.isAbstract || node.externalKeyword != null || (parameters == null || parameters.parameters.isEmpty)) { @@ -113,6 +115,9 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor { bool _isExcluded(Declaration node) => _parameters.exclude.shouldIgnore(node); + bool _hasExcludedAnnotationClass(ConstructorDeclaration node) => + _parameters.excludeAnnotation.shouldIgnore(node); + Iterable _filterOutUnderscoresAndNamed( AstNode body, Iterable parameters, diff --git a/test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart b/test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart index 3ea643ce..82d5830c 100644 --- a/test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart +++ b/test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart @@ -52,6 +52,7 @@ class Placeholder extends StatelessWidget { 'SimpleClassName', 'exclude', ], + 'exclude_annotation': ['freezed'], }, ); @@ -345,6 +346,69 @@ class SimpleClassName { return; } } +'''); + } + + Future + test_does_not_report_on_redirecting_factory_constructors() async { + await assertNoDiagnostics(r''' +class RedirectingClass { + const factory RedirectingClass({required int parameter}) = _RedirectingClass; +} + +class _RedirectingClass implements RedirectingClass { + final int parameter; + const _RedirectingClass({required this.parameter}); +} +'''); + } + + Future test_does_not_report_on_freezed_classes() async { + await assertNoDiagnostics(r''' +const freezed = Object(); + +@freezed +class Test { + const Test(int unusedParameter); +} +'''); + } + + Future + test_does_not_report_on_excluded_declaration_and_annotation_single_string() async { + final FakeAnalysisOptionsLoader fakeAnalysisOptionsLoader = + FakeAnalysisOptionsLoader( + ruleOptions: { + 'exclude': 'excludeMethod', + 'exclude_annotation': 'freezed', + }, + ); + + rule = AvoidUnusedParametersRule( + analysisOptionsLoader: fakeAnalysisOptionsLoader, + ); + + await assertNoDiagnostics(r''' +const freezed = Object(); + +@freezed +class Test { + const Test(int unusedParameter); +} + +class Meta { + const Meta(); +} +const meta = Meta(); + +@meta.freezed +class TestWithPrefix { + const TestWithPrefix(int unusedParameter); +} + +void excludeMethod(String s) { + return; +} '''); } } diff --git a/test/src/common/parameters/parameters_parsing_test.dart b/test/src/common/parameters/parameters_parsing_test.dart new file mode 100644 index 00000000..f64d303a --- /dev/null +++ b/test/src/common/parameters/parameters_parsing_test.dart @@ -0,0 +1,87 @@ +import 'package:solid_lints/src/common/parameters/excluded_annotations_list_parameter.dart'; +import 'package:solid_lints/src/common/parameters/excluded_entities_list_parameter.dart'; +import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart'; +import 'package:test/test.dart'; + +void main() { + group('ExcludedAnnotationsListParameter', () { + test('parses list of strings', () { + final param = ExcludedAnnotationsListParameter.fromJson({ + 'exclude_annotation': ['MyAnnotation1', 'MyAnnotation2'], + }); + expect(param.excludedAnnotations, containsAll(['MyAnnotation1', 'MyAnnotation2'])); + }); + + test('parses single string', () { + final param = ExcludedAnnotationsListParameter.fromJson({ + 'exclude_annotation': 'MyAnnotation', + }); + expect(param.excludedAnnotations, contains('MyAnnotation')); + expect(param.excludedAnnotations.length, 1); + }); + + test('parses empty or invalid input', () { + final param = ExcludedAnnotationsListParameter.fromJson({}); + expect(param.excludedAnnotations, isEmpty); + }); + }); + + group('ExcludedEntitiesListParameter', () { + test('parses list of strings', () { + final param = ExcludedEntitiesListParameter.fromJson({ + 'exclude_entity': ['mixin', 'enum'], + }); + expect(param.excludedEntityNames, containsAll(['mixin', 'enum'])); + }); + + test('parses single string', () { + final param = ExcludedEntitiesListParameter.fromJson({ + 'exclude_entity': 'mixin', + }); + expect(param.excludedEntityNames, contains('mixin')); + expect(param.excludedEntityNames.length, 1); + }); + + test('parses empty or invalid input', () { + final param = ExcludedEntitiesListParameter.fromJson({}); + expect(param.excludedEntityNames, isEmpty); + }); + }); + + group('ExcludedIdentifiersListParameter', () { + test('parses list of strings and maps', () { + final param = ExcludedIdentifiersListParameter.defaultFromJson({ + 'exclude': [ + 'my_function', + {'class_name': 'MyClass', 'method_name': 'my_method'}, + ], + }); + expect(param.exclude.length, 2); + expect(param.exclude[0].declarationName, 'my_function'); + expect(param.exclude[1].className, 'MyClass'); + expect(param.exclude[1].methodName, 'my_method'); + }); + + test('parses single string', () { + final param = ExcludedIdentifiersListParameter.defaultFromJson({ + 'exclude': 'my_function', + }); + expect(param.exclude.length, 1); + expect(param.exclude[0].declarationName, 'my_function'); + }); + + test('parses single map', () { + final param = ExcludedIdentifiersListParameter.defaultFromJson({ + 'exclude': {'class_name': 'MyClass', 'method_name': 'my_method'}, + }); + expect(param.exclude.length, 1); + expect(param.exclude[0].className, 'MyClass'); + expect(param.exclude[0].methodName, 'my_method'); + }); + + test('parses empty or invalid input', () { + final param = ExcludedIdentifiersListParameter.defaultFromJson({}); + expect(param.exclude, isEmpty); + }); + }); +} From 562cddded5460030e93b5fd7573d10be446eebcb Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Mon, 6 Jul 2026 18:28:15 +0300 Subject: [PATCH 38/49] feat: avoid non null assertion ignored types (#313) * feat: add ignored_types configuration to avoid_non_null_assertion lint rule * refactor: extract analysis options configuration to a constant in avoid_non_null_assertion test * refactor: update ignoredTypes to a Set and improve JSON parsing to support string or map inputs * refactor: update avoid_non_null_assertion parameter parsing to correctly extract keys from maps * refactor: improve type safety and readability of AvoidNonNullAssertionParameters JSON decoding using typed patterns --------- Co-authored-by: Illia Aihistov --- lib/analysis_options.yaml | 5 +- lib/main.dart | 2 +- .../avoid_non_null_assertion_rule.dart | 20 ++++-- .../avoid_non_null_assertion_parameters.dart | 49 ++++++++++++++ .../avoid_non_null_assertion_visitor.dart | 20 +++++- .../avoid_non_null_assertion_rule_test.dart | 66 ++++++++++++++++++- 6 files changed, 151 insertions(+), 11 deletions(-) create mode 100644 lib/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart diff --git a/lib/analysis_options.yaml b/lib/analysis_options.yaml index 269a6441..dd2a9c95 100644 --- a/lib/analysis_options.yaml +++ b/lib/analysis_options.yaml @@ -43,7 +43,10 @@ solid_lints: allow_initialized: true ignored_types: - AnimationController - avoid_non_null_assertion: true + avoid_non_null_assertion: + ignored_types: + - IMap + - BuiltMap avoid_returning_widgets: true avoid_unnecessary_return_variable: true avoid_unnecessary_setstate: true diff --git a/lib/main.dart b/lib/main.dart index 4ab02252..43c08108 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -66,7 +66,7 @@ class SolidLintsPlugin extends Plugin { final lintRules = [ AvoidFinalWithGetterRule(), AvoidGlobalStateRule(), - AvoidNonNullAssertionRule(), + AvoidNonNullAssertionRule(analysisOptionsLoader: analysisLoader), avoidUnnecessaryTypeAssertionsRule, AvoidDebugPrintInReleaseRule(), doubleLiteralFormatRule, diff --git a/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart b/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart index 349297cd..171cbc29 100644 --- a/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart +++ b/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart @@ -1,8 +1,9 @@ -import 'package:analyzer/analysis_rule/analysis_rule.dart'; import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart'; +import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Rule which warns about usages of bang operator ("!") /// as it may result in unexpected runtime exceptions. @@ -35,7 +36,8 @@ import 'package:solid_lints/src/lints/avoid_non_null_assertion/visitors/avoid_no /// final map = {'key': 'value'}; /// map['key']!; /// ``` -class AvoidNonNullAssertionRule extends AnalysisRule { +class AvoidNonNullAssertionRule + extends SolidLintRule { /// Name of the lint static const String lintName = 'avoid_non_null_assertion'; @@ -46,11 +48,12 @@ class AvoidNonNullAssertionRule extends AnalysisRule { ); /// creates an instance of [AvoidNonNullAssertionRule] - AvoidNonNullAssertionRule() - : super( + AvoidNonNullAssertionRule({required super.analysisOptionsLoader}) + : super.withParameters( name: lintName, description: 'Warns about usages of bang operator (!) except valid Map access.', + parametersParser: AvoidNonNullAssertionParameters.fromJson, ); @override @@ -61,6 +64,13 @@ class AvoidNonNullAssertionRule extends AnalysisRule { RuleVisitorRegistry registry, RuleContext context, ) { - registry.addPostfixExpression(this, AvoidNonNullAssertionVisitor(this)); + final parameters = + getParametersForContext(context) ?? + AvoidNonNullAssertionParameters.empty(); + + registry.addPostfixExpression( + this, + AvoidNonNullAssertionVisitor(this, parameters), + ); } } diff --git a/lib/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart b/lib/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart new file mode 100644 index 00000000..e10755e4 --- /dev/null +++ b/lib/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart @@ -0,0 +1,49 @@ +/// A data model class that represents the "avoid non null assertion" input +/// parameters. +class AvoidNonNullAssertionParameters { + /// Types that would be ignored by avoid-non-null-assertion rule + /// + /// Example: + /// + /// ```yaml + /// plugins: + /// solid_lints: + /// diagnostics: + /// avoid_non_null_assertion: + /// ignored_types: + /// - IMap + /// ``` + /// + /// ```dart + /// IMap map; + /// map['key']!; // OK + /// ``` + final Set ignoredTypes; + + /// Constructor for [AvoidNonNullAssertionParameters] model + const AvoidNonNullAssertionParameters({ + required this.ignoredTypes, + }); + + /// Empty [AvoidNonNullAssertionParameters] model, ignores nothing. + factory AvoidNonNullAssertionParameters.empty() => + const AvoidNonNullAssertionParameters( + ignoredTypes: {}, + ); + + /// Method for creating from json data + factory AvoidNonNullAssertionParameters.fromJson(Map json) { + final raw = json['ignored_types']; + final excludeList = switch (raw) { + final Iterable rawList => rawList.whereType().toSet(), + final Map rawMap => + rawMap.keys.whereType().toSet(), + final String rawString => {rawString}, + _ => const {}, + }; + + return AvoidNonNullAssertionParameters( + ignoredTypes: excludeList, + ); + } +} diff --git a/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart b/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart index 1dd32ae2..5e27f29c 100644 --- a/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart +++ b/lib/src/lints/avoid_non_null_assertion/visitors/avoid_non_null_assertion_visitor.dart @@ -3,14 +3,18 @@ import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; +import 'package:solid_lints/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; /// visitor for [AvoidNonNullAssertionRule] class AvoidNonNullAssertionVisitor extends SimpleAstVisitor { /// Rule associated with this visitor final AvoidNonNullAssertionRule rule; + final AvoidNonNullAssertionParameters _parameters; + /// Creates an instance of [AvoidNonNullAssertionVisitor] - AvoidNonNullAssertionVisitor(this.rule); + AvoidNonNullAssertionVisitor(this.rule, this._parameters); @override void visitPostfixExpression(PostfixExpression node) { @@ -18,12 +22,12 @@ class AvoidNonNullAssertionVisitor extends SimpleAstVisitor { return; } - final operand = node.operand; + final operand = node.operand.unParenthesized; if (operand is IndexExpression) { final type = operand.target?.staticType; - if (_isMap(type)) { + if (_isMap(type) || _hasIgnoredType(type)) { return; } } @@ -38,4 +42,14 @@ class AvoidNonNullAssertionVisitor extends SimpleAstVisitor { return type.isDartCoreMap || type.allSupertypes.any((v) => v.isDartCoreMap); } + + bool _hasIgnoredType(DartType? type) { + if (type == null) { + return false; + } + + return type.hasIgnoredType( + ignoredTypes: _parameters.ignoredTypes, + ); + } } diff --git a/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart b/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart index 10cf6ced..001cf982 100644 --- a/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart +++ b/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart @@ -1,4 +1,6 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; @@ -13,10 +15,30 @@ void main() { @reflectiveTest class AvoidNonNullAssertionRuleTest extends AnalysisRuleTest with AutoTestLintOffsets { + static const _mockAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + avoid_non_null_assertion: + ignored_types: + - IMap + - BuiltMap + '''; + @override void setUp() { - rule = AvoidNonNullAssertionRule(); + rule = AvoidNonNullAssertionRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); } Future test_reports_non_null_assertion_on_nullable_value() async { @@ -51,6 +73,48 @@ void m(int? number) { final value = number; } } +'''); + } + + Future test_does_not_report_imap_access() async { + await assertNoDiagnostics(r''' +class IMap { + V? operator [](K key) => null; +} + +void m(IMap map) { + map['key']!; +} +'''); + } + + Future test_does_not_report_parenthesized_map_access() async { + await assertNoDiagnostics(r''' +void m() { + final map = {'key': 'value'}; + (map['key'])!; +} +'''); + } + + Future test_does_not_report_imap_access_with_single_string() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + avoid_non_null_assertion: + ignored_types: IMap''', + ); + await assertNoDiagnostics(r''' +class IMap { + V? operator [](K key) => null; +} + +void m(IMap map) { + map['key']!; +} '''); } } From e3a06fba158f807cedef220ff748dc8ba3474dc6 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Tue, 7 Jul 2026 18:21:54 +0300 Subject: [PATCH 39/49] refactor: migrate prefer_match_file_name (#305) * refactor: migrate prefer_match_file_name * refactor: improve prefer_match_file_name lint logic with switch expressions and centralized parameter initialization * test: add test cases for prefer_match_file_name rule * refactor: move normalization logic to visitor and add multi-sort utility to improve PreferMatchFileName rule encapsulation * refactor: replace hardcoded exclude_entity key with constant in ExcludedEntitiesListParameter --------- Co-authored-by: Illia Aihistov --- lib/main.dart | 2 + .../excluded_entities_list_parameter.dart | 20 +- .../prefer_match_file_name_parameters.dart | 7 + .../prefer_match_file_name_rule.dart | 118 ++++----- .../prefer_match_file_name_visitor.dart | 122 +++++----- lib/src/utils/iterable_utils.dart | 16 ++ .../prefer_match_file_name_enum_test.dart | 5 - ...prefer_match_file_name_extension_test.dart | 5 - .../enum/analysis_options.yaml | 9 - .../enum/test.dart | 15 -- .../extension/analysis_options.yaml | 9 - .../extension/test.dart | 15 -- .../mixin/analysis_options.yaml | 9 - .../mixin/test.dart | 13 - ...fer_match_file_name_ignore_extensions.dart | 5 - .../prefer_match_file_name_mixin_test.dart | 5 - lint_test/prefer_match_file_name_test.dart | 17 -- .../prefer_match_file_name_wrong_test.dart | 10 - .../prefer_match_file_name_rule_test.dart | 223 ++++++++++++++++++ 19 files changed, 375 insertions(+), 250 deletions(-) delete mode 100644 lint_test/prefer_match_file_name_enum_test.dart delete mode 100644 lint_test/prefer_match_file_name_extension_test.dart delete mode 100644 lint_test/prefer_match_file_name_ignore_entity/enum/analysis_options.yaml delete mode 100644 lint_test/prefer_match_file_name_ignore_entity/enum/test.dart delete mode 100644 lint_test/prefer_match_file_name_ignore_entity/extension/analysis_options.yaml delete mode 100644 lint_test/prefer_match_file_name_ignore_entity/extension/test.dart delete mode 100644 lint_test/prefer_match_file_name_ignore_entity/mixin/analysis_options.yaml delete mode 100644 lint_test/prefer_match_file_name_ignore_entity/mixin/test.dart delete mode 100644 lint_test/prefer_match_file_name_ignore_extensions.dart delete mode 100644 lint_test/prefer_match_file_name_mixin_test.dart delete mode 100644 lint_test/prefer_match_file_name_test.dart delete mode 100644 lint_test/prefer_match_file_name_wrong_test.dart create mode 100644 test/src/lints/prefer_match_file_name/prefer_match_file_name_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index 43c08108..b7a6f5db 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -32,6 +32,7 @@ import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; import 'package:solid_lints/src/lints/prefer_last/fixes/prefer_last_fix.dart'; import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; +import 'package:solid_lints/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; import 'package:solid_lints/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart'; import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart'; @@ -84,6 +85,7 @@ class SolidLintsPlugin extends Plugin { preferFirstRule, preferConditionalExpressionsRule, preferLastRule, + PreferMatchFileNameRule(analysisOptionsLoader: analysisLoader), // TODO: Add more lint rules and use analysisLoader // for rules that need parameters // For example: `CyclomaticComplexityRule(analysisLoader)` diff --git a/lib/src/common/parameters/excluded_entities_list_parameter.dart b/lib/src/common/parameters/excluded_entities_list_parameter.dart index c10b6793..4c68b7d0 100644 --- a/lib/src/common/parameters/excluded_entities_list_parameter.dart +++ b/lib/src/common/parameters/excluded_entities_list_parameter.dart @@ -1,11 +1,12 @@ import 'package:analyzer/dart/ast/ast.dart'; /// A model representing "exclude_entity" parameters for linting, defining -/// identifiers (classes, mixins, enums, extensions) to be ignored during -/// analysis. +/// identifiers (classes, mixins, enums, extensions, extension_types) to be +/// ignored during analysis. /// Supported entities: /// - mixin /// - extension +/// - extension_type /// - enum class ExcludedEntitiesListParameter { /// The parameter model @@ -19,12 +20,12 @@ class ExcludedEntitiesListParameter { required this.excludedEntityNames, }); - /// Method for creating from json data - factory ExcludedEntitiesListParameter.fromJson(Map json) { - final raw = json['exclude_entity']; + /// Creates an [ExcludedEntitiesListParameter] from JSON. + factory ExcludedEntitiesListParameter.fromJson(Map json) { + final raw = json[excludeEntityKey]; if (raw is Iterable) { return ExcludedEntitiesListParameter( - excludedEntityNames: Set.from(raw.whereType()), + excludedEntityNames: raw.whereType().toSet(), ); } else if (raw is String) { return ExcludedEntitiesListParameter( @@ -32,9 +33,7 @@ class ExcludedEntitiesListParameter { ); } - return ExcludedEntitiesListParameter( - excludedEntityNames: {}, - ); + return ExcludedEntitiesListParameter(excludedEntityNames: {}); } /// Returns whether the target node should be ignored during analysis. @@ -49,6 +48,9 @@ class ExcludedEntitiesListParameter { } else if (node is ExtensionDeclaration && excludedEntityNames.contains('extension')) { return true; + } else if (node is ExtensionTypeDeclaration && + excludedEntityNames.contains('extension_type')) { + return true; } return false; diff --git a/lib/src/lints/prefer_match_file_name/models/prefer_match_file_name_parameters.dart b/lib/src/lints/prefer_match_file_name/models/prefer_match_file_name_parameters.dart index be5a1245..39662156 100644 --- a/lib/src/lints/prefer_match_file_name/models/prefer_match_file_name_parameters.dart +++ b/lib/src/lints/prefer_match_file_name/models/prefer_match_file_name_parameters.dart @@ -11,6 +11,13 @@ class PreferMatchFileNameParameters { required this.excludeEntity, }); + /// Empty [PreferMatchFileNameParameters] model. + factory PreferMatchFileNameParameters.empty() { + return PreferMatchFileNameParameters( + excludeEntity: ExcludedEntitiesListParameter(excludedEntityNames: {}), + ); + } + /// Method for creating from json data factory PreferMatchFileNameParameters.fromJson(Map json) => PreferMatchFileNameParameters( diff --git a/lib/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart b/lib/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart index c0e075d1..d78556d6 100644 --- a/lib/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart +++ b/lib/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart @@ -1,16 +1,28 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:path/path.dart' as p; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/prefer_match_file_name/models/prefer_match_file_name_parameters.dart'; import 'package:solid_lints/src/lints/prefer_match_file_name/visitors/prefer_match_file_name_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; -import 'package:solid_lints/src/utils/node_utils.dart'; /// Warns about a mismatch between file name and first declared element inside. /// /// This improves navigation by matching file content and file name. /// +/// ### Example config: +/// +/// ```yaml +/// plugins: +/// solid_lints: +/// diagnostics: +/// prefer_match_file_name: +/// exclude_entity: +/// - mixin +/// - extension +/// - extension_type +/// - enum +/// ``` +/// /// ## Tests /// /// State: **Disabled**. @@ -51,80 +63,46 @@ import 'package:solid_lints/src/utils/node_utils.dart'; /// class PreferMatchFileNameRule extends SolidLintRule { - /// This lint rule represents the error if iterable - /// access can be simplified. - static const String lintName = 'prefer_match_file_name'; - static final _onlySymbolsRegex = RegExp('[^a-zA-Z0-9]'); + /// Name of the lint. + static const lintName = 'prefer_match_file_name'; - PreferMatchFileNameRule._(super.config); + static const _code = LintCode( + lintName, + 'File name does not match with first {0} name.', + ); + + @override + DiagnosticCode get diagnosticCode => _code; /// Creates a new instance of [PreferMatchFileNameRule] /// based on the lint configuration. - factory PreferMatchFileNameRule.createRule(CustomLintConfigs configs) { - final config = RuleConfig( - configs: configs, - name: lintName, - paramsParser: PreferMatchFileNameParameters.fromJson, - problemMessage: (value) => - 'File name does not match with first declared element name.', - ); - - return PreferMatchFileNameRule._(config); - } + PreferMatchFileNameRule({ + required super.analysisOptionsLoader, + }) : super.withParameters( + name: lintName, + description: + 'Warns about a mismatch between file name and first declared ' + 'element inside.', + parametersParser: PreferMatchFileNameParameters.fromJson, + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addCompilationUnit((node) { - final excludedEntities = config.parameters.excludeEntity; - - final visitor = PreferMatchFileNameVisitor( - excludedEntities: excludedEntities, - ); - - node.accept(visitor); + super.registerNodeProcessors(registry, context); - if (visitor.declarations.isEmpty) return; + final parameters = + getParametersForContext(context) ?? + PreferMatchFileNameParameters.empty(); - final firstDeclaration = visitor.declarations.first; - - if (_doNormalizedNamesMatch( - resolver.source.fullName, - firstDeclaration.token.lexeme, - )) { - return; - } - - final nodeType = - humanReadableNodeType(firstDeclaration.parent).toLowerCase(); - - reporter.atToken( - firstDeclaration.token, - LintCode( - name: lintName, - problemMessage: 'File name does not match with first $nodeType name.', - ), - ); - }); - } - - bool _doNormalizedNamesMatch(String path, String identifierName) { - final fileName = _normalizePath(path); - final dartIdentifier = _normalizeDartIdentifierName(identifierName); + final visitor = PreferMatchFileNameVisitor( + diagnosticCode: diagnosticCode, + context: context, + excludedEntities: parameters.excludeEntity, + ); - return fileName == dartIdentifier; + registry.addCompilationUnit(this, visitor); } - - String _normalizePath(String s) => p - .basename(s) - .split('.') - .first - .replaceAll(_onlySymbolsRegex, '') - .toLowerCase(); - - String _normalizeDartIdentifierName(String s) => - s.replaceAll(_onlySymbolsRegex, '').toLowerCase(); } diff --git a/lib/src/lints/prefer_match_file_name/visitors/prefer_match_file_name_visitor.dart b/lib/src/lints/prefer_match_file_name/visitors/prefer_match_file_name_visitor.dart index 5f008ddd..8f1f7228 100644 --- a/lib/src/lints/prefer_match_file_name/visitors/prefer_match_file_name_visitor.dart +++ b/lib/src/lints/prefer_match_file_name/visitors/prefer_match_file_name_visitor.dart @@ -1,12 +1,24 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as p; import 'package:solid_lints/src/common/parameters/excluded_entities_list_parameter.dart'; import 'package:solid_lints/src/lints/prefer_match_file_name/models/declaration_token_info.dart'; +import 'package:solid_lints/src/utils/iterable_utils.dart'; +import 'package:solid_lints/src/utils/node_utils.dart'; -/// The AST visitor that will collect all Class, Enum, Extension and Mixin -/// declarations -class PreferMatchFileNameVisitor extends RecursiveAstVisitor { - final _declarations = []; +/// The AST visitor that will collect all Class, Enum, Extension, Mixin and +/// Extension Type declarations +class PreferMatchFileNameVisitor extends SimpleAstVisitor { + static final _onlySymbolsRegex = RegExp('[^a-zA-Z0-9]'); + + /// The diagnostic code to report + final DiagnosticCode diagnosticCode; + + /// The rule context + final RuleContext context; /// Iterable that contains the name of entity (or entities) that should /// be ignored @@ -14,70 +26,72 @@ class PreferMatchFileNameVisitor extends RecursiveAstVisitor { /// Constructor of [PreferMatchFileNameVisitor] class PreferMatchFileNameVisitor({ + required this.diagnosticCode, + required this.context, required this.excludedEntities, }); - /// List of all declarations - Iterable get declarations => _declarations.where( - (declaration) { - if (declaration.parent is Declaration) { - return !excludedEntities - .shouldIgnoreEntity(declaration.parent as Declaration); - } - return true; - }, - ).toList() - ..sort( - (a, b) => _publicDeclarationsFirst(a, b) ?? _byDeclarationOrder(a, b), - ); - @override - void visitClassDeclaration(ClassDeclaration node) { - super.visitClassDeclaration(node); + void visitCompilationUnit(CompilationUnit node) { + final declarations = node.declarations + .whereNot(excludedEntities.shouldIgnoreEntity) + .map( + (d) { + final token = switch (d) { + ClassDeclaration() => d.namePart.typeName, + ExtensionDeclaration() => d.name, + MixinDeclaration() => d.name, + EnumDeclaration() => d.namePart.typeName, + ExtensionTypeDeclaration() => d.primaryConstructor.typeName, + _ => null, + }; - _declarations.add((token: node.name, parent: node)); - } + return token == null ? null : (token: token, parent: d); + }, + ) + .nonNulls + .multiSortedBy( + [ + (t) => Identifier.isPrivateName(t.token.lexeme) ? 1 : 0, + (t) => t.token.offset, + ], + ); - @override - void visitExtensionDeclaration(ExtensionDeclaration node) { - super.visitExtensionDeclaration(node); + if (declarations.isEmpty) return; - final name = node.name; - if (name != null) { - _declarations.add((token: name, parent: node)); + final firstDeclaration = declarations.first; + final fullName = context.currentUnit?.file.path; + + if (fullName != null && + _doNormalizedNamesMatch( + fullName, + firstDeclaration.token.lexeme, + )) { + return; } - } - @override - void visitMixinDeclaration(MixinDeclaration node) { - super.visitMixinDeclaration(node); + final nodeType = humanReadableNodeType( + firstDeclaration.parent, + ).toLowerCase(); - _declarations.add((token: node.name, parent: node)); + final reporter = context.currentUnit?.diagnosticReporter; + reporter?.atToken( + firstDeclaration.token, + diagnosticCode, + arguments: [nodeType], + ); } - @override - void visitEnumDeclaration(EnumDeclaration node) { - super.visitEnumDeclaration(node); + bool _doNormalizedNamesMatch(String path, String identifierName) { + final fileName = _normalizePath(path); + final dartIdentifier = _normalizeDartIdentifierName(identifierName); - _declarations.add((token: node.name, parent: node)); + return fileName == dartIdentifier; } - int? _publicDeclarationsFirst( - DeclarationTokenInfo a, - DeclarationTokenInfo b, - ) { - final isAPrivate = Identifier.isPrivateName(a.token.lexeme); - final isBPrivate = Identifier.isPrivateName(b.token.lexeme); - if (!isAPrivate && isBPrivate) { - return -1; - } else if (isAPrivate && !isBPrivate) { - return 1; - } - // no reorder needed; - return null; - } + String _normalizePath(String s) => + _normalizeDartIdentifierName(p.basename(s).split('.').first); - int _byDeclarationOrder(DeclarationTokenInfo a, DeclarationTokenInfo b) { - return a.token.offset.compareTo(b.token.offset); - } + String _normalizeDartIdentifierName(String s) => + s.replaceAll(_onlySymbolsRegex, '').toLowerCase(); } diff --git a/lib/src/utils/iterable_utils.dart b/lib/src/utils/iterable_utils.dart index 9f569e21..f7037447 100644 --- a/lib/src/utils/iterable_utils.dart +++ b/lib/src/utils/iterable_utils.dart @@ -1,3 +1,19 @@ +import 'package:collection/collection.dart'; + +/// Extension on [Iterable] to sort by multiple keys. +extension MultiSortedByIterable on Iterable { + /// Sorts this iterable by multiple keys sequentially. + /// + /// The first key selector that produces a non-zero comparison is used. + List multiSortedBy(List Function(T)> keys) => sorted( + (a, b) => + keys + .map((key) => key(a).compareTo(key(b))) + .firstWhereOrNull((comparison) => comparison != 0) ?? + 0, + ); +} + /// Extension on [Iterable] that provides a [pairwise] method for grouping /// elements. extension IterablePairwise on Iterable { diff --git a/lint_test/prefer_match_file_name_enum_test.dart b/lint_test/prefer_match_file_name_enum_test.dart deleted file mode 100644 index 91da9904..00000000 --- a/lint_test/prefer_match_file_name_enum_test.dart +++ /dev/null @@ -1,5 +0,0 @@ -// ignore_for_file: unused_element, unused_field - -/// Check the `prefer_match_file_name` rule -// expect_lint: prefer_match_file_name -enum WrongMixin { a, b } diff --git a/lint_test/prefer_match_file_name_extension_test.dart b/lint_test/prefer_match_file_name_extension_test.dart deleted file mode 100644 index dd5ea1fc..00000000 --- a/lint_test/prefer_match_file_name_extension_test.dart +++ /dev/null @@ -1,5 +0,0 @@ -// ignore_for_file: unused_element, unused_field - -/// Check the `prefer_match_file_name` rule -// expect_lint: prefer_match_file_name -extension WrongExtension on List {} diff --git a/lint_test/prefer_match_file_name_ignore_entity/enum/analysis_options.yaml b/lint_test/prefer_match_file_name_ignore_entity/enum/analysis_options.yaml deleted file mode 100644 index c6f0c8a1..00000000 --- a/lint_test/prefer_match_file_name_ignore_entity/enum/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -analyzer: - plugins: - - ../../../custom_lint - -custom_lint: - rules: - - prefer_match_file_name: - exclude_entity: - - enum diff --git a/lint_test/prefer_match_file_name_ignore_entity/enum/test.dart b/lint_test/prefer_match_file_name_ignore_entity/enum/test.dart deleted file mode 100644 index d73232dd..00000000 --- a/lint_test/prefer_match_file_name_ignore_entity/enum/test.dart +++ /dev/null @@ -1,15 +0,0 @@ -// ignore_for_file: unused_element, unused_field - -/// `prefer_match_file_name` rule will be ignored by this enttiy because of -/// exclude_entity: -/// - enum -/// in analysis_options.yaml -enum Ignored { _ } - -enum IgnoredAgain { _ } - -// expect_lint: prefer_match_file_name -abstract class WrongNamedClass {} - -/// Only first public element declaration is checked -class PreferMatchFileNameIgnoreExtensions {} diff --git a/lint_test/prefer_match_file_name_ignore_entity/extension/analysis_options.yaml b/lint_test/prefer_match_file_name_ignore_entity/extension/analysis_options.yaml deleted file mode 100644 index 8243e1fe..00000000 --- a/lint_test/prefer_match_file_name_ignore_entity/extension/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -analyzer: - plugins: - - ../../../custom_lint - -custom_lint: - rules: - - prefer_match_file_name: - exclude_entity: - - extension diff --git a/lint_test/prefer_match_file_name_ignore_entity/extension/test.dart b/lint_test/prefer_match_file_name_ignore_entity/extension/test.dart deleted file mode 100644 index c4925cfa..00000000 --- a/lint_test/prefer_match_file_name_ignore_entity/extension/test.dart +++ /dev/null @@ -1,15 +0,0 @@ -// ignore_for_file: unused_element, unused_field - -/// `prefer_match_file_name` rule will be ignored by this extension because of -/// exclude_entity: -/// - extension -/// in analysis_options.yaml -extension Ignored on String {} - -extension IgnoredAgain on String {} - -// expect_lint: prefer_match_file_name -abstract class WrongNamedClass {} - -/// Only first public element declaration is checked -class PreferMatchFileNameIgnoreExtensions {} diff --git a/lint_test/prefer_match_file_name_ignore_entity/mixin/analysis_options.yaml b/lint_test/prefer_match_file_name_ignore_entity/mixin/analysis_options.yaml deleted file mode 100644 index e4206812..00000000 --- a/lint_test/prefer_match_file_name_ignore_entity/mixin/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -analyzer: - plugins: - - ../../../custom_lint - -custom_lint: - rules: - - prefer_match_file_name: - exclude_entity: - - mixin diff --git a/lint_test/prefer_match_file_name_ignore_entity/mixin/test.dart b/lint_test/prefer_match_file_name_ignore_entity/mixin/test.dart deleted file mode 100644 index 70455afc..00000000 --- a/lint_test/prefer_match_file_name_ignore_entity/mixin/test.dart +++ /dev/null @@ -1,13 +0,0 @@ -// ignore_for_file: unused_element, unused_field - -/// `prefer_match_file_name` rule will be ignored by this entity because of -/// exclude_entity: -/// - mixin -/// in analysis_options.yaml -mixin IgnoredMixin {} - -// expect_lint: prefer_match_file_name -abstract class WrongNamedClass {} - -/// Only first public element declaration is checked -class PreferMatchFileNameIgnoreExtensions {} diff --git a/lint_test/prefer_match_file_name_ignore_extensions.dart b/lint_test/prefer_match_file_name_ignore_extensions.dart deleted file mode 100644 index 1af4d330..00000000 --- a/lint_test/prefer_match_file_name_ignore_extensions.dart +++ /dev/null @@ -1,5 +0,0 @@ -// ignore_for_file: unused_element, unused_field - -/// Check if the `prefer_match_file_name` rule ignored for extensions -// expect_lint: prefer_match_file_name -extension DefaultExtension on String {} diff --git a/lint_test/prefer_match_file_name_mixin_test.dart b/lint_test/prefer_match_file_name_mixin_test.dart deleted file mode 100644 index 5b8d2331..00000000 --- a/lint_test/prefer_match_file_name_mixin_test.dart +++ /dev/null @@ -1,5 +0,0 @@ -// ignore_for_file: unused_element, unused_field - -/// Check the `prefer_match_file_name` rule -// expect_lint: prefer_match_file_name -mixin WrongMixin {} diff --git a/lint_test/prefer_match_file_name_test.dart b/lint_test/prefer_match_file_name_test.dart deleted file mode 100644 index 68466050..00000000 --- a/lint_test/prefer_match_file_name_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -// ignore_for_file: unused_element, unused_field - -/// Check the `prefer_match_file_name` rule -class _AnotherPrivateClass {} - -class PreferMatchFileNameTest {} - -/// Only first public element declaration is checked -class WrongClass {} - -class _PrivateClass {} - -extension _PrivateExtension on PreferMatchFileNameTest {} - -enum _PrivateEnum { a, b } - -mixin _PrivateMixin {} diff --git a/lint_test/prefer_match_file_name_wrong_test.dart b/lint_test/prefer_match_file_name_wrong_test.dart deleted file mode 100644 index b0588354..00000000 --- a/lint_test/prefer_match_file_name_wrong_test.dart +++ /dev/null @@ -1,10 +0,0 @@ -// ignore_for_file: unused_element, unused_field - -/// Check the `prefer_match_file_name` rule -class _AnotherPrivateClass {} - -// expect_lint: prefer_match_file_name -class WrongClass {} - -/// Only first public element declaration is checked -class PreferMatchFileNameWrongTest {} diff --git a/test/src/lints/prefer_match_file_name/prefer_match_file_name_rule_test.dart b/test/src/lints/prefer_match_file_name/prefer_match_file_name_rule_test.dart new file mode 100644 index 00000000..ff27dfc7 --- /dev/null +++ b/test/src/lints/prefer_match_file_name/prefer_match_file_name_rule_test.dart @@ -0,0 +1,223 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferMatchFileNameRuleTest); + }); +} + +@reflectiveTest +class PreferMatchFileNameRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + static const _excludeEntitiesOptions = ''' +plugins: + solid_lints: + diagnostics: + prefer_match_file_name: + exclude_entity: + - extension + - enum + - mixin + - extension_type +'''; + + @override + void setUp() { + rule = PreferMatchFileNameRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + super.setUp(); + } + + void test_does_not_report_when_class_name_matches_file_name() async { + await assertNoDiagnostics(r''' +class Test {} +'''); + } + + void test_reports_when_class_name_does_not_match_file_name() async { + await assertAutoDiagnostics(''' +class ${expectLint('WrongClass')} {} +'''); + } + + void test_does_not_report_on_private_class() async { + await assertNoDiagnostics(r''' +class _PrivateClass {} + +class Test {} +'''); + } + + void test_reports_on_first_public_element_with_mismatch() async { + await assertAutoDiagnostics(''' +class _AnotherPrivateClass {} + +class ${expectLint('WrongClass')} {} + +class PreferMatchFileNameWrongTest {} +'''); + } + + void test_reports_enum_name_mismatch() async { + await assertAutoDiagnostics(''' +enum ${expectLint('WrongEnum')} { a, b } +'''); + } + + void test_reports_mixin_name_mismatch() async { + await assertAutoDiagnostics(''' +mixin ${expectLint('WrongMixin')} {} +'''); + } + + void test_reports_extension_name_mismatch() async { + await assertAutoDiagnostics(''' +extension ${expectLint('WrongExtension')} on List {} +'''); + } + + void test_reports_extension_type_mismatch() async { + await assertAutoDiagnostics(''' +extension type ${expectLint('WrongExtensionType')}(int id) {} +'''); + } + + void test_does_not_report_on_private_enum() async { + await assertNoDiagnostics(r''' +enum _PrivateEnum { a, b } + +class Test {} +'''); + } + + void test_does_not_report_on_private_mixin() async { + await assertNoDiagnostics(r''' +mixin _PrivateMixin {} + +class Test {} +'''); + } + + void test_does_not_report_on_private_extension() async { + await assertNoDiagnostics(r''' +extension _PrivateExtension on String {} + +class Test {} +'''); + } + + void test_does_not_report_on_extension_when_excluded() async { + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +$_excludeEntitiesOptions'''); + await assertAutoDiagnostics(''' +extension Ignored on String {} + +extension IgnoredAgain on String {} + +abstract class ${expectLint('WrongNamedClass')} {} + +class PreferMatchFileNameIgnoreExtensions {} +'''); + } + + void test_does_not_report_on_enum_when_excluded() async { + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +$_excludeEntitiesOptions'''); + await assertAutoDiagnostics(''' +enum Ignored { _ } + +enum IgnoredAgain { _ } + +abstract class ${expectLint('WrongNamedClass')} {} + +class PreferMatchFileNameIgnoreExtensions {} +'''); + } + + void test_does_not_report_on_mixin_when_excluded() async { + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +$_excludeEntitiesOptions'''); + await assertAutoDiagnostics(''' +mixin IgnoredMixin {} + +abstract class ${expectLint('WrongNamedClass')} {} + +class PreferMatchFileNameIgnoreExtensions {} +'''); + } + + void test_does_not_report_on_extension_type_when_excluded() async { + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +$_excludeEntitiesOptions'''); + await assertAutoDiagnostics(''' +extension type IgnoredExtensionType(int i) {} + +abstract class ${expectLint('WrongNamedClass')} {} + +class PreferMatchFileNameIgnoreExtensions {} +'''); + } + + void test_does_not_report_on_private_extension_type() async { + await assertNoDiagnostics(r''' +extension type _PrivateExtensionType(int id) {} + +class Test {} +'''); + } + + void test_does_not_report_on_unnamed_extension() async { + await assertNoDiagnostics(r''' +extension on String { + void hello() {} +} + +class Test {} +'''); + } + + void test_does_not_report_on_empty_file() async { + await assertNoDiagnostics(r''' +// Only comments +'''); + } + + void test_does_not_report_on_file_with_only_top_level_members() async { + await assertNoDiagnostics(r''' +void someFunction() {} +final someVariable = 42; +'''); + } + + void test_does_not_report_on_multiple_public_declarations_if_first_matches() async { + await assertNoDiagnostics(r''' +class Test {} +class AnotherPublicClass {} +'''); + } + + void test_reports_when_only_private_class_does_not_match_file_name() async { + await assertAutoDiagnostics(''' +class ${expectLint('_WrongPrivateClass')} {} +'''); + } + + void test_does_not_report_on_only_private_class_when_matching() async { + await assertNoDiagnostics(r''' +class _Test {} +'''); + } +} From b38c6d98889c8616457e3813182d68ca3e8df2c2 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 8 Jul 2026 16:06:05 +0300 Subject: [PATCH 40/49] refactor: migrate avoid_using_api (#304) * refactor: migrate avoid_using_api * refactor: update rootPath parameter to nullable in avoid_using_api_visitor * fix: upgrade avoid_using_api to support per-entry severity levels and multiple lint codes * test: implement getRuleOptionsForFile in FakeAnalysisOptionsLoader * refactor: simplify logic and reduce nesting in AvoidUsingApiVisitor methods * refactor: replace conditional logic with switch expression in AvoidUsingApiVisitor to improve readability and structure * refactor: simplify visitor logic by abstracting reporting into a unified traversal method and moving member checks to extensions * refactor: simplify visitor logic by offloading API matching to node extension methods * refactor: use record destructuring to simplify AvoidUsingApiEntryParameters access in node extensions * style: wrap doc comment for NoMagicNumberRuleVisitor at 80 characters --------- Co-authored-by: Illia Aihistov --- lib/main.dart | 2 + .../avoid_using_api_linter.dart | 386 ----------------- .../avoid_using_api/avoid_using_api_rule.dart | 245 +++++------ .../avoid_using_api_utils.dart | 21 - .../avoid_using_api_entry_parameters.dart | 33 +- .../models/avoid_using_api_parameters.dart | 54 ++- .../avoid_using_api_node_extensions.dart | 227 ++++++++++ .../visitors/avoid_using_api_visitor.dart | 115 +++++ .../no_magic_number_rule_visitor.dart | 3 +- lib/src/utils/node_utils.dart | 52 +++ lib/src/utils/path_utils.dart | 39 +- .../avoid_using_api/analysis_options.yaml | 7 - .../class_source_ban/analysis_options.yaml | 15 - .../lib/class_source_ban_test.dart | 37 -- .../class_source_ban/pubspec.yaml | 17 - .../external_source/lib/banned_library.dart | 1 - .../external_source/lib/external_source.dart | 30 -- .../external_source/pubspec.yaml | 13 - .../analysis_options.yaml | 40 -- .../lib/identifier_class_source_ban_test.dart | 38 -- .../identifier_class_source_ban/pubspec.yaml | 17 - .../analysis_options.yaml | 18 - .../identifier_extension_source_ban_test.dart | 11 - .../pubspec.yaml | 17 - .../analysis_options.yaml | 12 - .../lib/identifier_source_ban_test.dart | 9 - .../identifier_source_ban/pubspec.yaml | 17 - .../named_parameter_ban/analysis_options.yaml | 34 -- .../lib/named_parameter_ban.dart | 16 - .../lib/named_parameter_ban_test.dart | 26 -- .../named_parameter_ban/pubspec.yaml | 17 - .../source_ban/analysis_options.yaml | 18 - .../source_ban/lib/source_ban_test.dart | 6 - .../lib/src/domain/excludes_test.ex.dart | 6 - .../lib/src/domain/includes_test.dart | 9 - .../avoid_using_api/source_ban/pubspec.yaml | 17 - .../source_excludes_ban/analysis_options.yaml | 18 - .../lib/source_excludes_ban_test.dart | 6 - .../lib/src/domain/excludes_test.ex.dart | 6 - .../lib/src/domain/includes_test.dart | 9 - .../source_excludes_ban/pubspec.yaml | 17 - .../source_includes_ban/analysis_options.yaml | 22 - .../lib/source_includes_ban_test.dart | 5 - .../lib/src/domain/excludes_test.ex.dart | 9 - .../lib/src/domain/includes_test.dart | 14 - .../source_includes_ban/pubspec.yaml | 17 - test/lints/auto_lint_data.dart | 19 + .../avoid_using_api_rule_test.dart | 399 ++++++++++++++++++ 48 files changed, 986 insertions(+), 1180 deletions(-) delete mode 100644 lib/src/lints/avoid_using_api/avoid_using_api_linter.dart delete mode 100644 lib/src/lints/avoid_using_api/avoid_using_api_utils.dart create mode 100644 lib/src/lints/avoid_using_api/utils/avoid_using_api_node_extensions.dart create mode 100644 lib/src/lints/avoid_using_api/visitors/avoid_using_api_visitor.dart delete mode 100644 lint_test/avoid_using_api/analysis_options.yaml delete mode 100644 lint_test/avoid_using_api/class_source_ban/analysis_options.yaml delete mode 100644 lint_test/avoid_using_api/class_source_ban/lib/class_source_ban_test.dart delete mode 100644 lint_test/avoid_using_api/class_source_ban/pubspec.yaml delete mode 100644 lint_test/avoid_using_api/external_source/lib/banned_library.dart delete mode 100644 lint_test/avoid_using_api/external_source/lib/external_source.dart delete mode 100644 lint_test/avoid_using_api/external_source/pubspec.yaml delete mode 100644 lint_test/avoid_using_api/identifier_class_source_ban/analysis_options.yaml delete mode 100644 lint_test/avoid_using_api/identifier_class_source_ban/lib/identifier_class_source_ban_test.dart delete mode 100644 lint_test/avoid_using_api/identifier_class_source_ban/pubspec.yaml delete mode 100644 lint_test/avoid_using_api/identifier_extension_source_ban/analysis_options.yaml delete mode 100644 lint_test/avoid_using_api/identifier_extension_source_ban/lib/identifier_extension_source_ban_test.dart delete mode 100644 lint_test/avoid_using_api/identifier_extension_source_ban/pubspec.yaml delete mode 100644 lint_test/avoid_using_api/identifier_source_ban/analysis_options.yaml delete mode 100644 lint_test/avoid_using_api/identifier_source_ban/lib/identifier_source_ban_test.dart delete mode 100644 lint_test/avoid_using_api/identifier_source_ban/pubspec.yaml delete mode 100644 lint_test/avoid_using_api/named_parameter_ban/analysis_options.yaml delete mode 100644 lint_test/avoid_using_api/named_parameter_ban/lib/named_parameter_ban.dart delete mode 100644 lint_test/avoid_using_api/named_parameter_ban/lib/named_parameter_ban_test.dart delete mode 100644 lint_test/avoid_using_api/named_parameter_ban/pubspec.yaml delete mode 100644 lint_test/avoid_using_api/source_ban/analysis_options.yaml delete mode 100644 lint_test/avoid_using_api/source_ban/lib/source_ban_test.dart delete mode 100644 lint_test/avoid_using_api/source_ban/lib/src/domain/excludes_test.ex.dart delete mode 100644 lint_test/avoid_using_api/source_ban/lib/src/domain/includes_test.dart delete mode 100644 lint_test/avoid_using_api/source_ban/pubspec.yaml delete mode 100644 lint_test/avoid_using_api/source_excludes_ban/analysis_options.yaml delete mode 100644 lint_test/avoid_using_api/source_excludes_ban/lib/source_excludes_ban_test.dart delete mode 100644 lint_test/avoid_using_api/source_excludes_ban/lib/src/domain/excludes_test.ex.dart delete mode 100644 lint_test/avoid_using_api/source_excludes_ban/lib/src/domain/includes_test.dart delete mode 100644 lint_test/avoid_using_api/source_excludes_ban/pubspec.yaml delete mode 100644 lint_test/avoid_using_api/source_includes_ban/analysis_options.yaml delete mode 100644 lint_test/avoid_using_api/source_includes_ban/lib/source_includes_ban_test.dart delete mode 100644 lint_test/avoid_using_api/source_includes_ban/lib/src/domain/excludes_test.ex.dart delete mode 100644 lint_test/avoid_using_api/source_includes_ban/lib/src/domain/includes_test.dart delete mode 100644 lint_test/avoid_using_api/source_includes_ban/pubspec.yaml create mode 100644 test/lints/auto_lint_data.dart create mode 100644 test/src/lints/avoid_using_api/avoid_using_api_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index b7a6f5db..d997dbe4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,6 +13,7 @@ import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_un import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart'; import 'package:solid_lints/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart'; import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart'; +import 'package:solid_lints/src/lints/avoid_using_api/avoid_using_api_rule.dart'; import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; @@ -82,6 +83,7 @@ class SolidLintsPlugin extends Plugin { NoEmptyBlockRule(analysisOptionsLoader: analysisLoader), UseNearestContextRule(), NoMagicNumberRule(analysisOptionsLoader: analysisLoader), + AvoidUsingApiRule(analysisOptionsLoader: analysisLoader), preferFirstRule, preferConditionalExpressionsRule, preferLastRule, diff --git a/lib/src/lints/avoid_using_api/avoid_using_api_linter.dart b/lib/src/lints/avoid_using_api/avoid_using_api_linter.dart deleted file mode 100644 index 2f369081..00000000 --- a/lib/src/lints/avoid_using_api/avoid_using_api_linter.dart +++ /dev/null @@ -1,386 +0,0 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/element/element.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:collection/collection.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:path/path.dart' as p; -import 'package:solid_lints/src/lints/avoid_using_api/avoid_using_api_rule.dart'; -import 'package:solid_lints/src/lints/avoid_using_api/avoid_using_api_utils.dart'; -import 'package:solid_lints/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; - -/// Linter for [AvoidUsingApiRule] -class AvoidUsingApiLinter { - /// Linter for [AvoidUsingApiRule] - const AvoidUsingApiLinter({ - required this.resolver, - required this.reporter, - required this.context, - required this.config, - }); - - /// The identifier for the default constructor - static const String _defaultConstructorIdentifier = '()'; - - /// Access to the resolver for this lint context - final CustomLintResolver resolver; - - /// The error reporter to create lints - final DiagnosticReporter reporter; - - /// The current lint context - final CustomLintContext context; - - /// The configuration parameters for the lint - final RuleConfig config; - - bool _matchesSource(String parentSourcePath, String sourceToMatch) { - final Uri parentUri = p.toUri(parentSourcePath); - final Uri sourceUri = p.toUri(sourceToMatch); - - final libraryName = parentUri.pathSegments.first; - final sourceLibraryName = sourceUri.pathSegments.first; - - final matchesLibraryName = libraryName == sourceLibraryName; - final shouldLintFileFromSource = sourceUri.pathSegments.length > 1; - if (shouldLintFileFromSource) { - final sourceFile = sourceUri.pathSegments.sublist(1).join('/'); - final libraryFile = parentUri.pathSegments.sublist(1).join('/'); - - final matchesFile = p.equals(sourceFile, libraryFile); - return matchesLibraryName && matchesFile; - } - - return matchesLibraryName; - } - - /// Lints usages of a given source - void banSource(LintCode entryCode, String source) { - context.registry.addSimpleIdentifier((node) { - final sourcePath = node.sourceUrl; - if (sourcePath == null || !_matchesSource(sourcePath, source)) { - return; - } - - if (node.parent is ConstructorDeclaration) { - return; - } - - reporter.atNode(node, entryCode); - }); - - context.registry.addNamedType((node) { - final sourceName = node.sourceUrl; - if (sourceName == null || !_matchesSource(sourceName, source)) { - return; - } - - reporter.atNode(node, entryCode); - }); - } - - /// Lints usages of a global variable or function from a given source - void banIdFromSource(LintCode entryCode, String identifier, String source) { - // only matches globals - context.registry.addSimpleIdentifier((node) { - final name = node.name; - if (name != identifier) { - return; - } - - final sourcePath = node.sourceUrl; - if (sourcePath == null || !_matchesSource(sourcePath, source)) { - return; - } - - if (node.parent is ConstructorDeclaration) { - return; - } - - switch (node.element) { - case LocalFunctionElement() || - TopLevelFunctionElement() || - PropertyAccessorElement(): - reporter.atNode(node, entryCode); - } - }); - } - - /// Lints usages of a class and its members from a given source - void banClassFromSource( - LintCode entryCode, - String className, - String source, - ) { - context.registry.addVariableDeclaration((node) { - final typeName = node.declaredFragment?.element.type.element?.name; - if (typeName != className) { - return; - } - - final sourcePath = - node.declaredFragment?.element.type.element?.library?.uri.toString(); - if (sourcePath == null || !_matchesSource(sourcePath, source)) { - return; - } - reporter.atNode(node, entryCode); - }); - - context.registry.addSimpleIdentifier((node) { - final parent = node.parent; - if (parent == null) { - return; - } - - if (parent is ConstructorDeclaration) { - return; - } - - final entityBeforeNode = parent.childEntities.firstOrNull; - switch (entityBeforeNode) { - case InstanceCreationExpression(:final staticType?): - case SimpleIdentifier(:final staticType?): - final parentSourcePath = - staticType.element?.library?.uri.toString() ?? node.sourceUrl; - if (parentSourcePath == null || - !_matchesSource(parentSourcePath, source)) { - return; - } - - final parentTypeName = staticType.element?.name; - if (parentTypeName != className) { - return; - } - case SimpleIdentifier(:final element?): - final parentSourcePath = - element.library?.uri.toString() ?? node.sourceUrl; - if (parentSourcePath == null || - !_matchesSource(parentSourcePath, source)) { - return; - } - final parentElementName = element.name; - if (parentElementName != className) { - return; - } - default: - return; - } - - reporter.atNode(parent, entryCode); - }); - - context.registry.addNamedType((node) { - final nodeTypeName = node.name.lexeme; - if (nodeTypeName != className) { - return; - } - - final sourcePath = node.sourceUrl; - if (sourcePath == null || !_matchesSource(sourcePath, source)) { - return; - } - - reporter.atNode( - node.parent?.parent ?? node.parent ?? node, - entryCode, - ); - }); - } - - /// Lints usages of a class member from a given source - void banIdFromClassFromSource( - LintCode entryCode, - String identifier, - String className, - String source, - ) { - if (identifier == _defaultConstructorIdentifier) { - _banDefaultConstructor(className, source, entryCode); - return; - } - - context.registry.addSimpleIdentifier((node) { - final name = node.name; - if (name != identifier) { - return; - } - - final sourcePath = node.sourceUrl; - if (sourcePath == null || !_matchesSource(sourcePath, source)) { - return; - } - - final parent = node.parent; - if (parent == null) { - return; - } - - if (parent is ConstructorDeclaration) { - return; - } - - final entityBeforeNode = parent.childEntities.firstOrNull; - switch (entityBeforeNode) { - case InstanceCreationExpression(:final staticType?): - case SimpleIdentifier(:final staticType?): - final parentTypeName = staticType.element?.name; - if (parentTypeName != className) { - return; - } - - reporter.atNode( - node.parent ?? node, - entryCode, - ); - case SimpleIdentifier(:final element?): - final parentElementName = element.name; - if (parentElementName != className) { - return; - } - - reporter.atNode( - node.parent ?? node, - entryCode, - ); - case NamedType(:final element?): - final parentTypeName = element.name; - if (parentTypeName != className) { - return; - } - - reporter.atNode( - node.parent?.parent ?? node.parent ?? node, - entryCode, - ); - } - }); - - context.registry.addMethodInvocation((node) { - final methodName = node.methodName.name; - if (methodName != identifier) return; - - final enclosingElement = node.methodName.element?.enclosingElement; - if (enclosingElement is! ExtensionElement || - enclosingElement.name != className) { - return; - } - - final sourcePath = enclosingElement.library.uri.toString(); - if (!_matchesSource(sourcePath, source)) { - return; - } - - reporter.atNode(node.methodName, entryCode); - }); - - context.registry.addPrefixedIdentifier((node) { - final propertyName = node.identifier.name; - if (propertyName != identifier) return; - - final enclosingElement = node.identifier.element?.enclosingElement; - if (enclosingElement is! ExtensionElement || - enclosingElement.name != className) { - return; - } - - final sourcePath = enclosingElement.library.uri.toString(); - if (!_matchesSource(sourcePath, source)) return; - - reporter.atNode(node.identifier, entryCode); - }); - } - - void _banDefaultConstructor( - String className, - String source, - LintCode entryCode, - ) { - context.registry.addInstanceCreationExpression((node) { - final constructorName = node.constructorName.type.name.lexeme; - if (constructorName != className || node.constructorName.name != null) { - return; - } - - final sourcePath = - node.constructorName.type.element?.library?.uri.toString(); - if (sourcePath == null || !_matchesSource(sourcePath, source)) { - return; - } - - reporter.atNode(node, entryCode); - }); - } - - /// Lints usages of a named parameter from a given source - void banUsageWithSpecificNamedParameter( - LintCode entryCode, - String identifier, - String namedParameter, - String className, - String source, - ) { - context.registry.addMethodInvocation((node) { - final methodName = node.methodName.name; - if (methodName != identifier) return; - - final enclosingElement = node.methodName.element?.enclosingElement; - if (enclosingElement == null || enclosingElement.name != className) { - return; - } - - if (!_containsNamedParameter(node.argumentList, namedParameter)) { - return; - } - - final libSource = enclosingElement.library; - if (libSource == null) { - return; - } - - final sourcePath = libSource.uri.toString(); - if (!_matchesSource(sourcePath, source)) { - return; - } - - reporter.atNode(node.methodName, entryCode); - }); - - context.registry.addInstanceCreationExpression((node) { - String? expectedConstructorName; - - if (identifier != _defaultConstructorIdentifier) { - expectedConstructorName = identifier; - } - - final actualClassName = node.constructorName.type.name.lexeme; - if (actualClassName != className) { - return; - } - - if (node.constructorName.name?.name != expectedConstructorName) { - return; - } - - if (!_containsNamedParameter(node.argumentList, namedParameter)) { - return; - } - - final sourcePath = - node.constructorName.type.element?.library?.uri.toString(); - if (sourcePath == null || !_matchesSource(sourcePath, source)) { - return; - } - - reporter.atNode(node, entryCode); - }); - } - - bool _containsNamedParameter( - ArgumentList argumentList, - String namedParameter, - ) => - argumentList.arguments.any( - (arg) => - arg is NamedExpression && arg.name.label.name == namedParameter, - ); -} diff --git a/lib/src/lints/avoid_using_api/avoid_using_api_rule.dart b/lib/src/lints/avoid_using_api/avoid_using_api_rule.dart index 1cab45ce..2a03131e 100644 --- a/lib/src/lints/avoid_using_api/avoid_using_api_rule.dart +++ b/lib/src/lints/avoid_using_api/avoid_using_api_rule.dart @@ -1,164 +1,119 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/lints/avoid_using_api/avoid_using_api_linter.dart'; -import 'package:solid_lints/src/lints/avoid_using_api/models/avoid_using_api_entry_parameters.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; -import 'package:solid_lints/src/utils/parameter_utils.dart'; -import 'package:solid_lints/src/utils/path_utils.dart'; +import 'package:solid_lints/src/lints/avoid_using_api/visitors/avoid_using_api_visitor.dart'; +import 'package:solid_lints/src/models/solid_multi_lint_rule.dart'; -/// A `avoid_using_api` rule which -/// warns about usage of avoided APIs +/// A `avoid_using_api` rule which warns about usage of avoided APIs. /// +/// You can configure specific classes, methods, or packages that should be +/// avoided in your codebase, along with custom warning messages. /// -/// ### Usage -/// -/// External code can be "deprecated" when there is a better option available: -/// -/// ```yaml -/// custom_lint: -/// rules: -/// - avoid_using_api: -/// severity: info -/// entries: -/// - class_name: Future -/// identifier: wait -/// source: dart:async -/// reason: "Future.wait should be avoided because it loses type -/// safety for the results. Use a Record's `wait` method -/// instead." -/// severity: warning -/// ``` -/// -/// Result: -/// -/// ```dart -/// void main() async { -/// await Future.wait([...]); // LINT -/// await (...).wait; // OK -/// } -/// ``` -/// -/// ### Advanced Usage -/// -/// Each entry also has `includes` and `excludes` parameters. -/// These paths utilize [Glob](https://pub.dev/packages/glob) -/// patterns to determine if a lint entry should be applied to a file. -/// -/// For example, a lint to prevent usage of a domain-only package outside of the -/// domain folder: +/// ### Example config: /// /// ```yaml -/// custom_lint: -/// rules: -/// - avoid_using_api: -/// severity: info -/// entries: -/// - source: package:domain_models -/// excludes: -/// - "**/domain/**.dart" -/// reason: "domain_models is only intended to be used in the domain -/// layer." +/// plugins: +/// solid_lints: +/// diagnostics: +/// avoid_using_api: +/// avoid_using_api: true +/// severity: warning +/// entries: +/// - class_name: LegacyClient +/// source: package:legacy_api/legacy_api.dart +/// reason: 'Use ModernClient instead.' +/// severity: error +/// - identifier: print +/// source: dart:core +/// reason: 'Use logging framework instead.' /// ``` -/// -/// Contributed by getBoolean (https://github.com/getBoolean). -class AvoidUsingApiRule extends SolidLintRule { - /// This lint rule represents - /// the error whether we use bad formatted double literals. +class AvoidUsingApiRule extends SolidMultiLintRule { + /// This lint name. static const String lintName = 'avoid_using_api'; - AvoidUsingApiRule._(super.config); + /// The default lint message when no reason is provided. + static const String defaultMessage = + 'Usage of this code has been discouraged by your project.'; - /// Creates a new instance of [AvoidUsingApiRule] - /// based on the lint configuration. - factory AvoidUsingApiRule.createRule( - CustomLintConfigs configs, - ) { - final RuleConfig rule = RuleConfig( - configs: configs, - name: lintName, - paramsParser: AvoidUsingApiParameters.fromJson, - problemMessage: (_) => - 'Usage of this code has been discouraged by your project.', - ); + /// Unique name for info level diagnostic codes. + static const infoUniqueName = 'info'; - return AvoidUsingApiRule._(rule); - } + /// Unique name for warning level diagnostic codes. + static const warningUniqueName = 'warning'; + + /// Unique name for error level diagnostic codes. + static const errorUniqueName = 'error'; + + /// Returns the unique name corresponding to the [severity]. + static String getUniqueName(DiagnosticSeverity severity) => + switch (severity) { + DiagnosticSeverity.WARNING => warningUniqueName, + DiagnosticSeverity.ERROR => errorUniqueName, + _ => infoUniqueName, + }; + + /// The lint code for info level diagnostics. + static const infoCode = LintCode( + lintName, + defaultMessage, + uniqueName: infoUniqueName, + ); + + /// The lint code for warning level diagnostics. + static const warningCode = LintCode( + lintName, + defaultMessage, + severity: DiagnosticSeverity.WARNING, + uniqueName: warningUniqueName, + ); + + /// The lint code for error level diagnostics. + static const errorCode = LintCode( + lintName, + defaultMessage, + severity: DiagnosticSeverity.ERROR, + uniqueName: errorUniqueName, + ); @override - Future run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, - ) async { - final rootPath = (await resolver.getResolvedUnitResult()) - .session - .analysisContext - .contextRoot - .root - .path; - final linter = AvoidUsingApiLinter( - resolver: resolver, - reporter: reporter, - context: context, - config: config, - ); + List get diagnosticCodes => [ + infoCode, + warningCode, + errorCode, + ]; - final parameters = config.parameters; - for (final entry in parameters.entries) { - if (shouldSkipFile( - includeGlobs: entry.includes, - excludeGlobs: entry.excludes, - path: resolver.path, - rootPath: rootPath, - )) { - continue; - } + /// Creates a new instance of [AvoidUsingApiRule]. + AvoidUsingApiRule({ + required super.analysisOptionsLoader, + }) : super( + parametersParser: AvoidUsingApiParameters.fromJson, + name: lintName, + description: 'Avoid using specific APIs.', + ); - final entryCode = super.code.copyWith( - errorSeverity: entry.severity ?? - parameters.severity ?? - super.code.errorSeverity, - problemMessage: entry.reason, - ); + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + super.registerNodeProcessors(registry, context); - switch (entry) { - case AvoidUsingApiEntryParameters(:final source) when source == null: - break; - case AvoidUsingApiEntryParameters( - :final identifier?, - :final namedParameter?, - :final className?, - :final source? - ): - linter.banUsageWithSpecificNamedParameter( - entryCode, - identifier, - namedParameter, - className, - source, - ); - case AvoidUsingApiEntryParameters( - :final identifier?, - :final className?, - :final source? - ): - linter.banIdFromClassFromSource( - entryCode, - identifier, - className, - source, - ); - case AvoidUsingApiEntryParameters(:final className?, :final source?): - linter.banClassFromSource(entryCode, className, source); - case AvoidUsingApiEntryParameters(:final identifier?, :final source?): - linter.banIdFromSource(entryCode, identifier, source); - case AvoidUsingApiEntryParameters(:final source?): - linter.banSource(entryCode, source); - case AvoidUsingApiEntryParameters(): - break; - } + final parameters = + getParametersForContext(context) ?? AvoidUsingApiParameters.empty(); + if (parameters.entries.isEmpty) { + return; } + + final visitor = AvoidUsingApiVisitor( + parameters: parameters, + context: context, + ); + + registry.addSimpleIdentifier(this, visitor); + registry.addNamedType(this, visitor); + registry.addVariableDeclaration(this, visitor); + registry.addInstanceCreationExpression(this, visitor); + registry.addMethodInvocation(this, visitor); } } diff --git a/lib/src/lints/avoid_using_api/avoid_using_api_utils.dart b/lib/src/lints/avoid_using_api/avoid_using_api_utils.dart deleted file mode 100644 index ef744380..00000000 --- a/lib/src/lints/avoid_using_api/avoid_using_api_utils.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:analyzer/dart/ast/ast.dart'; - -/// Extension to get library of the node -extension SimpleIdentifierParentSourceExtension on SimpleIdentifier { - /// Returns library of the node - String? get sourceUrl { - final parentSource = element?.library; - final parentSourceName = parentSource?.uri.toString(); - return parentSourceName; - } -} - -/// Extension to get library of the node -extension NamedTypeParentSourceExtension on NamedType { - /// Returns library of the node - String? get sourceUrl { - final parentSource = element?.library; - final parentSourceName = parentSource?.uri.toString(); - return parentSourceName; - } -} diff --git a/lib/src/lints/avoid_using_api/models/avoid_using_api_entry_parameters.dart b/lib/src/lints/avoid_using_api/models/avoid_using_api_entry_parameters.dart index 145116da..03337134 100644 --- a/lib/src/lints/avoid_using_api/models/avoid_using_api_entry_parameters.dart +++ b/lib/src/lints/avoid_using_api/models/avoid_using_api_entry_parameters.dart @@ -51,10 +51,10 @@ class AvoidUsingApiEntryParameters { /// Explain why the code is banned final String? reason; - /// Regex patterns for files to include + /// Glob patterns for files to include final List includes; - /// Regex patterns for files to exclude + /// Glob patterns for files to exclude final List excludes; /// Constructor for [AvoidUsingApiEntryParameters] model @@ -72,19 +72,18 @@ class AvoidUsingApiEntryParameters { /// Method for creating from json data factory AvoidUsingApiEntryParameters.fromJson( Map json, - ) => - AvoidUsingApiEntryParameters( - identifier: json['identifier'] as String?, - namedParameter: json['named_parameter'] as String?, - className: json['class_name'] as String?, - source: json['source'] as String?, - severity: decodeErrorSeverity(json['severity'] as String?), - reason: json['reason'] as String?, - includes: (json['includes'] as List? ?? []) - .whereType() - .toList(), - excludes: (json['excludes'] as List? ?? []) - .whereType() - .toList(), - ); + ) => AvoidUsingApiEntryParameters( + identifier: json['identifier'] as String?, + namedParameter: json['named_parameter'] as String?, + className: json['class_name'] as String?, + source: json['source'] as String?, + severity: decodeErrorSeverity(json['severity'] as String?), + reason: json['reason'] as String?, + includes: (json['includes'] as List? ?? []) + .whereType() + .toList(), + excludes: (json['excludes'] as List? ?? []) + .whereType() + .toList(), + ); } diff --git a/lib/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart b/lib/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart index b7e603a3..6494fcaf 100644 --- a/lib/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart +++ b/lib/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart @@ -9,20 +9,20 @@ import 'package:yaml/yaml.dart'; /// /// Parameters: /// * entries: A list of BannedCodeOption parameters. -/// * severity: The default severity of the lint for each entry. /// /// Example: /// ```yaml -/// custom_lint: -/// rules: -/// - avoid_using_api: -/// severity: error -/// entries: -/// - identifier: wait -/// class_name: Future -/// source: dart:async -/// reason: "Future.wait from dart:async isnt allowed" -/// severity: warning +/// plugins: +/// solid_lints: +/// diagnostics: +/// avoid_using_api: +/// avoid_using_api: error +/// entries: +/// - identifier: wait +/// class_name: Future +/// source: dart:async +/// reason: "Future.wait from dart:async isnt allowed" +/// severity: warning /// ``` class AvoidUsingApiParameters { /// A list of BannedCodeOption parameters. @@ -37,19 +37,27 @@ class AvoidUsingApiParameters { this.severity, }); + /// Empty [AvoidUsingApiParameters] model. + factory AvoidUsingApiParameters.empty() => const AvoidUsingApiParameters(); + /// Method for creating from json data factory AvoidUsingApiParameters.fromJson( Map json, - ) => - AvoidUsingApiParameters( - entries: List.from( - (json['entries'] as Iterable?)?.map( - (e) => AvoidUsingApiEntryParameters.fromJson( - (e as YamlMap).toMap(), - ), - ) ?? - [], - ), - severity: decodeErrorSeverity(json['severity'] as String?), - ); + ) { + final avoidUsingApi = json['avoid_using_api']; + return AvoidUsingApiParameters( + entries: List.from( + (json['entries'] as Iterable?)?.map( + (e) => AvoidUsingApiEntryParameters.fromJson( + (e as YamlMap).toMap(), + ), + ) ?? + [], + ), + severity: decodeErrorSeverity( + json['severity'] as String? ?? + (avoidUsingApi is String ? avoidUsingApi : null), + ), + ); + } } diff --git a/lib/src/lints/avoid_using_api/utils/avoid_using_api_node_extensions.dart b/lib/src/lints/avoid_using_api/utils/avoid_using_api_node_extensions.dart new file mode 100644 index 00000000..3d61f304 --- /dev/null +++ b/lib/src/lints/avoid_using_api/utils/avoid_using_api_node_extensions.dart @@ -0,0 +1,227 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:solid_lints/src/lints/avoid_using_api/models/avoid_using_api_entry_parameters.dart'; +import 'package:solid_lints/src/utils/node_utils.dart'; +import 'package:solid_lints/src/utils/path_utils.dart'; + +const String _defaultConstructorIdentifier = '()'; + +/// Extension on [SimpleIdentifier] to check if it matches avoided API +/// configurations. +extension AvoidUsingApiSimpleIdentifierExtension on SimpleIdentifier { + /// Returns `true` if this identifier matches the given [entry] parameters. + bool matches(AvoidUsingApiEntryParameters entry) { + final AvoidUsingApiEntryParameters( + :source, + :className, + :identifier, + :namedParameter, + ) = entry; + if (source == null) return false; + + return switch ((className, identifier, namedParameter)) { + (final String _, final String _, null) => _matchesIdFromClassFromSource( + entry, + ), + (final String _, null, null) => _matchesClassFromSource(entry), + (null, final String _, null) => _matchesIdFromSource(entry), + (null, null, null) => _matchesSource(entry), + _ => false, + }; + } + + bool _matchesClassFromSource(AvoidUsingApiEntryParameters entry) { + final AvoidUsingApiEntryParameters(:className, :source) = entry; + final parent = this.parent; + final element = this.element; + if (className == null || + source == null || + parent == null || + element == null || + parent is ConstructorDeclaration || + !element.isMemberOrClass( + className: className, + source: source, + )) { + return false; + } + + return true; + } + + bool _matchesIdFromClassFromSource(AvoidUsingApiEntryParameters entry) { + final AvoidUsingApiEntryParameters(:identifier, :className, :source) = + entry; + final parent = this.parent; + final element = this.element; + if (identifier == null || + className == null || + source == null || + element == null || + identifier == _defaultConstructorIdentifier || + name != identifier || + parent == null || + parent is ConstructorDeclaration || + !element.isMemberOrClass( + className: className, + source: source, + )) { + return false; + } + + return true; + } + + bool _matchesSource(AvoidUsingApiEntryParameters entry) { + final source = entry.source; + if (source == null || + !matchesSource(sourceUrl, source) || + parent is ConstructorDeclaration) { + return false; + } + + return true; + } + + bool _matchesIdFromSource(AvoidUsingApiEntryParameters entry) { + final AvoidUsingApiEntryParameters(:identifier, :source) = entry; + if (identifier == null || + source == null || + name != identifier || + !matchesSource(sourceUrl, source) || + parent is ConstructorDeclaration) { + return false; + } + + final el = element; + return el is LocalFunctionElement || + el is TopLevelFunctionElement || + el is PropertyAccessorElement; + } +} + +/// Extension on [NamedType] to check if it matches avoided API configurations. +extension AvoidUsingApiNamedTypeExtension on NamedType { + /// Returns `true` if this named type matches the given [entry] parameters. + bool matches(AvoidUsingApiEntryParameters entry) { + final AvoidUsingApiEntryParameters( + :source, + :className, + :identifier, + ) = entry; + if (source == null || + identifier != null || + !matchesSource(sourceUrl, source) || + (className != null && name.lexeme != className)) { + return false; + } + + return true; + } +} + +/// Extension on [VariableDeclaration] to check if it matches avoided API +/// configurations. +extension AvoidUsingApiVariableDeclarationExtension on VariableDeclaration { + /// Returns `true` if this variable declaration matches the given [entry] + /// parameters. + bool matches(AvoidUsingApiEntryParameters entry) { + final AvoidUsingApiEntryParameters( + :source, + :className, + :identifier, + ) = entry; + final typeElement = declaredType?.element; + if (source == null || + identifier != null || + className == null || + typeElement?.name != className || + !matchesSource(typeElement?.libraryUri, source)) { + return false; + } + + return true; + } +} + +/// Extension on [InstanceCreationExpression] to check if it matches avoided API +/// configurations. +extension AvoidUsingApiInstanceCreationExtension on InstanceCreationExpression { + /// Returns `true` if this instance creation matches the given [entry] + /// parameters. + bool matches(AvoidUsingApiEntryParameters entry) { + final AvoidUsingApiEntryParameters( + :source, + :className, + :identifier, + :namedParameter, + ) = entry; + if (source == null || className == null) return false; + + switch ((identifier, namedParameter)) { + case ( + final String identifier, + final String namedParameter, + ): + // banUsageWithSpecificNamedParameter + String? expectedConstructorName; + if (identifier != _defaultConstructorIdentifier) { + expectedConstructorName = identifier; + } + + final actualClassName = constructorName.type.name.lexeme; + return actualClassName == className && + constructorName.name?.name == expectedConstructorName && + argumentList.containsNamed(namedParameter) && + matchesSource( + constructorName.type.element?.libraryUri, + source, + ); + + case ( + _defaultConstructorIdentifier, + null, + ): + // banIdFromClassFromSource for default constructor + final actualConstructorName = constructorName.type.name.lexeme; + return actualConstructorName == className && + constructorName.name == null && + matchesSource( + constructorName.type.element?.libraryUri, + source, + ); + default: + return false; + } + } +} + +/// Extension on [MethodInvocation] to check if it matches avoided API +/// configurations. +extension AvoidUsingApiMethodInvocationExtension on MethodInvocation { + /// Returns `true` if this method invocation matches the given [entry] + /// parameters. + bool matches(AvoidUsingApiEntryParameters entry) { + final AvoidUsingApiEntryParameters( + :source, + :className, + :identifier, + :namedParameter, + ) = entry; + + final enclosingElement = methodName.element?.enclosingElement; + + if (source == null || + className == null || + namedParameter == null || + methodName.name != identifier || + enclosingElement == null || + enclosingElement.name != className || + !argumentList.containsNamed(namedParameter) || + !matchesSource(enclosingElement.libraryUri, source)) { + return false; + } + + return true; + } +} diff --git a/lib/src/lints/avoid_using_api/visitors/avoid_using_api_visitor.dart b/lib/src/lints/avoid_using_api/visitors/avoid_using_api_visitor.dart new file mode 100644 index 00000000..81b53e93 --- /dev/null +++ b/lib/src/lints/avoid_using_api/visitors/avoid_using_api_visitor.dart @@ -0,0 +1,115 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/syntactic_entity.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_using_api/avoid_using_api_rule.dart'; +import 'package:solid_lints/src/lints/avoid_using_api/models/avoid_using_api_entry_parameters.dart'; +import 'package:solid_lints/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart'; +import 'package:solid_lints/src/lints/avoid_using_api/utils/avoid_using_api_node_extensions.dart'; +import 'package:solid_lints/src/utils/path_utils.dart'; + +/// The AST visitor that checks for avoided API usages. +class AvoidUsingApiVisitor extends SimpleAstVisitor { + /// The parameters configuration. + final AvoidUsingApiParameters parameters; + + /// The rule context. + final RuleContext context; + + List? _cachedActiveEntries; + + /// Creates a new instance of [AvoidUsingApiVisitor]. + AvoidUsingApiVisitor({ + required this.parameters, + required this.context, + }); + + List _getActiveEntries( + String filePath, + String? rootPath, + ) { + return parameters.entries.where((entry) { + return !shouldSkipFile( + includeGlobs: entry.includes, + excludeGlobs: entry.excludes, + path: filePath, + rootPath: rootPath, + ); + }).toList(); + } + + @override + void visitSimpleIdentifier(SimpleIdentifier node) => _visit( + node, + super.visitSimpleIdentifier, + (node, entry) => node.matches(entry) ? node : null, + ); + + @override + void visitNamedType(NamedType node) => _visit( + node, + super.visitNamedType, + (node, entry) => node.matches(entry) ? node : null, + ); + + @override + void visitVariableDeclaration(VariableDeclaration node) => _visit( + node, + super.visitVariableDeclaration, + (node, entry) => node.matches(entry) ? node.name : null, + ); + + @override + void visitInstanceCreationExpression(InstanceCreationExpression node) => + _visit( + node, + super.visitInstanceCreationExpression, + (node, entry) => node.matches(entry) ? node.constructorName.type : null, + ); + + @override + void visitMethodInvocation(MethodInvocation node) => _visit( + node, + super.visitMethodInvocation, + (node, entry) => node.matches(entry) ? node.methodName : null, + ); + + void _visit( + T node, + void Function(T) visitSuper, + SyntacticEntity? Function(T, AvoidUsingApiEntryParameters) visitEntry, + ) { + visitSuper(node); + + final currentUnit = context.currentUnit; + if (currentUnit == null) return; + + final activeEntries = _cachedActiveEntries ??= _getActiveEntries( + currentUnit.file.path, + context.package?.root.path, + ); + + final reporter = currentUnit.diagnosticReporter; + + for (final entry in activeEntries) { + final severity = + entry.severity ?? parameters.severity ?? DiagnosticSeverity.INFO; + + final code = LintCode( + AvoidUsingApiRule.lintName, + entry.reason ?? AvoidUsingApiRule.defaultMessage, + severity: severity, + uniqueName: AvoidUsingApiRule.getUniqueName(severity), + ); + + final target = visitEntry(node, entry); + (() => switch (target) { + AstNode() => reporter.atNode(target, code), + Token() => reporter.atToken(target, code), + _ => (), + })(); + } + } +} diff --git a/lib/src/lints/no_magic_number/visitors/no_magic_number_rule_visitor.dart b/lib/src/lints/no_magic_number/visitors/no_magic_number_rule_visitor.dart index efd6e13b..012aead9 100644 --- a/lib/src/lints/no_magic_number/visitors/no_magic_number_rule_visitor.dart +++ b/lib/src/lints/no_magic_number/visitors/no_magic_number_rule_visitor.dart @@ -6,7 +6,8 @@ import 'package:solid_lints/src/lints/no_magic_number/models/no_magic_number_par import 'package:solid_lints/src/models/solid_lint_rule.dart'; import 'package:solid_lints/src/utils/node_utils.dart'; -/// The AST visitor that checks if double and integer literals are magic numbers. +/// The AST visitor that checks if double and integer literals are magic +/// numbers. class NoMagicNumberRuleVisitor extends SimpleAstVisitor { final SolidLintRule _rule; final NoMagicNumberParameters _parameters; diff --git a/lib/src/utils/node_utils.dart b/lib/src/utils/node_utils.dart index 7afb28a1..9b526184 100644 --- a/lib/src/utils/node_utils.dart +++ b/lib/src/utils/node_utils.dart @@ -1,6 +1,8 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:solid_lints/src/utils/path_utils.dart'; /// Check node is override method from its metadata bool isOverride(List metadata) => metadata.any( @@ -58,6 +60,9 @@ extension SimpleIdentifierExtension on SimpleIdentifier { if (declOffset == null) return false; return declOffset >= body.offset && declOffset < body.end; } + + /// Returns the library URI string of the element, or null. + String? get sourceUrl => element?.libraryUri; } /// Extension on [AstNode] to provide generic context/traversal checks. @@ -103,3 +108,50 @@ extension AstNodeExtension on AstNode { return p is IndexExpression; } } + +/// Extension on [NamedType] to provide source URL utility. +extension NamedTypeExtension on NamedType { + /// Returns the library URI string of the element, or null. + String? get sourceUrl => element?.libraryUri; +} + +/// Extension on [ArgumentList] to check for parameter names. +extension ArgumentListExtension on ArgumentList { + /// Returns `true` if this argument list contains a named parameter argument + /// with the given [name]. + bool containsNamed(String name) => arguments.any( + (arg) => arg is NamedExpression && arg.name.label.name == name, + ); +} + +/// Extension on [Element] to provide element utility checks. +extension ElementExtension on Element { + /// Returns the library URI string of this element, or null. + String? get libraryUri => library?.uri.toString(); + + /// Returns `true` if this element or its enclosing element matches the + /// given [className] and [source]. + bool isMemberOrClass({ + required String className, + required String source, + }) { + final target = this is InterfaceElement ? this : enclosingElement; + + if (target case InterfaceElement() || ExtensionElement() + when target != null) { + return target.name == className && + matchesSource(target.libraryUri, source); + } + + return false; + } +} + +/// Extension on [VariableDeclaration] to check declared type. +extension VariableDeclarationExtension on VariableDeclaration { + /// Returns the type of the declared variable, or null. + DartType? get declaredType { + final element = declaredFragment?.element; + return element is VariableElement ? element.type : null; + } +} diff --git a/lib/src/utils/path_utils.dart b/lib/src/utils/path_utils.dart index 1973c57d..e6ba3a67 100644 --- a/lib/src/utils/path_utils.dart +++ b/lib/src/utils/path_utils.dart @@ -18,30 +18,37 @@ bool shouldSkipFile({ required String path, String? rootPath, }) { + final includes = includeGlobs.map(Glob.new).toList(); + final excludes = excludeGlobs.map(Glob.new).toList(); + final relative = relativePath(path, rootPath); - final shouldAnalyzeFile = - (includeGlobs.isEmpty || _matchesAnyGlob(includeGlobs, relative)) && - (excludeGlobs.isEmpty || _doesNotMatchGlobs(excludeGlobs, relative)); - return !shouldAnalyzeFile; -} -bool _matchesAnyGlob(List globsList, String path) { - final hasMatch = - globsList.map(Glob.new).toList().any((glob) => glob.matches(path)); - return hasMatch; -} + final matchesInclude = includes.isEmpty || + _matchesAny(includes, relative) || + _matchesAny(includes, path); -bool _doesNotMatchGlobs(List globList, String path) { - return !_matchesAnyGlob(globList, path); + final matchesExclude = excludes.isNotEmpty && + (_matchesAny(excludes, relative) || _matchesAny(excludes, path)); + + return !matchesInclude || matchesExclude; } +bool _matchesAny(List globs, String path) => + globs.any((glob) => glob.matches(path)); + /// Converts path to relative using posix style and /// replaces backslashes with forward slashes String relativePath(String path, [String? root]) { - final uriNormlizedPath = p.toUri(path).normalizePath().path; - final uriNormlizedRoot = + final uriNormalizedPath = p.toUri(path).normalizePath().path; + final uriNormalizedRoot = root != null ? p.toUri(root).normalizePath().path : null; - final relative = p.posix.relative(uriNormlizedPath, from: uriNormlizedRoot); - return relative; + return p.posix.relative(uriNormalizedPath, from: uriNormalizedRoot); } + +/// Checks if the given [libraryUri] matches the [targetSource] string +/// (either exact match or as a directory/package prefix). +/// Returns `false` if [libraryUri] is null. +bool matchesSource(String? libraryUri, String targetSource) => + libraryUri != null && + (libraryUri == targetSource || libraryUri.startsWith('$targetSource/')); diff --git a/lint_test/avoid_using_api/analysis_options.yaml b/lint_test/avoid_using_api/analysis_options.yaml deleted file mode 100644 index 18050911..00000000 --- a/lint_test/avoid_using_api/analysis_options.yaml +++ /dev/null @@ -1,7 +0,0 @@ -analyzer: - plugins: - - ../custom_lint - -custom_lint: - rules: - - avoid_using_api: diff --git a/lint_test/avoid_using_api/class_source_ban/analysis_options.yaml b/lint_test/avoid_using_api/class_source_ban/analysis_options.yaml deleted file mode 100644 index 7eae7de0..00000000 --- a/lint_test/avoid_using_api/class_source_ban/analysis_options.yaml +++ /dev/null @@ -1,15 +0,0 @@ -analyzer: - plugins: - - ../../custom_lint - -custom_lint: - rules: - - avoid_using_api: - severity: warning - entries: - - class_name: BannedCodeUsage - source: package:external_source - reason: "BannedCodeUsage from package:external_source is not allowed" - - class_name: UnmodifiableListView - source: dart:collection - reason: "UnmodifiableListView from dart:collection is not allowed" diff --git a/lint_test/avoid_using_api/class_source_ban/lib/class_source_ban_test.dart b/lint_test/avoid_using_api/class_source_ban/lib/class_source_ban_test.dart deleted file mode 100644 index 261172fb..00000000 --- a/lint_test/avoid_using_api/class_source_ban/lib/class_source_ban_test.dart +++ /dev/null @@ -1,37 +0,0 @@ -// ignore_for_file: unused_local_variable - -import 'dart:collection'; - -import 'package:external_source/external_source.dart'; - -void testingBannedCodeLint() { - // expect_lint: avoid_using_api - final bannedCodeUsage = BannedCodeUsage(); - // expect_lint: avoid_using_api - BannedCodeUsage.test2(); - // expect_lint: avoid_using_api - final res = BannedCodeUsage.test2(); - // expect_lint: avoid_using_api - bannedCodeUsage.test(); - - // expect_lint: avoid_using_api - final bannedCodeUsage2 = BannedCodeUsage.test3(); - // expect_lint: avoid_using_api - BannedCodeUsage.test3().test(); - // expect_lint: avoid_using_api - bannedCodeUsage2.test(); - test2; - // expect_lint: avoid_using_api - bannedCodeUsage2.test4; - - // expect_lint: avoid_using_api - final typed = bannedCodeUsage; - - // expect_lint: avoid_using_api - final unmodifiable = UnmodifiableListView([1, 2, 3]); - - // expect_lint: avoid_using_api - final first = unmodifiable.first; -} - -const test2 = 'Hello World'; diff --git a/lint_test/avoid_using_api/class_source_ban/pubspec.yaml b/lint_test/avoid_using_api/class_source_ban/pubspec.yaml deleted file mode 100644 index e8e30186..00000000 --- a/lint_test/avoid_using_api/class_source_ban/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: class_source_ban -description: A sample command-line application. -version: 1.0.0 -publish_to: none - -environment: - sdk: ^3.1.3 - -dependencies: - external_source: - path: ../external_source - -dev_dependencies: - lints: ^3.0.0 - test: ^1.21.0 - solid_lints: - path: ../../../ diff --git a/lint_test/avoid_using_api/external_source/lib/banned_library.dart b/lint_test/avoid_using_api/external_source/lib/banned_library.dart deleted file mode 100644 index d36ffcde..00000000 --- a/lint_test/avoid_using_api/external_source/lib/banned_library.dart +++ /dev/null @@ -1 +0,0 @@ -int banned = 5; diff --git a/lint_test/avoid_using_api/external_source/lib/external_source.dart b/lint_test/avoid_using_api/external_source/lib/external_source.dart deleted file mode 100644 index 144c6bd1..00000000 --- a/lint_test/avoid_using_api/external_source/lib/external_source.dart +++ /dev/null @@ -1,30 +0,0 @@ -// ignore_for_file: prefer_match_file_name -// ignore_for_file: no_empty_block - -class BannedCodeUsage { - final String test4 = 'Hello World'; - - BannedCodeUsage(); - - factory BannedCodeUsage.test3() { - return BannedCodeUsage(); - } - - void test() {} - - static String test2() { - return 'Hello World'; - } -} - -const test2 = 'Hello World'; - -void test() {} - -int banned = 5; - -extension BannedExtension on int { - int banned() => this + 10; - - int get bannedGetter => 10; -} diff --git a/lint_test/avoid_using_api/external_source/pubspec.yaml b/lint_test/avoid_using_api/external_source/pubspec.yaml deleted file mode 100644 index e727de12..00000000 --- a/lint_test/avoid_using_api/external_source/pubspec.yaml +++ /dev/null @@ -1,13 +0,0 @@ -name: external_source -description: A starting point for Dart libraries or applications. -version: 1.0.0 -publish_to: none - -environment: - sdk: ^3.1.3 - -dev_dependencies: - lints: ^3.0.0 - test: ^1.21.0 - solid_lints: - path: ../../../ diff --git a/lint_test/avoid_using_api/identifier_class_source_ban/analysis_options.yaml b/lint_test/avoid_using_api/identifier_class_source_ban/analysis_options.yaml deleted file mode 100644 index c4a4dcf2..00000000 --- a/lint_test/avoid_using_api/identifier_class_source_ban/analysis_options.yaml +++ /dev/null @@ -1,40 +0,0 @@ -analyzer: - plugins: - - ../../custom_lint - -custom_lint: - rules: - - avoid_using_api: - severity: warning - entries: - - identifier: () - class_name: BannedCodeUsage - source: package:external_source - reason: "BannedCodeUsage() from package:external_source is not allowed" - - identifier: test4 - class_name: BannedCodeUsage - source: package:external_source - reason: "BannedCodeUsage.test4 from package:external_source is not allowed" - - identifier: test3 - class_name: BannedCodeUsage - source: package:external_source - reason: "BannedCodeUsage.test3 from package:external_source is not allowed" - - identifier: test2 - class_name: BannedCodeUsage - source: package:external_source - reason: "BannedCodeUsage.test2 from package:external_source is not allowed" - - identifier: test - class_name: BannedCodeUsage - source: package:external_source - reason: "BannedCodeUsage.test from package:external_source is not allowed" - - class_name: UnmodifiableListView - source: dart:collection - reason: "UnmodifiableListView from dart:collection is not allowed" - - class_name: UnmodifiableListView - identifier: first - source: dart:collection - reason: "UnmodifiableListView from dart:collection is not allowed" - - identifier: wait - class_name: Future - source: dart:async - reason: "Future.wait from dart:async is not allowed" diff --git a/lint_test/avoid_using_api/identifier_class_source_ban/lib/identifier_class_source_ban_test.dart b/lint_test/avoid_using_api/identifier_class_source_ban/lib/identifier_class_source_ban_test.dart deleted file mode 100644 index 7e8570e6..00000000 --- a/lint_test/avoid_using_api/identifier_class_source_ban/lib/identifier_class_source_ban_test.dart +++ /dev/null @@ -1,38 +0,0 @@ -// ignore_for_file: unused_local_variable - -import 'dart:collection'; - -import 'package:external_source/external_source.dart'; - -void testingBannedCodeLint() async { - // expect_lint: avoid_using_api - final bannedCodeUsage = BannedCodeUsage(); - // expect_lint: avoid_using_api - BannedCodeUsage.test2(); - // expect_lint: avoid_using_api - final res = BannedCodeUsage.test2(); - // expect_lint: avoid_using_api - bannedCodeUsage.test(); - - // expect_lint: avoid_using_api - final bannedCodeUsage2 = BannedCodeUsage.test3(); - // expect_lint: avoid_using_api - BannedCodeUsage.test3().test(); - // expect_lint: avoid_using_api - bannedCodeUsage2.test(); - test2; - // expect_lint: avoid_using_api - bannedCodeUsage2.test4; - test(); - - // expect_lint: avoid_using_api - final unmodifiable = UnmodifiableListView([1, 2, 3]); - - // expect_lint: avoid_using_api - final first = unmodifiable.first; - - // expect_lint: avoid_using_api - Future.wait([Future.value(1), Future.value(2)]); - - await (Future.value(1), Future.value(2)).wait; -} diff --git a/lint_test/avoid_using_api/identifier_class_source_ban/pubspec.yaml b/lint_test/avoid_using_api/identifier_class_source_ban/pubspec.yaml deleted file mode 100644 index 83756002..00000000 --- a/lint_test/avoid_using_api/identifier_class_source_ban/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: identifier_class_source_ban -description: A sample command-line application. -version: 1.0.0 -publish_to: none - -environment: - sdk: ^3.1.3 - -dependencies: - external_source: - path: ../external_source - -dev_dependencies: - lints: ^3.0.0 - test: ^1.21.0 - solid_lints: - path: ../../../ diff --git a/lint_test/avoid_using_api/identifier_extension_source_ban/analysis_options.yaml b/lint_test/avoid_using_api/identifier_extension_source_ban/analysis_options.yaml deleted file mode 100644 index 3a2b7c69..00000000 --- a/lint_test/avoid_using_api/identifier_extension_source_ban/analysis_options.yaml +++ /dev/null @@ -1,18 +0,0 @@ -analyzer: - plugins: - - ../../custom_lint - -custom_lint: - rules: - - avoid_using_api: - severity: warning - entries: - - class_name: BannedExtension - identifier: banned - source: package:external_source - reason: "Banned identifier from BannedExtension from package:external_source is not allowed" - - class_name: BannedExtension - identifier: bannedGetter - source: package:external_source - reason: "bannedGetter identifier from BannedExtension from package:external_source is not allowed" - diff --git a/lint_test/avoid_using_api/identifier_extension_source_ban/lib/identifier_extension_source_ban_test.dart b/lint_test/avoid_using_api/identifier_extension_source_ban/lib/identifier_extension_source_ban_test.dart deleted file mode 100644 index a3c0a8d0..00000000 --- a/lint_test/avoid_using_api/identifier_extension_source_ban/lib/identifier_extension_source_ban_test.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:external_source/external_source.dart'; - -void extensionIdentifierBanTesting() { - const int a = 10; - - // expect_lint: avoid_using_api - a.banned(); - - // expect_lint: avoid_using_api - a.bannedGetter; -} diff --git a/lint_test/avoid_using_api/identifier_extension_source_ban/pubspec.yaml b/lint_test/avoid_using_api/identifier_extension_source_ban/pubspec.yaml deleted file mode 100644 index 012fa6ca..00000000 --- a/lint_test/avoid_using_api/identifier_extension_source_ban/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: identifier_extension_source_ban -description: A sample command-line application. -version: 1.0.0 -publish_to: none - -environment: - sdk: ^3.7.2 - -dependencies: - external_source: - path: ../external_source - -dev_dependencies: - lints: ^3.0.0 - test: ^1.21.0 - solid_lints: - path: ../../../ diff --git a/lint_test/avoid_using_api/identifier_source_ban/analysis_options.yaml b/lint_test/avoid_using_api/identifier_source_ban/analysis_options.yaml deleted file mode 100644 index 34bd5331..00000000 --- a/lint_test/avoid_using_api/identifier_source_ban/analysis_options.yaml +++ /dev/null @@ -1,12 +0,0 @@ -analyzer: - plugins: - - ../../custom_lint - -custom_lint: - rules: - - avoid_using_api: - severity: warning - entries: - - identifier: test - source: package:external_source - reason: "test methods from package:external_source is not allowed" diff --git a/lint_test/avoid_using_api/identifier_source_ban/lib/identifier_source_ban_test.dart b/lint_test/avoid_using_api/identifier_source_ban/lib/identifier_source_ban_test.dart deleted file mode 100644 index f5e8890b..00000000 --- a/lint_test/avoid_using_api/identifier_source_ban/lib/identifier_source_ban_test.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:external_source/external_source.dart'; - -void testingBannedCodeLint() { - final bannedCodeUsage = BannedCodeUsage(); - bannedCodeUsage.test(); - - // expect_lint: avoid_using_api - test(); -} diff --git a/lint_test/avoid_using_api/identifier_source_ban/pubspec.yaml b/lint_test/avoid_using_api/identifier_source_ban/pubspec.yaml deleted file mode 100644 index 8cd612da..00000000 --- a/lint_test/avoid_using_api/identifier_source_ban/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: identifier_source_ban -description: A starting point for Dart libraries or applications. -version: 1.0.0 -publish_to: none - -environment: - sdk: ^3.1.4 - -dependencies: - external_source: - path: ../external_source - -dev_dependencies: - lints: ^3.0.0 - test: ^1.21.0 - solid_lints: - path: ../../../ diff --git a/lint_test/avoid_using_api/named_parameter_ban/analysis_options.yaml b/lint_test/avoid_using_api/named_parameter_ban/analysis_options.yaml deleted file mode 100644 index 557c6a83..00000000 --- a/lint_test/avoid_using_api/named_parameter_ban/analysis_options.yaml +++ /dev/null @@ -1,34 +0,0 @@ -analyzer: - plugins: - - ../../custom_lint - -custom_lint: - rules: - - avoid_using_api: - severity: warning - entries: - - identifier: () - named_parameter: badParameter - class_name: NamedParameterBan - source: package:named_parameter_ban - reason: "Use goodParameter instead" - - identifier: namedConstructor - named_parameter: badParameter - class_name: NamedParameterBan - source: package:named_parameter_ban - reason: "Use goodParameter instead" - - identifier: staticMethod - named_parameter: badParameter - class_name: NamedParameterBan - source: package:named_parameter_ban - reason: "Use goodParameter instead" - - identifier: method - named_parameter: badParameter - class_name: NamedParameterBan - source: package:named_parameter_ban - reason: "Use goodParameter instead" - - identifier: extensionMethod - named_parameter: badParameter - class_name: NamedParameterBanExtension - source: package:named_parameter_ban - reason: "Use goodParameter instead" diff --git a/lint_test/avoid_using_api/named_parameter_ban/lib/named_parameter_ban.dart b/lint_test/avoid_using_api/named_parameter_ban/lib/named_parameter_ban.dart deleted file mode 100644 index c81889b3..00000000 --- a/lint_test/avoid_using_api/named_parameter_ban/lib/named_parameter_ban.dart +++ /dev/null @@ -1,16 +0,0 @@ -class NamedParameterBan { - NamedParameterBan({this.badParameter, this.goodParameter}); - - String? badParameter; - String? goodParameter; - - NamedParameterBan.namedConstructor( - {String? badParameter, String? goodParameter}) {} - - void method({String? badParameter, String? goodParameter}) {} - static void staticMethod({String? badParameter, String? goodParameter}) {} -} - -extension NamedParameterBanExtension on int { - String extensionMethod({String? badParameter, String? goodParameter}) => ''; -} diff --git a/lint_test/avoid_using_api/named_parameter_ban/lib/named_parameter_ban_test.dart b/lint_test/avoid_using_api/named_parameter_ban/lib/named_parameter_ban_test.dart deleted file mode 100644 index 1a583c72..00000000 --- a/lint_test/avoid_using_api/named_parameter_ban/lib/named_parameter_ban_test.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:named_parameter_ban/named_parameter_ban.dart'; - -void testingBannedCodeLint() async { - // expect_lint: avoid_using_api - NamedParameterBan(badParameter: ''); - NamedParameterBan(goodParameter: ''); - - // expect_lint: avoid_using_api - NamedParameterBan.namedConstructor(badParameter: ''); - NamedParameterBan.namedConstructor(goodParameter: ''); - - // expect_lint: avoid_using_api - NamedParameterBan.staticMethod( - badParameter: 'test', - ); - NamedParameterBan.staticMethod(goodParameter: ''); - - final obj = NamedParameterBan(); - // expect_lint: avoid_using_api - obj.method(badParameter: ''); - obj.method(goodParameter: ''); - - // expect_lint: avoid_using_api - 0.extensionMethod(badParameter: ''); - 0.extensionMethod(goodParameter: ''); -} diff --git a/lint_test/avoid_using_api/named_parameter_ban/pubspec.yaml b/lint_test/avoid_using_api/named_parameter_ban/pubspec.yaml deleted file mode 100644 index db054efe..00000000 --- a/lint_test/avoid_using_api/named_parameter_ban/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: named_parameter_ban -description: A sample command-line application. -version: 1.0.0 -publish_to: none - -environment: - sdk: ^3.1.3 - -dependencies: - external_source: - path: ../external_source - -dev_dependencies: - lints: ^3.0.0 - test: ^1.21.0 - solid_lints: - path: ../../../ diff --git a/lint_test/avoid_using_api/source_ban/analysis_options.yaml b/lint_test/avoid_using_api/source_ban/analysis_options.yaml deleted file mode 100644 index 8cdb4a71..00000000 --- a/lint_test/avoid_using_api/source_ban/analysis_options.yaml +++ /dev/null @@ -1,18 +0,0 @@ -analyzer: - plugins: - - ../../custom_lint - -custom_lint: - rules: - - avoid_using_api: - severity: warning - entries: - - source: dart:collection - reason: "dart:collection should not be used" - # Uses Glob notation, test using https://www.digitalocean.com/community/tools/glob - includes: - - "**/domain/**.dart" - excludes: - - "**/domain/**.ex.dart" - - source: package:external_source/banned_library.dart - reason: "package:external_source/banned_library.dart should not be used anymore" diff --git a/lint_test/avoid_using_api/source_ban/lib/source_ban_test.dart b/lint_test/avoid_using_api/source_ban/lib/source_ban_test.dart deleted file mode 100644 index b05d50c3..00000000 --- a/lint_test/avoid_using_api/source_ban/lib/source_ban_test.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:external_source/banned_library.dart'; - -void testingBannedCodeLint() { - // expect_lint: avoid_using_api - banned; -} diff --git a/lint_test/avoid_using_api/source_ban/lib/src/domain/excludes_test.ex.dart b/lint_test/avoid_using_api/source_ban/lib/src/domain/excludes_test.ex.dart deleted file mode 100644 index 845085d7..00000000 --- a/lint_test/avoid_using_api/source_ban/lib/src/domain/excludes_test.ex.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'dart:collection'; - -void testingBannedCodeLint() { - final unmodifiable = UnmodifiableListView([1, 2, 3]); - unmodifiable.first; -} diff --git a/lint_test/avoid_using_api/source_ban/lib/src/domain/includes_test.dart b/lint_test/avoid_using_api/source_ban/lib/src/domain/includes_test.dart deleted file mode 100644 index 91a0932d..00000000 --- a/lint_test/avoid_using_api/source_ban/lib/src/domain/includes_test.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'dart:collection'; - -void testingBannedCodeLint() { - // expect_lint: avoid_using_api - final unmodifiable = UnmodifiableListView([1, 2, 3]); - - // expect_lint: avoid_using_api - unmodifiable.first; -} diff --git a/lint_test/avoid_using_api/source_ban/pubspec.yaml b/lint_test/avoid_using_api/source_ban/pubspec.yaml deleted file mode 100644 index a1e1c52c..00000000 --- a/lint_test/avoid_using_api/source_ban/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: source_ban -description: A starting point for Dart libraries or applications. -version: 1.0.0 -publish_to: none - -environment: - sdk: ^3.1.3 - -dependencies: - external_source: - path: ../external_source - -dev_dependencies: - lints: ^3.0.0 - test: ^1.21.0 - solid_lints: - path: ../../../ diff --git a/lint_test/avoid_using_api/source_excludes_ban/analysis_options.yaml b/lint_test/avoid_using_api/source_excludes_ban/analysis_options.yaml deleted file mode 100644 index 19acd133..00000000 --- a/lint_test/avoid_using_api/source_excludes_ban/analysis_options.yaml +++ /dev/null @@ -1,18 +0,0 @@ -analyzer: - plugins: - - ../../custom_lint - -custom_lint: - rules: - - avoid_using_api: - severity: warning - entries: - - source: dart:collection - reason: "dart:collection should not be used" - # Uses Glob notation, test using https://www.digitalocean.com/community/tools/glob - excludes: - - "**.ex.dart" - - source: package:external_source/banned_library.dart - reason: "package:external_source/banned_library.dart should not be used anymore" - excludes: - - "**.ex.dart" diff --git a/lint_test/avoid_using_api/source_excludes_ban/lib/source_excludes_ban_test.dart b/lint_test/avoid_using_api/source_excludes_ban/lib/source_excludes_ban_test.dart deleted file mode 100644 index b05d50c3..00000000 --- a/lint_test/avoid_using_api/source_excludes_ban/lib/source_excludes_ban_test.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:external_source/banned_library.dart'; - -void testingBannedCodeLint() { - // expect_lint: avoid_using_api - banned; -} diff --git a/lint_test/avoid_using_api/source_excludes_ban/lib/src/domain/excludes_test.ex.dart b/lint_test/avoid_using_api/source_excludes_ban/lib/src/domain/excludes_test.ex.dart deleted file mode 100644 index 845085d7..00000000 --- a/lint_test/avoid_using_api/source_excludes_ban/lib/src/domain/excludes_test.ex.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'dart:collection'; - -void testingBannedCodeLint() { - final unmodifiable = UnmodifiableListView([1, 2, 3]); - unmodifiable.first; -} diff --git a/lint_test/avoid_using_api/source_excludes_ban/lib/src/domain/includes_test.dart b/lint_test/avoid_using_api/source_excludes_ban/lib/src/domain/includes_test.dart deleted file mode 100644 index 91a0932d..00000000 --- a/lint_test/avoid_using_api/source_excludes_ban/lib/src/domain/includes_test.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'dart:collection'; - -void testingBannedCodeLint() { - // expect_lint: avoid_using_api - final unmodifiable = UnmodifiableListView([1, 2, 3]); - - // expect_lint: avoid_using_api - unmodifiable.first; -} diff --git a/lint_test/avoid_using_api/source_excludes_ban/pubspec.yaml b/lint_test/avoid_using_api/source_excludes_ban/pubspec.yaml deleted file mode 100644 index 8ad20af5..00000000 --- a/lint_test/avoid_using_api/source_excludes_ban/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: source_excludes_ban -description: A starting point for Dart libraries or applications. -version: 1.0.0 -publish_to: none - -environment: - sdk: ^3.1.3 - -dependencies: - external_source: - path: ../external_source - -dev_dependencies: - lints: ^3.0.0 - test: ^1.21.0 - solid_lints: - path: ../../../ diff --git a/lint_test/avoid_using_api/source_includes_ban/analysis_options.yaml b/lint_test/avoid_using_api/source_includes_ban/analysis_options.yaml deleted file mode 100644 index 1c4cb4de..00000000 --- a/lint_test/avoid_using_api/source_includes_ban/analysis_options.yaml +++ /dev/null @@ -1,22 +0,0 @@ -analyzer: - plugins: - - ../../custom_lint - -custom_lint: - rules: - - avoid_using_api: - severity: warning - entries: - - source: dart:collection - reason: "dart:collection should not be used" - # Uses Glob notation, test using https://www.digitalocean.com/community/tools/glob - includes: - - "**/domain/**.dart" - excludes: - - "**.ex.dart" - - source: package:external_source/banned_library.dart - reason: "package:external_source/banned_library.dart should not be used anymore" - includes: - - "**/domain/**.dart" - excludes: - - "**.ex.dart" diff --git a/lint_test/avoid_using_api/source_includes_ban/lib/source_includes_ban_test.dart b/lint_test/avoid_using_api/source_includes_ban/lib/source_includes_ban_test.dart deleted file mode 100644 index e2031dff..00000000 --- a/lint_test/avoid_using_api/source_includes_ban/lib/source_includes_ban_test.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:external_source/banned_library.dart'; - -void testingBannedCodeLint() { - banned; -} diff --git a/lint_test/avoid_using_api/source_includes_ban/lib/src/domain/excludes_test.ex.dart b/lint_test/avoid_using_api/source_includes_ban/lib/src/domain/excludes_test.ex.dart deleted file mode 100644 index bd2b4a82..00000000 --- a/lint_test/avoid_using_api/source_includes_ban/lib/src/domain/excludes_test.ex.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'dart:collection'; - -import 'package:external_source/banned_library.dart'; - -void testingBannedCodeLint() { - final unmodifiable = UnmodifiableListView([1, 2, 3]); - unmodifiable.first; - banned; -} diff --git a/lint_test/avoid_using_api/source_includes_ban/lib/src/domain/includes_test.dart b/lint_test/avoid_using_api/source_includes_ban/lib/src/domain/includes_test.dart deleted file mode 100644 index 0308e417..00000000 --- a/lint_test/avoid_using_api/source_includes_ban/lib/src/domain/includes_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'dart:collection'; - -import 'package:external_source/banned_library.dart'; - -void testingBannedCodeLint() { - // expect_lint: avoid_using_api - final unmodifiable = UnmodifiableListView([1, 2, 3]); - - // expect_lint: avoid_using_api - unmodifiable.first; - - // expect_lint: avoid_using_api - banned; -} diff --git a/lint_test/avoid_using_api/source_includes_ban/pubspec.yaml b/lint_test/avoid_using_api/source_includes_ban/pubspec.yaml deleted file mode 100644 index a531643c..00000000 --- a/lint_test/avoid_using_api/source_includes_ban/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: source_includes_ban -description: A starting point for Dart libraries or applications. -version: 1.0.0 -publish_to: none - -environment: - sdk: ^3.1.3 - -dependencies: - external_source: - path: ../external_source - -dev_dependencies: - lints: ^3.0.0 - test: ^1.21.0 - solid_lints: - path: ../../../ diff --git a/test/lints/auto_lint_data.dart b/test/lints/auto_lint_data.dart new file mode 100644 index 00000000..61b900dc --- /dev/null +++ b/test/lints/auto_lint_data.dart @@ -0,0 +1,19 @@ +import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart'; + +/// Holds data about a lint placeholder, including the original code +/// and optional diagnostic assertions. +class AutoLintData { + final String code; + final Pattern? correctionContains; + final List messageContainsAll; + final String? name; + final List? contextMessages; + + const AutoLintData({ + required this.code, + this.correctionContains, + this.messageContainsAll = const [], + this.name, + this.contextMessages, + }); +} diff --git a/test/src/lints/avoid_using_api/avoid_using_api_rule_test.dart b/test/src/lints/avoid_using_api/avoid_using_api_rule_test.dart new file mode 100644 index 00000000..09b070b3 --- /dev/null +++ b/test/src/lints/avoid_using_api/avoid_using_api_rule_test.dart @@ -0,0 +1,399 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/avoid_using_api/avoid_using_api_rule.dart'; +import 'package:test/test.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidUsingApiRuleTest); + }); +} + +@reflectiveTest +class AvoidUsingApiRuleTest extends AnalysisRuleTest with AutoTestLintOffsets { + static const _mockMyDepContent = ''' +class BadClass { + const BadClass({int? badParam}); + const BadClass.named({int? badParam}); + void badMethod({int? badParam}) {} +} +class Holder { + const Holder(); + BadClass get badClass => const BadClass(); +} +int get badGlobal => 42; +void badFunction() {} +'''; + + @override + void setUp() { + rule = AvoidUsingApiRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + newPackage('my_dep')..addFile('lib/my_dep.dart', _mockMyDepContent); + newPackage('other_dep')..addFile('lib/other_dep.dart', 'class BadClass {}'); + super.setUp(); + } + + void _configureRule(String entriesYaml) { + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + avoid_using_api: + entries: +$entriesYaml'''); + } + + Future test_reports_ban_source() { + _configureRule(''' + - source: package:my_dep/my_dep.dart +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + final a = ${expectLint('BadClass')}(); + final b = ${expectLint('badGlobal')}; + ${expectLint('badFunction')}(); +} +'''); + } + + Future test_reports_ban_class_from_source() { + _configureRule(''' + - class_name: BadClass + source: package:my_dep/my_dep.dart +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + ${expectLint('BadClass')} ${expectLint('a')} = ${expectLint('BadClass')}(); + final b = badGlobal; +} +'''); + } + + Future test_reports_ban_id_from_source() { + _configureRule(''' + - identifier: badGlobal + source: package:my_dep/my_dep.dart +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + final a = BadClass(); + final b = ${expectLint('badGlobal')}; + badFunction(); +} +'''); + } + + Future test_reports_ban_id_from_class_from_source() { + _configureRule(''' + - class_name: BadClass + identifier: badMethod + source: package:my_dep/my_dep.dart +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + final a = BadClass(); + a.${expectLint('badMethod')}(); +} +'''); + } + + Future test_reports_ban_default_constructor() { + _configureRule(''' + - class_name: BadClass + identifier: "()" + source: package:my_dep/my_dep.dart +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + final a = ${expectLint('BadClass')}(); + final b = BadClass.named(); +} +'''); + } + + Future test_reports_ban_usage_with_specific_named_parameter() { + _configureRule(''' + - class_name: BadClass + identifier: badMethod + named_parameter: badParam + source: package:my_dep/my_dep.dart +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + final a = BadClass(); + a.${expectLint('badMethod')}(badParam: 123); + a.badMethod(); // OK +} +'''); + } + + Future test_respects_exclude_patterns() { + _configureRule(''' + - class_name: BadClass + source: package:my_dep/my_dep.dart + excludes: + - "lib/src/**" +'''); + + // Create a file in a folder matching the exclude pattern + final excludedFilePath = '$testPackageRootPath/lib/src/excluded_test.dart'; + newFile(excludedFilePath, ''' +import 'package:my_dep/my_dep.dart'; +void fn() { + BadClass a = BadClass(); +} +'''); + + // Verify no diagnostics in the excluded file + return assertNoDiagnosticsInFile(excludedFilePath); + } + + Future test_reports_ban_id_from_class_from_source_chained() { + _configureRule(''' + - class_name: BadClass + identifier: badMethod + source: package:my_dep/my_dep.dart +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + final h = const Holder(); + h.badClass.${expectLint('badMethod')}(); +} +'''); + } + + Future test_reports_ban_class_from_source_chained() { + _configureRule(''' + - class_name: BadClass + source: package:my_dep/my_dep.dart +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + final h = const Holder(); + h.badClass.${expectLint('badMethod')}(); +} +'''); + } + + Future test_reports_ban_source_with_custom_reason() { + _configureRule(''' + - source: package:my_dep/my_dep.dart + reason: "Custom warning message" +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + final a = ${expectLint('BadClass', messageContainsAll: ["Custom warning message"])}(); +} +'''); + } + + Future test_does_not_report_same_names_from_other_source() { + _configureRule(''' + - class_name: BadClass + source: package:my_dep/my_dep.dart +'''); + + return assertNoDiagnostics(''' +import 'package:other_dep/other_dep.dart'; + +void fn() { + final a = BadClass(); // OK +} +'''); + } + + Future test_reports_ban_extension_method() { + newPackage('my_dep')..addFile('lib/my_dep.dart', ''' +extension BadExtension on String { + void badExtMethod() {} +} +'''); + + _configureRule(''' + - class_name: BadExtension + identifier: badExtMethod + source: package:my_dep/my_dep.dart +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + "".${expectLint('badExtMethod')}(); +} +'''); + } + + Future test_reports_ban_static_method() { + newPackage('my_dep')..addFile('lib/my_dep.dart', ''' +class BadClass { + static void staticBadMethod() {} +} +'''); + + _configureRule(''' + - class_name: BadClass + identifier: staticBadMethod + source: package:my_dep/my_dep.dart +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + BadClass.${expectLint('staticBadMethod')}(); +} +'''); + } + + Future test_respects_include_patterns() { + _configureRule(''' + - class_name: BadClass + source: package:my_dep/my_dep.dart + includes: + - "**/test.dart" +'''); + + // Create a file not matching the include pattern + final otherFilePath = '$testPackageRootPath/lib/other.dart'; + newFile(otherFilePath, ''' +import 'package:my_dep/my_dep.dart'; +void fn() { + final a = BadClass(); +} +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; +void fn() { + final ${expectLint('a')} = ${expectLint('BadClass')}(); +} +''').then((_) => assertNoDiagnosticsInFile(otherFilePath)); + } + + Future + test_reports_ban_usage_with_specific_named_parameter_in_constructors() { + _configureRule(''' + - class_name: BadClass + identifier: "()" + named_parameter: badParam + source: package:my_dep/my_dep.dart + - class_name: BadClass + identifier: named + named_parameter: badParam + source: package:my_dep/my_dep.dart +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + final a = ${expectLint('BadClass')}(badParam: 123); + final b = BadClass(); // OK + final c = ${expectLint('BadClass')}.named(badParam: 123); + final d = BadClass.named(); // OK +} +'''); + } + + Future test_reports_ban_extension_getter() { + newPackage('my_dep')..addFile('lib/my_dep.dart', ''' +extension BadExtension on String { + String get badExtGetter => ''; +} +'''); + + _configureRule(''' + - class_name: BadExtension + identifier: badExtGetter + source: package:my_dep/my_dep.dart +'''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + final val = "".${expectLint('badExtGetter')}; +} +'''); + } + + Future test_reports_ban_with_custom_severities() async { + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + avoid_using_api: + avoid_using_api: true + severity: warning + entries: + - class_name: BadClass + source: package:my_dep/my_dep.dart + severity: error + - identifier: badGlobal + source: package:my_dep/my_dep.dart + severity: info + - identifier: badFunction + source: package:my_dep/my_dep.dart +'''); + + await assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + final ${expectLint('a')} = ${expectLint('BadClass')}(); + final b = ${expectLint('badGlobal')}; + ${expectLint('badFunction')}(); +} +'''); + + final diagnostics = result.diagnostics + .where((d) => d.diagnosticCode.lowerCaseName == 'avoid_using_api') + .toList(); + + diagnostics.sort((first, second) => first.offset.compareTo(second.offset)); + + expect(diagnostics, hasLength(4)); + expect(diagnostics[0].severity.name, 'error'); + expect(diagnostics[1].severity.name, 'error'); + expect(diagnostics[2].severity.name, 'info'); + expect(diagnostics[3].severity.name, 'warning'); + } +} From 9567254a85da20f12adba2e6897d0a4659e82477 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 10 Jul 2026 10:55:53 +0300 Subject: [PATCH 41/49] refactor: consolidate lint fix registration logic (#316) * refactor: consolidate lint fix registration logic using a new RuleWithFixes interface * refactor: simplify lint rule fix registration by removing redundant type cast variable * refactor: introduce FixesForCodes typedef and replace Iterable return types in RuleWithFixes implementation --------- Co-authored-by: Illia Aihistov --- lib/main.dart | 103 +++++------------- .../avoid_final_with_getter_rule.dart | 11 +- ...void_unnecessary_type_assertions_rule.dart | 19 +++- .../double_literal_format_rule.dart | 23 ++-- .../named_parameters_ordering_rule.dart | 16 ++- .../prefer_conditional_expressions_rule.dart | 16 ++- .../lints/prefer_first/prefer_first_rule.dart | 13 ++- .../lints/prefer_last/prefer_last_rule.dart | 7 +- .../use_nearest_context_rule.dart | 19 +++- lib/src/models/rule_with_fixes.dart | 19 ++++ 10 files changed, 138 insertions(+), 108 deletions(-) create mode 100644 lib/src/models/rule_with_fixes.dart diff --git a/lib/main.dart b/lib/main.dart index d997dbe4..a6f55fad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,40 +3,34 @@ import 'package:analysis_server_plugin/registry.dart'; import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; import 'package:solid_lints/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart'; -import 'package:solid_lints/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; +import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; -import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart'; import 'package:solid_lints/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart'; import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart'; import 'package:solid_lints/src/lints/avoid_using_api/avoid_using_api_rule.dart'; import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; -import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart'; import 'package:solid_lints/src/lints/member_ordering/member_ordering_rule.dart'; -import 'package:solid_lints/src/lints/named_parameters_ordering/fixes/named_parameters_ordering_fix.dart'; import 'package:solid_lints/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart'; import 'package:solid_lints/src/lints/newline_before_return/newline_before_return_rule.dart'; import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; import 'package:solid_lints/src/lints/no_equal_then_else/no_equal_then_else_rule.dart'; import 'package:solid_lints/src/lints/no_magic_number/no_magic_number_rule.dart'; import 'package:solid_lints/src/lints/number_of_parameters/number_of_parameters_rule.dart'; -import 'package:solid_lints/src/lints/prefer_conditional_expressions/fixes/prefer_conditional_expressions_fix.dart'; import 'package:solid_lints/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart'; import 'package:solid_lints/src/lints/prefer_early_return/prefer_early_return_rule.dart'; -import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; -import 'package:solid_lints/src/lints/prefer_last/fixes/prefer_last_fix.dart'; import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; import 'package:solid_lints/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; -import 'package:solid_lints/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart'; import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart'; +import 'package:solid_lints/src/models/rule_with_fixes.dart'; /// The entry point for the Solid Lints analyser server plugin. /// @@ -56,90 +50,45 @@ class SolidLintsPlugin extends Plugin { void register(PluginRegistry registry) { final analysisLoader = AnalysisOptionsLoader(); - final avoidUnnecessaryTypeAssertionsRule = - AvoidUnnecessaryTypeAssertionsRule(); - final doubleLiteralFormatRule = DoubleLiteralFormatRule(); - final preferFirstRule = PreferFirstRule(); - final preferConditionalExpressionsRule = PreferConditionalExpressionsRule( - analysisOptionsLoader: analysisLoader, - ); - final preferLastRule = PreferLastRule(); - final lintRules = [ + AvoidDebugPrintInReleaseRule(), AvoidFinalWithGetterRule(), AvoidGlobalStateRule(), + AvoidLateKeywordRule(analysisOptionsLoader: analysisLoader), AvoidNonNullAssertionRule(analysisOptionsLoader: analysisLoader), - avoidUnnecessaryTypeAssertionsRule, - AvoidDebugPrintInReleaseRule(), - doubleLiteralFormatRule, - ProperSuperCallsRule(), - NamedParametersOrderingRule(analysisOptionsLoader: analysisLoader), AvoidReturningWidgetsRule(analysisOptionsLoader: analysisLoader), - MemberOrderingRule(analysisOptionsLoader: analysisLoader), - AvoidUnusedParametersRule(analysisOptionsLoader: analysisLoader), - NumberOfParametersRule(analysisOptionsLoader: analysisLoader), - FunctionLinesOfCodeRule(analysisOptionsLoader: analysisLoader), - CyclomaticComplexityRule(analysisOptionsLoader: analysisLoader), - NoEmptyBlockRule(analysisOptionsLoader: analysisLoader), - UseNearestContextRule(), - NoMagicNumberRule(analysisOptionsLoader: analysisLoader), - AvoidUsingApiRule(analysisOptionsLoader: analysisLoader), - preferFirstRule, - preferConditionalExpressionsRule, - preferLastRule, - PreferMatchFileNameRule(analysisOptionsLoader: analysisLoader), - // TODO: Add more lint rules and use analysisLoader - // for rules that need parameters - // For example: `CyclomaticComplexityRule(analysisLoader)` AvoidUnnecessaryReturnVariableRule(), AvoidUnnecessarySetStateRule(), + AvoidUnnecessaryTypeAssertionsRule(), AvoidUnrelatedTypeAssertionsRule(), + AvoidUnusedParametersRule(analysisOptionsLoader: analysisLoader), + AvoidUsingApiRule(analysisOptionsLoader: analysisLoader), + CyclomaticComplexityRule(analysisOptionsLoader: analysisLoader), + DoubleLiteralFormatRule(), + FunctionLinesOfCodeRule(analysisOptionsLoader: analysisLoader), + MemberOrderingRule(analysisOptionsLoader: analysisLoader), + NamedParametersOrderingRule(analysisOptionsLoader: analysisLoader), NewlineBeforeReturnRule(), + NoEmptyBlockRule(analysisOptionsLoader: analysisLoader), NoEqualThenElseRule(), + NoMagicNumberRule(analysisOptionsLoader: analysisLoader), + NumberOfParametersRule(analysisOptionsLoader: analysisLoader), + PreferConditionalExpressionsRule(analysisOptionsLoader: analysisLoader), PreferEarlyReturnRule(), + PreferFirstRule(), + PreferLastRule(), + PreferMatchFileNameRule(analysisOptionsLoader: analysisLoader), + ProperSuperCallsRule(), + UseNearestContextRule(), ]; for (final lintRule in lintRules) { registry.registerLintRule(lintRule); + if (lintRule is RuleWithFixes) { + for (final entry in (lintRule as RuleWithFixes).fixesForCodes) { + registry.registerFixForRule(entry.key, entry.value); + } + } } - - for (final code in doubleLiteralFormatRule.diagnosticCodes) { - registry.registerFixForRule(code, DoubleLiteralFormatFix.new); - } - - registry.registerFixForRule( - AvoidFinalWithGetterRule.code, - AvoidFinalWithGetterFix.new, - ); - - registry.registerFixForRule( - avoidUnnecessaryTypeAssertionsRule.diagnosticCode, - AvoidUnnecessaryTypeAssertionsFix.new, - ); - - registry.registerFixForRule( - UseNearestContextRule.code, - RenameNearestContextParameterFix.new, - ); - - registry.registerFixForRule( - preferFirstRule.diagnosticCode, - PreferFirstFix.new, - ); - - registry.registerFixForRule( - preferLastRule.diagnosticCode, - PreferLastFix.new, - ); - - registry.registerFixForRule( - preferConditionalExpressionsRule.diagnosticCode, - PreferConditionalExpressionsFix.new, - ); - - registry.registerFixForRule( - NamedParametersOrderingRule.code, - NamedParametersOrderingFix.new, - ); } } diff --git a/lib/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart b/lib/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart index 727e3dfe..f06218f9 100644 --- a/lib/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart +++ b/lib/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart @@ -2,7 +2,9 @@ import 'package:analyzer/analysis_rule/analysis_rule.dart'; import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.dart'; import 'package:solid_lints/src/lints/avoid_final_with_getter/visitors/avoid_final_with_getter_visitor.dart'; +import 'package:solid_lints/src/models/rule_with_fixes.dart'; /// Avoid using final private fields with getters. /// @@ -30,7 +32,7 @@ import 'package:solid_lints/src/lints/avoid_final_with_getter/visitors/avoid_fin /// } /// ``` /// -class AvoidFinalWithGetterRule extends AnalysisRule { +class AvoidFinalWithGetterRule extends AnalysisRule implements RuleWithFixes { /// This lint rule represents /// the error whether we use final private fields with getters. static const lintName = 'avoid_final_with_getter'; @@ -44,11 +46,16 @@ class AvoidFinalWithGetterRule extends AnalysisRule { /// Creates a new instance of [AvoidFinalWithGetterRule] AvoidFinalWithGetterRule() - : super(name: lintName, description: code.problemMessage); + : super(name: lintName, description: code.problemMessage); @override LintCode get diagnosticCode => code; + @override + FixesForCodes get fixesForCodes => const [ + MapEntry(code, AvoidFinalWithGetterFix.new), + ]; + @override void registerNodeProcessors( RuleVisitorRegistry registry, diff --git a/lib/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart b/lib/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart index 906c934b..7148ee55 100644 --- a/lib/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart +++ b/lib/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart @@ -2,15 +2,18 @@ import 'package:analyzer/analysis_rule/analysis_rule.dart'; import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/fixes/avoid_unnecessary_type_assertions_fix.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_is_expression_visitor.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_where_type_visitor.dart'; +import 'package:solid_lints/src/models/rule_with_fixes.dart'; /// Warns about unnecessary usage of `is` and `whereType` operators. /// /// ### Example: /// {@macro solid_lints.avoid_unnecessary_type_assertions.example_is} /// {@macro solid_lints.avoid_unnecessary_type_assertions.example_where} -class AvoidUnnecessaryTypeAssertionsRule extends AnalysisRule { +class AvoidUnnecessaryTypeAssertionsRule extends AnalysisRule + implements RuleWithFixes { /// The name of 'is' operator static const operatorIsName = 'is'; @@ -26,9 +29,6 @@ class AvoidUnnecessaryTypeAssertionsRule extends AnalysisRule { "Unnecessary usage of the {0}.", ); - @override - DiagnosticCode get diagnosticCode => _unnecessaryTypeAssertionsCode; - /// Creates a new instance of [AvoidUnnecessaryTypeAssertionsRule] AvoidUnnecessaryTypeAssertionsRule() : super( @@ -36,6 +36,17 @@ class AvoidUnnecessaryTypeAssertionsRule extends AnalysisRule { description: "Unnecessary usage of typecast operators.", ); + @override + DiagnosticCode get diagnosticCode => _unnecessaryTypeAssertionsCode; + + @override + FixesForCodes get fixesForCodes => const [ + MapEntry( + _unnecessaryTypeAssertionsCode, + AvoidUnnecessaryTypeAssertionsFix.new, + ), + ]; + @override void registerNodeProcessors( RuleVisitorRegistry registry, diff --git a/lib/src/lints/double_literal_format/double_literal_format_rule.dart b/lib/src/lints/double_literal_format/double_literal_format_rule.dart index d90d3726..5fb7806e 100644 --- a/lib/src/lints/double_literal_format/double_literal_format_rule.dart +++ b/lib/src/lints/double_literal_format/double_literal_format_rule.dart @@ -2,7 +2,9 @@ import 'package:analyzer/analysis_rule/analysis_rule.dart'; import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/double_literal_format/visitors/double_literal_format_visitor.dart'; +import 'package:solid_lints/src/models/rule_with_fixes.dart'; /// A `double_literal_format` rule which /// checks that double literals should begin with 0. instead of just ., @@ -23,7 +25,8 @@ import 'package:solid_lints/src/lints/double_literal_format/visitors/double_lite /// var a = 5.23, b = 0.16e+5, c = -0.25, d = -0.4e-5; /// ``` /// {@endtemplate} -class DoubleLiteralFormatRule extends MultiAnalysisRule { +class DoubleLiteralFormatRule extends MultiAnalysisRule + implements RuleWithFixes { /// This lint rule represents /// the error whether we use bad formatted double literals. static const lintName = 'double_literal_format'; @@ -96,13 +99,6 @@ class DoubleLiteralFormatRule extends MultiAnalysisRule { uniqueName: 'trailingZero', ); - @override - List get diagnosticCodes => [ - leadingZeroCode, - leadingDecimalCode, - trailingZeroCode, - ]; - /// Creates a new instance of [DoubleLiteralFormatRule] DoubleLiteralFormatRule() : super( @@ -113,6 +109,17 @@ class DoubleLiteralFormatRule extends MultiAnalysisRule { 'should not start with a leading 0', ); + @override + List get diagnosticCodes => [ + leadingZeroCode, + leadingDecimalCode, + trailingZeroCode, + ]; + + @override + FixesForCodes get fixesForCodes => + diagnosticCodes.map((code) => MapEntry(code, DoubleLiteralFormatFix.new)); + @override void registerNodeProcessors( RuleVisitorRegistry registry, diff --git a/lib/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart b/lib/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart index 110ee5de..fd1a1b65 100644 --- a/lib/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart +++ b/lib/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart @@ -1,9 +1,11 @@ import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/named_parameters_ordering/fixes/named_parameters_ordering_fix.dart'; import 'package:solid_lints/src/lints/named_parameters_ordering/models/named_parameters_ordering_parameters.dart'; import 'package:solid_lints/src/lints/named_parameters_ordering/models/parameter_type.dart'; import 'package:solid_lints/src/lints/named_parameters_ordering/visitors/named_parameters_ordering_visitor.dart'; +import 'package:solid_lints/src/models/rule_with_fixes.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// A lint which allows to enforce a particular named parameter ordering @@ -102,7 +104,8 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// } /// ``` class NamedParametersOrderingRule - extends SolidLintRule { + extends SolidLintRule + implements RuleWithFixes { /// The name of this lint rule. static const lintName = 'named_parameters_ordering'; @@ -112,9 +115,6 @@ class NamedParametersOrderingRule '{0} named parameters should be before {1} named parameters.', ); - @override - LintCode get diagnosticCode => code; - /// Creates a new instance of [NamedParametersOrderingRule]. NamedParametersOrderingRule({ required super.analysisOptionsLoader, @@ -126,6 +126,14 @@ class NamedParametersOrderingRule parametersParser: NamedParametersOrderingParameters.fromJson, ); + @override + LintCode get diagnosticCode => code; + + @override + FixesForCodes get fixesForCodes => const [ + MapEntry(code, NamedParametersOrderingFix.new), + ]; + @override void registerNodeProcessors( RuleVisitorRegistry registry, diff --git a/lib/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart b/lib/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart index 5f501972..4e7731f5 100644 --- a/lib/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart +++ b/lib/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart @@ -1,8 +1,10 @@ import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/prefer_conditional_expressions/fixes/prefer_conditional_expressions_fix.dart'; import 'package:solid_lints/src/lints/prefer_conditional_expressions/models/prefer_conditional_expressions_parameters.dart'; import 'package:solid_lints/src/lints/prefer_conditional_expressions/visitors/prefer_conditional_expressions_visitor.dart'; +import 'package:solid_lints/src/models/rule_with_fixes.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; // Inspired by TSLint (https://palantir.github.io/tslint/rules/prefer-conditional-expression/) @@ -56,7 +58,8 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// } /// ``` class PreferConditionalExpressionsRule - extends SolidLintRule { + extends SolidLintRule + implements RuleWithFixes { /// This lint rule represents the error when an if-else statement /// can be simplified to a conditional expression. static const lintName = 'prefer_conditional_expressions'; @@ -66,9 +69,6 @@ class PreferConditionalExpressionsRule 'Prefer conditional expression.', ); - @override - LintCode get diagnosticCode => _code; - /// Creates a new instance of [PreferConditionalExpressionsRule] PreferConditionalExpressionsRule({ required super.analysisOptionsLoader, @@ -78,6 +78,14 @@ class PreferConditionalExpressionsRule parametersParser: PreferConditionalExpressionsParameters.fromJson, ); + @override + LintCode get diagnosticCode => _code; + + @override + FixesForCodes get fixesForCodes => const [ + MapEntry(_code, PreferConditionalExpressionsFix.new), + ]; + @override void registerNodeProcessors( RuleVisitorRegistry registry, diff --git a/lib/src/lints/prefer_first/prefer_first_rule.dart b/lib/src/lints/prefer_first/prefer_first_rule.dart index b644cffe..ee40ea13 100644 --- a/lib/src/lints/prefer_first/prefer_first_rule.dart +++ b/lib/src/lints/prefer_first/prefer_first_rule.dart @@ -1,7 +1,9 @@ import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/visitors/prefer_first_visitor.dart'; +import 'package:solid_lints/src/models/rule_with_fixes.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Warns about usage of iterable[0] or iterable.elementAt(0) instead of @@ -25,7 +27,7 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// /// a.first; // OK /// ``` -class PreferFirstRule extends SolidLintRule { +class PreferFirstRule extends SolidLintRule implements RuleWithFixes { /// This lint rule represents the error if number of /// parameters reaches the maximum value. static const lintName = 'prefer_first'; @@ -35,11 +37,16 @@ class PreferFirstRule extends SolidLintRule { "Use first instead of accessing the element at zero index.", ); + /// Creates a new instance of [PreferFirstRule] + PreferFirstRule() : super(name: lintName, description: _code.problemMessage); + @override LintCode get diagnosticCode => _code; - /// Creates a new instance of [PreferFirstRule] - PreferFirstRule() : super(name: lintName, description: _code.problemMessage); + @override + FixesForCodes get fixesForCodes => const [ + MapEntry(_code, PreferFirstFix.new), + ]; @override void registerNodeProcessors( diff --git a/lib/src/lints/prefer_last/prefer_last_rule.dart b/lib/src/lints/prefer_last/prefer_last_rule.dart index ff75f5ba..07bf81b8 100644 --- a/lib/src/lints/prefer_last/prefer_last_rule.dart +++ b/lib/src/lints/prefer_last/prefer_last_rule.dart @@ -1,7 +1,9 @@ import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/prefer_last/fixes/prefer_last_fix.dart'; import 'package:solid_lints/src/lints/prefer_last/visitors/prefer_last_visitor.dart'; +import 'package:solid_lints/src/models/rule_with_fixes.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Warns about usage of `iterable[length - 1]` or @@ -25,7 +27,7 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// /// a.last; // OK /// ``` -class PreferLastRule extends SolidLintRule { +class PreferLastRule extends SolidLintRule implements RuleWithFixes { /// This lint rule represents the error if iterable /// access can be simplified. static const lintName = 'prefer_last'; @@ -41,6 +43,9 @@ class PreferLastRule extends SolidLintRule { @override LintCode get diagnosticCode => _code; + @override + FixesForCodes get fixesForCodes => const [MapEntry(_code, PreferLastFix.new)]; + @override void registerNodeProcessors( RuleVisitorRegistry registry, diff --git a/lib/src/lints/use_nearest_context/use_nearest_context_rule.dart b/lib/src/lints/use_nearest_context/use_nearest_context_rule.dart index 56af784a..3fab889f 100644 --- a/lib/src/lints/use_nearest_context/use_nearest_context_rule.dart +++ b/lib/src/lints/use_nearest_context/use_nearest_context_rule.dart @@ -2,7 +2,10 @@ import 'package:analyzer/analysis_rule/analysis_rule.dart'; import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart'; +import 'package:solid_lints/src/lints/use_nearest_context/fixes/replace_with_nearest_context_parameter_fix.dart'; import 'package:solid_lints/src/lints/use_nearest_context/visitors/use_nearest_context_visitor.dart'; +import 'package:solid_lints/src/models/rule_with_fixes.dart'; /// A rule which checks that we use BuildContext from the nearest available /// scope. @@ -49,7 +52,7 @@ import 'package:solid_lints/src/lints/use_nearest_context/visitors/use_nearest_c /// } /// ``` /// -class UseNearestContextRule extends AnalysisRule { +class UseNearestContextRule extends AnalysisRule implements RuleWithFixes { /// This lint rule represents the error if BuildContext is used not from the /// nearest available scope static const lintName = 'use_nearest_context'; @@ -62,14 +65,20 @@ class UseNearestContextRule extends AnalysisRule { /// Creates a new instance of [UseNearestContextRule]. UseNearestContextRule() - : super( - name: lintName, - description: code.problemMessage, - ); + : super( + name: lintName, + description: code.problemMessage, + ); @override LintCode get diagnosticCode => code; + @override + FixesForCodes get fixesForCodes => const [ + MapEntry(code, RenameNearestContextParameterFix.new), + MapEntry(code, ReplaceWithNearestContextParameterFix.new), + ]; + @override void registerNodeProcessors( RuleVisitorRegistry registry, diff --git a/lib/src/models/rule_with_fixes.dart b/lib/src/models/rule_with_fixes.dart new file mode 100644 index 00000000..2be5ca98 --- /dev/null +++ b/lib/src/models/rule_with_fixes.dart @@ -0,0 +1,19 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/error/error.dart'; + +/// A function that creates a [CorrectionProducer] for a given context. +typedef ProducerGenerator = + CorrectionProducer Function({ + required CorrectionProducerContext context, + }); + +/// A collection of diagnostic codes and their associated fix generators. +typedef FixesForCodes = Iterable>; + +/// An interface for lint rules that have associated quick fixes. +abstract interface class RuleWithFixes { + /// Returns the map entries of diagnostic code and its associated fix + /// generator. + FixesForCodes get fixesForCodes; +} From c22a8aa30c2b459a5b7c0c2e3ace703d9dae8879 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 17 Jul 2026 16:32:36 +0300 Subject: [PATCH 42/49] refactor: update automatic documentation generation (#315) * feat: handle docs parser structured parameters * feat: implement macro and template support in documentation parser and update parameter metadata * docs: update documentation and examples for analysis_server_plugin migration * refactor: move and modularize documentation parsing utilities and improve rule parsing logic * fix: handle null field types in parameters parser and refine macro circular dependency detection logic * refactor: enhance documentation error reporting by including source context and specific field names * refactor: use toSource for type parsing and improve file name suffix extraction logic * refactor: extract parser extensions and optimize AST navigation and comment processing logic * refactor: extract file reading logic into FileUtils and modernize ParserUtils implementation --------- Co-authored-by: Illia Aihistov --- README.md | 20 +- doc/docusaurus/docs/1_rulesets/test.md | 6 + doc/docusaurus/package.json | 4 +- doc/docusaurus/yarn.lock | 4724 +++++++++++++++-- example/analysis_options.yaml | 33 +- example/pubspec.yaml | 1 - lib/analysis_options.yaml | 2 - .../excluded_annotations_list_parameter.dart | 2 + .../excluded_entities_list_parameter.dart | 2 + .../excluded_identifiers_list_parameter.dart | 2 + .../avoid_late_keyword_rule.dart | 22 +- .../models/avoid_late_keyword_parameters.dart | 17 +- .../avoid_non_null_assertion_rule.dart | 12 + .../avoid_returning_widgets_rule.dart | 12 + ...void_unnecessary_return_variable_rule.dart | 8 +- .../avoid_unnecessary_set_state_rule.dart | 8 +- .../unnecessary_is_expression_visitor.dart | 6 +- .../unnecessary_where_type_visitor.dart | 6 +- .../avoid_unrelated_type_assertions_rule.dart | 8 +- .../avoid_unused_parameters_rule.dart | 17 +- .../avoid_using_api_entry_parameters.dart | 1 + .../function_lines_of_code_rule.dart | 2 +- .../models/member_group/member_group.dart | 2 + .../models/parameter_type.dart | 2 + .../newline_before_return_rule.dart | 8 +- .../no_equal_then_else_rule.dart | 8 +- lib/src/utils/docs_parser/main.dart | 21 +- .../output_formatters/markdown_formatter.dart | 8 +- lib/src/utils/docs_parser/parser_utils.dart | 44 - .../docs_parser/parsers/base_parser.dart | 26 - .../docs_parser/parsers/docs_parser.dart | 69 +- .../parsers/parameters_parser.dart | 116 +- .../docs_parser/parsers/rule_parser.dart | 78 +- .../docs_parser/utils/parser_extensions.dart | 51 + .../docs_parser/utils/parser_regexes.dart | 113 + .../utils/docs_parser/utils/parser_utils.dart | 166 + lib/src/utils/file_utils.dart | 14 + 37 files changed, 4985 insertions(+), 656 deletions(-) delete mode 100644 lib/src/utils/docs_parser/parser_utils.dart delete mode 100644 lib/src/utils/docs_parser/parsers/base_parser.dart create mode 100644 lib/src/utils/docs_parser/utils/parser_extensions.dart create mode 100644 lib/src/utils/docs_parser/utils/parser_regexes.dart create mode 100644 lib/src/utils/docs_parser/utils/parser_utils.dart create mode 100644 lib/src/utils/file_utils.dart diff --git a/README.md b/README.md index 02d62c85..1c5cd07b 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,16 @@ dev_dependencies: solid_lints: ``` -And then include `solid_lints` into your project top-level `analysis_options.yaml`: +Enable the plugin and include `solid_lints` in your project's top-level `analysis_options.yaml`: ```yaml include: package:solid_lints/analysis_options.yaml + +plugins: + solid_lints: ``` -Also you can use a specialized rule set designed for Dart tests. +Also, you can use a specialized rule set designed for Dart tests. Add an `analysis_options.yaml` file under the `test/` directory, and include the ruleset: ```yaml @@ -36,7 +39,18 @@ Then you can see suggestions in your IDE or you can run checks manually: ```bash dart analyze; -dart run custom_lint; +``` + +# Configuration + +You can customize individual rule settings in your `analysis_options.yaml` under the `solid_lints` configuration block: + +```yaml +solid_lints: + diagnostics: + cyclomatic_complexity: + max_complexity: 10 + avoid_non_null_assertion: true ``` # Badge diff --git a/doc/docusaurus/docs/1_rulesets/test.md b/doc/docusaurus/docs/1_rulesets/test.md index eba8c562..8120df3e 100644 --- a/doc/docusaurus/docs/1_rulesets/test.md +++ b/doc/docusaurus/docs/1_rulesets/test.md @@ -65,6 +65,12 @@ Given the quite large threshold configured for this metric we considered extract Both options didn't look right, so we decided that tests are ok to be long. +## cyclomatic_complexity + +State: **Disabled**. + +Since we're not using the `function_lines_of_code` rule, the `main()` function in tests can have high cyclomatic complexity. For the rationale against splitting up `main()` in tests, see the comments for `function_lines_of_code` above. + ## prefer_match_file_name State: **Disabled**. diff --git a/doc/docusaurus/package.json b/doc/docusaurus/package.json index d584d319..81f2d586 100644 --- a/doc/docusaurus/package.json +++ b/doc/docusaurus/package.json @@ -16,8 +16,8 @@ "typecheck": "tsc" }, "dependencies": { - "@docusaurus/core": "^3.1.0", - "@docusaurus/preset-classic": "^3.1.0", + "@docusaurus/core": "^3.10.1", + "@docusaurus/preset-classic": "^3.10.1", "@easyops-cn/docusaurus-search-local": "^0.40.1", "@mdx-js/react": "^3.0.0", "clsx": "^2.1.0", diff --git a/doc/docusaurus/yarn.lock b/doc/docusaurus/yarn.lock index b531fe25..ccaa145b 100644 --- a/doc/docusaurus/yarn.lock +++ b/doc/docusaurus/yarn.lock @@ -2,141 +2,176 @@ # yarn lockfile v1 -"@algolia/autocomplete-core@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz#1d56482a768c33aae0868c8533049e02e8961be7" - integrity sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw== - dependencies: - "@algolia/autocomplete-plugin-algolia-insights" "1.9.3" - "@algolia/autocomplete-shared" "1.9.3" - -"@algolia/autocomplete-plugin-algolia-insights@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz#9b7f8641052c8ead6d66c1623d444cbe19dde587" - integrity sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg== - dependencies: - "@algolia/autocomplete-shared" "1.9.3" - -"@algolia/autocomplete-preset-algolia@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz#64cca4a4304cfcad2cf730e83067e0c1b2f485da" - integrity sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA== - dependencies: - "@algolia/autocomplete-shared" "1.9.3" - -"@algolia/autocomplete-shared@1.9.3": - version "1.9.3" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz#2e22e830d36f0a9cf2c0ccd3c7f6d59435b77dfa" - integrity sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ== - -"@algolia/cache-browser-local-storage@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.22.1.tgz#14b6dc9abc9e3a304a5fffb063d15f30af1032d1" - integrity sha512-Sw6IAmOCvvP6QNgY9j+Hv09mvkvEIDKjYW8ow0UDDAxSXy664RBNQk3i/0nt7gvceOJ6jGmOTimaZoY1THmU7g== - dependencies: - "@algolia/cache-common" "4.22.1" - -"@algolia/cache-common@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.22.1.tgz#c625dff4bc2a74e79f9aed67b4e053b0ef1b3ec1" - integrity sha512-TJMBKqZNKYB9TptRRjSUtevJeQVXRmg6rk9qgFKWvOy8jhCPdyNZV1nB3SKGufzvTVbomAukFR8guu/8NRKBTA== - -"@algolia/cache-in-memory@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.22.1.tgz#858a3d887f521362e87d04f3943e2810226a0d71" - integrity sha512-ve+6Ac2LhwpufuWavM/aHjLoNz/Z/sYSgNIXsinGofWOysPilQZPUetqLj8vbvi+DHZZaYSEP9H5SRVXnpsNNw== - dependencies: - "@algolia/cache-common" "4.22.1" - -"@algolia/client-account@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.22.1.tgz#a7fb8b66b9a4f0a428e1426b2561144267d76d43" - integrity sha512-k8m+oegM2zlns/TwZyi4YgCtyToackkOpE+xCaKCYfBfDtdGOaVZCM5YvGPtK+HGaJMIN/DoTL8asbM3NzHonw== - dependencies: - "@algolia/client-common" "4.22.1" - "@algolia/client-search" "4.22.1" - "@algolia/transporter" "4.22.1" - -"@algolia/client-analytics@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.22.1.tgz#506558740b4d49b1b1e3393861f729a8ce921851" - integrity sha512-1ssi9pyxyQNN4a7Ji9R50nSdISIumMFDwKNuwZipB6TkauJ8J7ha/uO60sPJFqQyqvvI+px7RSNRQT3Zrvzieg== - dependencies: - "@algolia/client-common" "4.22.1" - "@algolia/client-search" "4.22.1" - "@algolia/requester-common" "4.22.1" - "@algolia/transporter" "4.22.1" - -"@algolia/client-common@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.22.1.tgz#042b19c1b6157c485fa1b551349ab313944d2b05" - integrity sha512-IvaL5v9mZtm4k4QHbBGDmU3wa/mKokmqNBqPj0K7lcR8ZDKzUorhcGp/u8PkPC/e0zoHSTvRh7TRkGX3Lm7iOQ== - dependencies: - "@algolia/requester-common" "4.22.1" - "@algolia/transporter" "4.22.1" - -"@algolia/client-personalization@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.22.1.tgz#ff088d797648224fb582e9fe5828f8087835fa3d" - integrity sha512-sl+/klQJ93+4yaqZ7ezOttMQ/nczly/3GmgZXJ1xmoewP5jmdP/X/nV5U7EHHH3hCUEHeN7X1nsIhGPVt9E1cQ== - dependencies: - "@algolia/client-common" "4.22.1" - "@algolia/requester-common" "4.22.1" - "@algolia/transporter" "4.22.1" - -"@algolia/client-search@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.22.1.tgz#508cc6ab3d1f4e9c02735a630d4dff6fbb8514a2" - integrity sha512-yb05NA4tNaOgx3+rOxAmFztgMTtGBi97X7PC3jyNeGiwkAjOZc2QrdZBYyIdcDLoI09N0gjtpClcackoTN0gPA== - dependencies: - "@algolia/client-common" "4.22.1" - "@algolia/requester-common" "4.22.1" - "@algolia/transporter" "4.22.1" +"@algolia/abtesting@1.21.1": + version "1.21.1" + resolved "https://registry.yarnpkg.com/@algolia/abtesting/-/abtesting-1.21.1.tgz#bf5c7881302a5843b78c6f44431d27ba5eb8f179" + integrity sha512-Wia5/mNTfiU0PIUN25UMfAGGdASkkwuCS9nBAdmhqrNPY/ff7U/6MgBVdwFDPsa3sA1msutPtO50gvOzx6MOXA== + dependencies: + "@algolia/client-common" "5.55.1" + "@algolia/requester-browser-xhr" "5.55.1" + "@algolia/requester-fetch" "5.55.1" + "@algolia/requester-node-http" "5.55.1" + +"@algolia/autocomplete-core@1.19.2": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.19.2.tgz#702df67a08cb3cfe8c33ee1111ef136ec1a9e232" + integrity sha512-mKv7RyuAzXvwmq+0XRK8HqZXt9iZ5Kkm2huLjgn5JoCPtDy+oh9yxUMfDDaVCw0oyzZ1isdJBc7l9nuCyyR7Nw== + dependencies: + "@algolia/autocomplete-plugin-algolia-insights" "1.19.2" + "@algolia/autocomplete-shared" "1.19.2" + +"@algolia/autocomplete-core@^1.19.2": + version "1.19.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.19.9.tgz#bbed371e56aeea4a31a3af239f16733e1b8aedca" + integrity sha512-4U2JKLMWlDu0CotYyUkWakDxr8AIav3QtIUXXRpfavYN29aVWfzlwJp9T0rPKEf/dO2QCPAUc0Kq1Tj1GJxo2A== + dependencies: + "@algolia/autocomplete-plugin-algolia-insights" "1.19.9" + "@algolia/autocomplete-shared" "1.19.9" + +"@algolia/autocomplete-plugin-algolia-insights@1.19.2": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.2.tgz#3584b625b9317e333d1ae43664d02358e175c52d" + integrity sha512-TjxbcC/r4vwmnZaPwrHtkXNeqvlpdyR+oR9Wi2XyfORkiGkLTVhX2j+O9SaCCINbKoDfc+c2PB8NjfOnz7+oKg== + dependencies: + "@algolia/autocomplete-shared" "1.19.2" + +"@algolia/autocomplete-plugin-algolia-insights@1.19.9": + version "1.19.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.19.9.tgz#f799737d13bf0c4ec8421619c7107fa05c836535" + integrity sha512-6mExC6X7762s2SV3eJy3QOkB8bdMmnUhQ2agvGVDuzwoGyr3PquGSY/0vPQXCfiAiCaXUz1rXn+lwghgSi0l0w== + dependencies: + "@algolia/autocomplete-shared" "1.19.9" + +"@algolia/autocomplete-shared@1.19.2": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz#c0b7b8dc30a5c65b70501640e62b009535e4578f" + integrity sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w== + +"@algolia/autocomplete-shared@1.19.9": + version "1.19.9" + resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.9.tgz#c5b05e23c71027e4e45a301f286593dffdcdfbdf" + integrity sha512-YosP9Uoek6y/Ur1r1qeogk4biMe/hzkyNcgMCciw0//3XpCM7VlYLSHnyt/vOnEOGhCCc0+3v+unEiH6zz+Z1A== + +"@algolia/client-abtesting@5.55.1": + version "5.55.1" + resolved "https://registry.yarnpkg.com/@algolia/client-abtesting/-/client-abtesting-5.55.1.tgz#192d0fd57ec9b7d6924a8b19cd98e72c7e8ae6de" + integrity sha512-miW8RzAtBgNiEJ9fGEhsOPgWUpekAe64YcVufqXrlykj0Jjmo5nj0a5f/HAzRVX5ZuU1GAVd7BkzFDx7q50P3A== + dependencies: + "@algolia/client-common" "5.55.1" + "@algolia/requester-browser-xhr" "5.55.1" + "@algolia/requester-fetch" "5.55.1" + "@algolia/requester-node-http" "5.55.1" + +"@algolia/client-analytics@5.55.1": + version "5.55.1" + resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-5.55.1.tgz#2e6e14d03f1cb181c2086fd60324c24f5f3007f9" + integrity sha512-eR3J3kB9JX6DdCvDRi3I4KPfwO6fR9HWYRXhVke2TXIoOQafMKCRAneg33JRmIrb+DnnJ/eWApJLF1O1CLPERg== + dependencies: + "@algolia/client-common" "5.55.1" + "@algolia/requester-browser-xhr" "5.55.1" + "@algolia/requester-fetch" "5.55.1" + "@algolia/requester-node-http" "5.55.1" + +"@algolia/client-common@5.55.1": + version "5.55.1" + resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-5.55.1.tgz#4e023b4127805f8d2a3229f1711822927f3f4303" + integrity sha512-P5ak7EurwYqgAiDyb95mgA3WRR/Zu8CPMv36lWTISvL2AmlPyqQPy2nX/KEJRTcwaeTWwrk6wJV4/M93GfjOWw== + +"@algolia/client-insights@5.55.1": + version "5.55.1" + resolved "https://registry.yarnpkg.com/@algolia/client-insights/-/client-insights-5.55.1.tgz#f58d7908071e9f5fb328533368e3bb2dad797fe5" + integrity sha512-OVtj9uA//+pjvKQI5INnzbyLrf3ClNv3XRbWswwJ2kHIStQNHtBfHo+LofNB/WhM9xjuXlW5ANn2aMj65UGx7w== + dependencies: + "@algolia/client-common" "5.55.1" + "@algolia/requester-browser-xhr" "5.55.1" + "@algolia/requester-fetch" "5.55.1" + "@algolia/requester-node-http" "5.55.1" + +"@algolia/client-personalization@5.55.1": + version "5.55.1" + resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-5.55.1.tgz#1b4d2bd3ba031864b1c8cd3e839ecb81146f8023" + integrity sha512-oKlVFlp+qbIEe4p7E54zSiP2gEV/vDu972Ykv8VDMFwEvreS7m0YKA3a8hGGHwc7yiBUGGiR3LlwzMLfnJmy6Q== + dependencies: + "@algolia/client-common" "5.55.1" + "@algolia/requester-browser-xhr" "5.55.1" + "@algolia/requester-fetch" "5.55.1" + "@algolia/requester-node-http" "5.55.1" + +"@algolia/client-query-suggestions@5.55.1": + version "5.55.1" + resolved "https://registry.yarnpkg.com/@algolia/client-query-suggestions/-/client-query-suggestions-5.55.1.tgz#7ddb439a51f348711d2b5b7bddef9497e906be86" + integrity sha512-BOVrld6vdtsFmotVDMTVQfYXwrVplJ+DUvy60JFi+tkWV698q2J9NNPKEO3dr5qxtSLKQP4vHF8n+3U5PDWhOQ== + dependencies: + "@algolia/client-common" "5.55.1" + "@algolia/requester-browser-xhr" "5.55.1" + "@algolia/requester-fetch" "5.55.1" + "@algolia/requester-node-http" "5.55.1" + +"@algolia/client-search@5.55.1": + version "5.55.1" + resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-5.55.1.tgz#8a918fb875b9f3c8e73b7331e508196a33538e71" + integrity sha512-GAqHl9zERhC3bbBfubwUu07G3UXO06gORvOcsiTBZB3et0s3auNUbHlYdYNp4VKa3sUZqH5AcD3OKzU/KDGXjQ== + dependencies: + "@algolia/client-common" "5.55.1" + "@algolia/requester-browser-xhr" "5.55.1" + "@algolia/requester-fetch" "5.55.1" + "@algolia/requester-node-http" "5.55.1" "@algolia/events@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== -"@algolia/logger-common@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.22.1.tgz#79cf4cd295de0377a94582c6aaac59b1ded731d9" - integrity sha512-OnTFymd2odHSO39r4DSWRFETkBufnY2iGUZNrMXpIhF5cmFE8pGoINNPzwg02QLBlGSaLqdKy0bM8S0GyqPLBg== +"@algolia/ingestion@1.55.1": + version "1.55.1" + resolved "https://registry.yarnpkg.com/@algolia/ingestion/-/ingestion-1.55.1.tgz#036d40d394314d3f94dd1779d33378d8a65f48a5" + integrity sha512-BXZw+C+gsWL7pZvbnhJUnCXASiDLGcQxVV7h55Pyh2DmSzwdZIVccE5xc9RVD2trtrhIqk5smuODTxtaZqd0IA== + dependencies: + "@algolia/client-common" "5.55.1" + "@algolia/requester-browser-xhr" "5.55.1" + "@algolia/requester-fetch" "5.55.1" + "@algolia/requester-node-http" "5.55.1" -"@algolia/logger-console@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.22.1.tgz#0355345f6940f67aaa78ae9b81c06e44e49f2336" - integrity sha512-O99rcqpVPKN1RlpgD6H3khUWylU24OXlzkavUAMy6QZd1776QAcauE3oP8CmD43nbaTjBexZj2nGsBH9Tc0FVA== +"@algolia/monitoring@1.55.1": + version "1.55.1" + resolved "https://registry.yarnpkg.com/@algolia/monitoring/-/monitoring-1.55.1.tgz#307c71309cd52dbcc43d0fbd7f35a698354dd6d8" + integrity sha512-9g/ceZrZTqA62FA3588Xj0onRPjDNfu0pVQqefK0rrHp9H6Wblph/YmzGjZ2g8uqbTh0ZGIvAGCzErU8f7MHpA== dependencies: - "@algolia/logger-common" "4.22.1" + "@algolia/client-common" "5.55.1" + "@algolia/requester-browser-xhr" "5.55.1" + "@algolia/requester-fetch" "5.55.1" + "@algolia/requester-node-http" "5.55.1" -"@algolia/requester-browser-xhr@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.22.1.tgz#f04df6fe9690a071b267c77d26b83a3be9280361" - integrity sha512-dtQGYIg6MteqT1Uay3J/0NDqD+UciHy3QgRbk7bNddOJu+p3hzjTRYESqEnoX/DpEkaNYdRHUKNylsqMpgwaEw== +"@algolia/recommend@5.55.1": + version "5.55.1" + resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-5.55.1.tgz#72b5440d6221283acf5dde2828cf442769f5315a" + integrity sha512-cZTIrGyAP+W4A6jDVwvWM/JOaoJKQkD/2a5eLUEeNdKAD45jN7BCpsMDONyhZlosLa4UwL8uiINQzj4iFy9nqg== dependencies: - "@algolia/requester-common" "4.22.1" + "@algolia/client-common" "5.55.1" + "@algolia/requester-browser-xhr" "5.55.1" + "@algolia/requester-fetch" "5.55.1" + "@algolia/requester-node-http" "5.55.1" -"@algolia/requester-common@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.22.1.tgz#27be35f3718aafcb6b388ff9c3aa2defabd559ff" - integrity sha512-dgvhSAtg2MJnR+BxrIFqlLtkLlVVhas9HgYKMk2Uxiy5m6/8HZBL40JVAMb2LovoPFs9I/EWIoFVjOrFwzn5Qg== +"@algolia/requester-browser-xhr@5.55.1": + version "5.55.1" + resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.55.1.tgz#6809cfab8aab74f6cb3d69cd4c1e2d2c2be362da" + integrity sha512-N6I3leW0UO8Y9Zv90yo2UHgYGuxZO0mjbvzNxDIJDjO0qECEF7Z9XMvSNeUWXQh/iNDA9lr8MfEy3rmZGIcclw== + dependencies: + "@algolia/client-common" "5.55.1" -"@algolia/requester-node-http@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.22.1.tgz#589a6fa828ad0f325e727a6fcaf4e1a2343cc62b" - integrity sha512-JfmZ3MVFQkAU+zug8H3s8rZ6h0ahHZL/SpMaSasTCGYR5EEJsCc8SI5UZ6raPN2tjxa5bxS13BRpGSBUens7EA== +"@algolia/requester-fetch@5.55.1": + version "5.55.1" + resolved "https://registry.yarnpkg.com/@algolia/requester-fetch/-/requester-fetch-5.55.1.tgz#f75cc8681524bd8eb60d5d976c31f568dbdbe0ea" + integrity sha512-ukU5zeeFs44rQkzv+TRdYard+d+3lmPGs8lPZhHtWE8rfz+LlBSF6s9kP3VQ7LeOYL8Dz0u6tZfnyTrqrumbHQ== dependencies: - "@algolia/requester-common" "4.22.1" + "@algolia/client-common" "5.55.1" -"@algolia/transporter@4.22.1": - version "4.22.1" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.22.1.tgz#8843841b857dc021668f31647aa557ff19cd9cb1" - integrity sha512-kzWgc2c9IdxMa3YqA6TN0NW5VrKYYW/BELIn7vnLyn+U/RFdZ4lxxt9/8yq3DKV5snvoDzzO4ClyejZRdV3lMQ== +"@algolia/requester-node-http@5.55.1": + version "5.55.1" + resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-5.55.1.tgz#1920f83d70be909739dbc4d1421ca241bc0f1e0e" + integrity sha512-lCwXyijwPm3vbYHpBXPRomMcD6mgiptmps27gnMCf4HK+u/AOeFPBnIFh4V3l4A5SnP9VRiKBZqwGBpUH0vaTg== dependencies: - "@algolia/cache-common" "4.22.1" - "@algolia/logger-common" "4.22.1" - "@algolia/requester-common" "4.22.1" + "@algolia/client-common" "5.55.1" "@ampproject/remapping@^2.2.0": version "2.2.1" @@ -154,11 +189,25 @@ "@babel/highlight" "^7.23.4" chalk "^2.4.2" +"@babel/code-frame@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.7.tgz#f2fbbfea87c44a21590ec515b778b2c26d8866e7" + integrity sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== + dependencies: + "@babel/helper-validator-identifier" "^7.29.7" + js-tokens "^4.0.0" + picocolors "^1.1.1" + "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.3", "@babel/compat-data@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== +"@babel/compat-data@^7.28.6", "@babel/compat-data@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.7.tgz#6f0237f0f36d2e51c0570a636faed9d2d0efe629" + integrity sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg== + "@babel/core@^7.19.6", "@babel/core@^7.23.3": version "7.23.7" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.7.tgz#4d8016e06a14b5f92530a13ed0561730b5c6483f" @@ -180,6 +229,27 @@ json5 "^2.2.3" semver "^6.3.1" +"@babel/core@^7.21.3", "@babel/core@^7.25.9": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.7.tgz#80c10b17248082968b57a857b91640971f2070f7" + integrity sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.7" + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helpers" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/template" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + "@babel/generator@^7.23.3", "@babel/generator@^7.23.6": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" @@ -190,6 +260,17 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.25.9", "@babel/generator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.7.tgz#cca0b8827e6bcf3ba176788e7f3b180ad6db2fa3" + integrity sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ== + dependencies: + "@babel/parser" "^7.29.7" + "@babel/types" "^7.29.7" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + "@babel/helper-annotate-as-pure@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" @@ -197,6 +278,13 @@ dependencies: "@babel/types" "^7.22.5" +"@babel/helper-annotate-as-pure@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz#c70fe3c6ecbdc3fd2dd1b0f498428b88b82ce47f" + integrity sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw== + dependencies: + "@babel/types" "^7.29.7" + "@babel/helper-builder-binary-assignment-operator-visitor@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956" @@ -215,6 +303,17 @@ lru-cache "^5.1.1" semver "^6.3.1" +"@babel/helper-compilation-targets@^7.28.6", "@babel/helper-compilation-targets@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz#7a1def704302401c47f64fa85589e974ae217042" + integrity sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g== + dependencies: + "@babel/compat-data" "^7.29.7" + "@babel/helper-validator-option" "^7.29.7" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-create-class-features-plugin@^7.22.15", "@babel/helper-create-class-features-plugin@^7.23.6": version "7.23.7" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz#b2e6826e0e20d337143655198b79d58fdc9bd43d" @@ -230,6 +329,19 @@ "@babel/helper-split-export-declaration" "^7.22.6" semver "^6.3.1" +"@babel/helper-create-class-features-plugin@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz#6eddf286f2ec418f740c91d60a83347c55838ddd" + integrity sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-member-expression-to-functions" "^7.29.7" + "@babel/helper-optimise-call-expression" "^7.29.7" + "@babel/helper-replace-supers" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + "@babel/traverse" "^7.29.7" + semver "^6.3.1" + "@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.15", "@babel/helper-create-regexp-features-plugin@^7.22.5": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" @@ -239,6 +351,15 @@ regexpu-core "^5.3.1" semver "^6.3.1" +"@babel/helper-create-regexp-features-plugin@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz#5d4c3f928f315cf6c4184ea2fc3b5b38745b2430" + integrity sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + regexpu-core "^6.3.1" + semver "^6.3.1" + "@babel/helper-define-polyfill-provider@^0.4.4": version "0.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.4.tgz#64df615451cb30e94b59a9696022cffac9a10088" @@ -261,6 +382,17 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" +"@babel/helper-define-polyfill-provider@^0.6.5", "@babel/helper-define-polyfill-provider@^0.6.8": + version "0.6.8" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz#cf1e4462b613f2b54c41e6ff758d5dfcaa2c85d1" + integrity sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA== + dependencies: + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + debug "^4.4.3" + lodash.debounce "^4.0.8" + resolve "^1.22.11" + "@babel/helper-environment-visitor@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" @@ -274,6 +406,11 @@ "@babel/template" "^7.22.15" "@babel/types" "^7.23.0" +"@babel/helper-globals@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz#f04a96fbd8473241b1079243f5b3f03a3010ab7b" + integrity sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA== + "@babel/helper-hoist-variables@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" @@ -288,6 +425,14 @@ dependencies: "@babel/types" "^7.23.0" +"@babel/helper-member-expression-to-functions@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz#8dbdb3ce0b5c487e1aec10e13c9a43a500814df8" + integrity sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg== + dependencies: + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + "@babel/helper-module-imports@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" @@ -295,6 +440,14 @@ dependencies: "@babel/types" "^7.22.15" +"@babel/helper-module-imports@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz#ef25048a518e828d7393fac5882ddd73921d7396" + integrity sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g== + dependencies: + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + "@babel/helper-module-transforms@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" @@ -306,6 +459,15 @@ "@babel/helper-split-export-declaration" "^7.22.6" "@babel/helper-validator-identifier" "^7.22.20" +"@babel/helper-module-transforms@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz#b062747a5997ba138637201328bbff77960574ae" + integrity sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg== + dependencies: + "@babel/helper-module-imports" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/helper-optimise-call-expression@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" @@ -313,11 +475,23 @@ dependencies: "@babel/types" "^7.22.5" +"@babel/helper-optimise-call-expression@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz#77b0b5b94f1997fa9d6e3125f445227b1faf9d85" + integrity sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong== + dependencies: + "@babel/types" "^7.29.7" + "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== +"@babel/helper-plugin-utils@^7.28.6", "@babel/helper-plugin-utils@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz#c0a0766f1a13617d8a17407d7ab8f9d486225ea4" + integrity sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw== + "@babel/helper-remap-async-to-generator@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0" @@ -327,6 +501,15 @@ "@babel/helper-environment-visitor" "^7.22.20" "@babel/helper-wrap-function" "^7.22.20" +"@babel/helper-remap-async-to-generator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz#34b1f68dd75b86d31df781a29c3ff2df88da82e6" + integrity sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-wrap-function" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/helper-replace-supers@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793" @@ -336,6 +519,15 @@ "@babel/helper-member-expression-to-functions" "^7.22.15" "@babel/helper-optimise-call-expression" "^7.22.5" +"@babel/helper-replace-supers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz#bc3c3964329043c79112e513c1b198f16589ac21" + integrity sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.29.7" + "@babel/helper-optimise-call-expression" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/helper-simple-access@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" @@ -350,6 +542,14 @@ dependencies: "@babel/types" "^7.22.5" +"@babel/helper-skip-transparent-expression-wrappers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz#50c95c7e4c4f54936cfa0116428edc559862d551" + integrity sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ== + dependencies: + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + "@babel/helper-split-export-declaration@^7.22.6": version "7.22.6" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" @@ -362,16 +562,31 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz#9478c707febcbbe1ddb38a3d91a2e054ae622d83" integrity sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ== +"@babel/helper-string-parser@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f" + integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== + "@babel/helper-validator-identifier@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== +"@babel/helper-validator-identifier@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2" + integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== + "@babel/helper-validator-option@^7.22.15", "@babel/helper-validator-option@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== +"@babel/helper-validator-option@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz#cf315be940213b354eb4abcc0bd01ebe3f73bc2a" + integrity sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw== + "@babel/helper-wrap-function@^7.22.20": version "7.22.20" resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz#15352b0b9bfb10fc9c76f79f6342c00e3411a569" @@ -381,6 +596,15 @@ "@babel/template" "^7.22.15" "@babel/types" "^7.22.19" +"@babel/helper-wrap-function@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz#eec72163044548a0935e9d182bf2d547ec5ff483" + integrity sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw== + dependencies: + "@babel/template" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/types" "^7.29.7" + "@babel/helpers@^7.23.7": version "7.23.8" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.8.tgz#fc6b2d65b16847fd50adddbd4232c76378959e34" @@ -390,6 +614,14 @@ "@babel/traverse" "^7.23.7" "@babel/types" "^7.23.6" +"@babel/helpers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.29.7.tgz#45abfde7548997e34376c3e69feb475cffb4a607" + integrity sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg== + dependencies: + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.7" + "@babel/highlight@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" @@ -404,6 +636,28 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b" integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== +"@babel/parser@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.7.tgz#837b87387cbf5ec5530cb634b3c622f68edb9334" + integrity sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg== + dependencies: + "@babel/types" "^7.29.7" + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz#2b535896d933a85aa92377eaa3d51a437d54a4e3" + integrity sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/traverse" "^7.29.7" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz#b00711a9e52bf4fe55ef7e54b2ef4a881bf804c8" + integrity sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.23.3.tgz#5cd1c87ba9380d0afb78469292c954fee5d2411a" @@ -411,6 +665,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz#2375328852026a3cf6bc0bcf2de7d236f2d5e701" + integrity sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz#759a857c46c4d2a6199685cf71070d81ae5f743a" + integrity sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.23.3.tgz#f6652bb16b94f8f9c20c50941e16e9756898dc5d" @@ -420,6 +689,15 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" "@babel/plugin-transform-optional-chaining" "^7.23.3" +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz#86de98dd8e03836178231ea96c27dab26016a705" + integrity sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + "@babel/plugin-transform-optional-chaining" "^7.29.7" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.23.7": version "7.23.7" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.23.7.tgz#516462a95d10a9618f197d39ad291a9b47ae1d7b" @@ -428,6 +706,14 @@ "@babel/helper-environment-visitor" "^7.22.20" "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz#f5d892681dbf4b08753436a5e55000d5ba728d6d" + integrity sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" @@ -475,6 +761,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-syntax-import-assertions@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz#c5cd868505269126cc18882e1f01f7b0e0e24b4e" + integrity sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-syntax-import-attributes@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.23.3.tgz#992aee922cf04512461d7dae3ff6951b90a2dc06" @@ -482,6 +775,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-syntax-import-attributes@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz#6115264516e95ead0f35a41710906612e447f605" + integrity sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-syntax-import-meta@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" @@ -503,6 +803,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-syntax-jsx@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz#622c16f9ad63782fe6e83dadc7e40330744b7f1e" + integrity sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-syntax-logical-assignment-operators@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" @@ -566,6 +873,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-syntax-typescript@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz#7c29388932313ed58413a0343048d75d92fb5b24" + integrity sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" @@ -581,6 +895,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-arrow-functions@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz#d651343f562c03f47951bd1802195d0e10605f27" + integrity sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-async-generator-functions@^7.23.7": version "7.23.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.7.tgz#3aa0b4f2fa3788b5226ef9346cf6d16ec61f99cd" @@ -591,6 +912,15 @@ "@babel/helper-remap-async-to-generator" "^7.22.20" "@babel/plugin-syntax-async-generators" "^7.8.4" +"@babel/plugin-transform-async-generator-functions@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz#a5365617921d82a1fee33124a1102bb38a1e677d" + integrity sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-remap-async-to-generator" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/plugin-transform-async-to-generator@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz#d1f513c7a8a506d43f47df2bf25f9254b0b051fa" @@ -600,6 +930,15 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-remap-async-to-generator" "^7.22.20" +"@babel/plugin-transform-async-to-generator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz#3b5e8f1fb58133cf701bcf0baaf6f01bfd1a8889" + integrity sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w== + dependencies: + "@babel/helper-module-imports" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-remap-async-to-generator" "^7.29.7" + "@babel/plugin-transform-block-scoped-functions@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.23.3.tgz#fe1177d715fb569663095e04f3598525d98e8c77" @@ -607,6 +946,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-block-scoped-functions@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz#96d292634434082d6687bcdb81139affedf77e8c" + integrity sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-block-scoping@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.4.tgz#b2d38589531c6c80fbe25e6b58e763622d2d3cf5" @@ -614,6 +960,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-block-scoping@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz#baa376691ae16244cd14335422fca6900f54e17d" + integrity sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-class-properties@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.23.3.tgz#35c377db11ca92a785a718b6aa4e3ed1eb65dc48" @@ -622,6 +975,14 @@ "@babel/helper-create-class-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-class-properties@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz#034897b8a21beec163332fac2de235b14409abdf" + integrity sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-class-static-block@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.23.4.tgz#2a202c8787a8964dd11dfcedf994d36bfc844ab5" @@ -631,6 +992,14 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-class-static-block" "^7.14.5" +"@babel/plugin-transform-class-static-block@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz#fed8efd19f3dd3e1114ee390707c70912778fd7c" + integrity sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-classes@^7.23.8": version "7.23.8" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.23.8.tgz#d08ae096c240347badd68cdf1b6d1624a6435d92" @@ -645,6 +1014,18 @@ "@babel/helper-split-export-declaration" "^7.22.6" globals "^11.1.0" +"@babel/plugin-transform-classes@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz#61d3e5aaae0c838acc3204d9db7c8dc05c25815b" + integrity sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-globals" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-replace-supers" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/plugin-transform-computed-properties@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.23.3.tgz#652e69561fcc9d2b50ba4f7ac7f60dcf65e86474" @@ -653,6 +1034,14 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/template" "^7.22.15" +"@babel/plugin-transform-computed-properties@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz#95028787ca31901b9a20b5c6d9605c32346f55ad" + integrity sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/template" "^7.29.7" + "@babel/plugin-transform-destructuring@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.3.tgz#8c9ee68228b12ae3dff986e56ed1ba4f3c446311" @@ -660,6 +1049,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-destructuring@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz#5781ec6947852e27b64c1165f0db431f408090e4" + integrity sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/plugin-transform-dotall-regex@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.23.3.tgz#3f7af6054882ede89c378d0cf889b854a993da50" @@ -668,6 +1065,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-dotall-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz#b203de9740e4c7ff6b55ce436ed5313b88d70af8" + integrity sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-duplicate-keys@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.23.3.tgz#664706ca0a5dfe8d066537f99032fc1dc8b720ce" @@ -675,6 +1080,21 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-duplicate-keys@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz#8f3fe721835cb7a433420841dae90afc962ea7ae" + integrity sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz#dc6c405e55c01b7657e1827a25332c4ac17e9cac" + integrity sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-dynamic-import@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.23.4.tgz#c7629e7254011ac3630d47d7f34ddd40ca535143" @@ -683,6 +1103,21 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-dynamic-import" "^7.8.3" +"@babel/plugin-transform-dynamic-import@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz#a83a6faec5bab5b619adf9d0eac6c1c270123c2a" + integrity sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-explicit-resource-management@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz#65c8b9f76ec915b02a0e1df703125a0fca58abaa" + integrity sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-destructuring" "^7.29.7" + "@babel/plugin-transform-exponentiation-operator@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.23.3.tgz#ea0d978f6b9232ba4722f3dbecdd18f450babd18" @@ -691,6 +1126,13 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-exponentiation-operator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz#00bf002fde8794356171f5d4df200f6bc0d5a303" + integrity sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-export-namespace-from@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.23.4.tgz#084c7b25e9a5c8271e987a08cf85807b80283191" @@ -699,6 +1141,13 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-export-namespace-from" "^7.8.3" +"@babel/plugin-transform-export-namespace-from@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz#d6014f45cec61d7691335c6c9804204bee801d51" + integrity sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-for-of@^7.23.6": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.23.6.tgz#81c37e24171b37b370ba6aaffa7ac86bcb46f94e" @@ -707,6 +1156,14 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" +"@babel/plugin-transform-for-of@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz#c65a678592117717aacdb10c1b73a9cb85e830be" + integrity sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + "@babel/plugin-transform-function-name@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.23.3.tgz#8f424fcd862bf84cb9a1a6b42bc2f47ed630f8dc" @@ -716,6 +1173,15 @@ "@babel/helper-function-name" "^7.23.0" "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-function-name@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz#8b87f8a7504dbcd96135167e3fc4f61126a7bd86" + integrity sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg== + dependencies: + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/plugin-transform-json-strings@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.23.4.tgz#a871d9b6bd171976efad2e43e694c961ffa3714d" @@ -724,6 +1190,13 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-json-strings" "^7.8.3" +"@babel/plugin-transform-json-strings@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz#f57d63dcc05b4481c281acedcd8fc4e3e439a1d4" + integrity sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-literals@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.23.3.tgz#8214665f00506ead73de157eba233e7381f3beb4" @@ -731,6 +1204,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-literals@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz#b90bd47463326c2a9d779e1bd5e1f88b9f421921" + integrity sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-logical-assignment-operators@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.23.4.tgz#e599f82c51d55fac725f62ce55d3a0886279ecb5" @@ -739,6 +1219,13 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" +"@babel/plugin-transform-logical-assignment-operators@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz#9b29425adf5c794967aabe4b046a046a167bac2f" + integrity sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-member-expression-literals@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.23.3.tgz#e37b3f0502289f477ac0e776b05a833d853cabcc" @@ -746,6 +1233,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-member-expression-literals@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz#1281689fa2fefc17b110d21ebafd0fe9402d5309" + integrity sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-modules-amd@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.3.tgz#e19b55436a1416829df0a1afc495deedfae17f7d" @@ -754,6 +1248,14 @@ "@babel/helper-module-transforms" "^7.23.3" "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-modules-amd@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz#f05ca662c8a1dc4be2f337af9c7e80369c942d6c" + integrity sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew== + dependencies: + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-modules-commonjs@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz#661ae831b9577e52be57dd8356b734f9700b53b4" @@ -763,6 +1265,14 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-simple-access" "^7.22.5" +"@babel/plugin-transform-modules-commonjs@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz#70e6835abf2663dafbe94b8ef1f51de7351ef135" + integrity sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ== + dependencies: + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-modules-systemjs@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.3.tgz#fa7e62248931cb15b9404f8052581c302dd9de81" @@ -773,6 +1283,16 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-validator-identifier" "^7.22.20" +"@babel/plugin-transform-modules-systemjs@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz#e575dd2ab9882906de120ff7dc9dee9914d8b6f3" + integrity sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ== + dependencies: + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/plugin-transform-modules-umd@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.23.3.tgz#5d4395fccd071dfefe6585a4411aa7d6b7d769e9" @@ -781,6 +1301,14 @@ "@babel/helper-module-transforms" "^7.23.3" "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-modules-umd@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz#391d1c0215aca6307257f2f608598dfe55feb6cf" + integrity sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA== + dependencies: + "@babel/helper-module-transforms" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f" @@ -789,6 +1317,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-named-capturing-groups-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz#21e75d847b31189842fa7a77703722ed4b43d27d" + integrity sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-new-target@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.23.3.tgz#5491bb78ed6ac87e990957cea367eab781c4d980" @@ -796,6 +1332,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-new-target@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz#714147ce7947e1b49cbd84137ca2e75e92b2a067" + integrity sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-nullish-coalescing-operator@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.23.4.tgz#45556aad123fc6e52189ea749e33ce090637346e" @@ -804,6 +1347,13 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" +"@babel/plugin-transform-nullish-coalescing-operator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz#8a54cdf88c3f50433a6173117a286195b67714cc" + integrity sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-numeric-separator@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.23.4.tgz#03d08e3691e405804ecdd19dd278a40cca531f29" @@ -812,6 +1362,13 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-numeric-separator" "^7.10.4" +"@babel/plugin-transform-numeric-separator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz#0266d5cd42ab87ec40fee45a4e36483cfdcbc66a" + integrity sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-object-rest-spread@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.23.4.tgz#2b9c2d26bf62710460bdc0d1730d4f1048361b83" @@ -823,6 +1380,17 @@ "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-transform-parameters" "^7.23.3" +"@babel/plugin-transform-object-rest-spread@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz#e0d5060241803922c545676613cc8acbbda0d266" + integrity sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A== + dependencies: + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-destructuring" "^7.29.7" + "@babel/plugin-transform-parameters" "^7.29.7" + "@babel/traverse" "^7.29.7" + "@babel/plugin-transform-object-super@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.23.3.tgz#81fdb636dcb306dd2e4e8fd80db5b2362ed2ebcd" @@ -831,6 +1399,14 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-replace-supers" "^7.22.20" +"@babel/plugin-transform-object-super@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz#e89283d14fa3c35817d4493ffc6bc649aa10e4eb" + integrity sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-replace-supers" "^7.29.7" + "@babel/plugin-transform-optional-catch-binding@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.23.4.tgz#318066de6dacce7d92fa244ae475aa8d91778017" @@ -839,6 +1415,13 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" +"@babel/plugin-transform-optional-catch-binding@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz#729664f79985be504eba112c51de9f71d009030b" + integrity sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-optional-chaining@^7.23.3", "@babel/plugin-transform-optional-chaining@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.4.tgz#6acf61203bdfc4de9d4e52e64490aeb3e52bd017" @@ -848,6 +1431,14 @@ "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" "@babel/plugin-syntax-optional-chaining" "^7.8.3" +"@babel/plugin-transform-optional-chaining@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz#b84a1b574b3c73001023092567e16c492b720e51" + integrity sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + "@babel/plugin-transform-parameters@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.23.3.tgz#83ef5d1baf4b1072fa6e54b2b0999a7b2527e2af" @@ -855,6 +1446,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-parameters@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz#a5ddc3b9bfb534814cb8334cbeba47d9cf9db090" + integrity sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-private-methods@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.23.3.tgz#b2d7a3c97e278bfe59137a978d53b2c2e038c0e4" @@ -863,6 +1461,14 @@ "@babel/helper-create-class-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-private-methods@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz#cea8bd3ab99533892897a02999d5b752584ad145" + integrity sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-private-property-in-object@^7.23.4": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.23.4.tgz#3ec711d05d6608fd173d9b8de39872d8dbf68bf5" @@ -873,6 +1479,15 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-private-property-in-object" "^7.14.5" +"@babel/plugin-transform-private-property-in-object@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz#4a2f6be5aba47be7afbdb4cd7903c46edf3a7661" + integrity sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-create-class-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-property-literals@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.23.3.tgz#54518f14ac4755d22b92162e4a852d308a560875" @@ -880,6 +1495,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-property-literals@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz#d45817cd72f9e134ab1f7fbb79264cfcb85cf636" + integrity sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-react-constant-elements@^7.18.12": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.23.3.tgz#5efc001d07ef0f7da0d73c3a86c132f73d28e43c" @@ -887,6 +1509,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-react-constant-elements@^7.21.3": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.29.7.tgz#8264440ea2ffc5ec405aebd7eac074815d6e28b1" + integrity sha512-J0wGhKan+rIiE2OhfhRptySLrJ6SjQYM6b6N1FMlhyhCcw1Mig8vQjWchyB+bgHGDvaWo6Diu6CLRMra2uMtmg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-react-display-name@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.23.3.tgz#70529f034dd1e561045ad3c8152a267f0d7b6200" @@ -894,6 +1523,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-react-display-name@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz#bf161a6d750267b79db7ff6f8fb89c3369b02df3" + integrity sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-react-jsx-development@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz#e716b6edbef972a92165cd69d92f1255f7e73e87" @@ -901,6 +1537,13 @@ dependencies: "@babel/plugin-transform-react-jsx" "^7.22.5" +"@babel/plugin-transform-react-jsx-development@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz#64e6aacb5cb43b9e80d3d5f19ddefc158a624f09" + integrity sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.29.7" + "@babel/plugin-transform-react-jsx@^7.22.15", "@babel/plugin-transform-react-jsx@^7.22.5": version "7.23.4" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz#393f99185110cea87184ea47bcb4a7b0c2e39312" @@ -912,6 +1555,17 @@ "@babel/plugin-syntax-jsx" "^7.23.3" "@babel/types" "^7.23.4" +"@babel/plugin-transform-react-jsx@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz#3d16a0e5773f079400a8c82a190709cdf92ee204" + integrity sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-module-imports" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-syntax-jsx" "^7.29.7" + "@babel/types" "^7.29.7" + "@babel/plugin-transform-react-pure-annotations@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.23.3.tgz#fabedbdb8ee40edf5da96f3ecfc6958e3783b93c" @@ -920,6 +1574,14 @@ "@babel/helper-annotate-as-pure" "^7.22.5" "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-react-pure-annotations@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz#76445c90112dd0a7371b63264563bfa9a4fcd6e3" + integrity sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-regenerator@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.23.3.tgz#141afd4a2057298602069fce7f2dc5173e6c561c" @@ -928,6 +1590,21 @@ "@babel/helper-plugin-utils" "^7.22.5" regenerator-transform "^0.15.2" +"@babel/plugin-transform-regenerator@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz#0f42626a7dbb0e7a7f52e036d3e43deebdc3ea4e" + integrity sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + +"@babel/plugin-transform-regexp-modifiers@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz#68311c0c10af2198212528863f8542843e424025" + integrity sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-reserved-words@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.23.3.tgz#4130dcee12bd3dd5705c587947eb715da12efac8" @@ -935,6 +1612,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-reserved-words@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz#a6feeb179b36a5f1fc6e3154c1eb727bdbe35876" + integrity sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-runtime@^7.22.9": version "7.23.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.7.tgz#52bbd20054855beb9deae3bee9ceb05289c343e6" @@ -947,6 +1631,18 @@ babel-plugin-polyfill-regenerator "^0.5.4" semver "^6.3.1" +"@babel/plugin-transform-runtime@^7.25.9": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz#7c7fb6e2a46dce67e278b6cc84421c1d16da5695" + integrity sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q== + dependencies: + "@babel/helper-module-imports" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + babel-plugin-polyfill-corejs2 "^0.4.14" + babel-plugin-polyfill-corejs3 "^0.13.0" + babel-plugin-polyfill-regenerator "^0.6.5" + semver "^6.3.1" + "@babel/plugin-transform-shorthand-properties@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.23.3.tgz#97d82a39b0e0c24f8a981568a8ed851745f59210" @@ -954,6 +1650,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-shorthand-properties@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz#25c0436b98f4bd9ca4b98e1fbd662743bbaab9bf" + integrity sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-spread@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.23.3.tgz#41d17aacb12bde55168403c6f2d6bdca563d362c" @@ -962,6 +1665,14 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" +"@babel/plugin-transform-spread@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz#a128bcdd6b5e5e47054907b2e50bc19c3f856edd" + integrity sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + "@babel/plugin-transform-sticky-regex@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.23.3.tgz#dec45588ab4a723cb579c609b294a3d1bd22ff04" @@ -969,6 +1680,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-sticky-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz#a42c0fd1fa42f7e98e1e0c7757f72a1bbca3a015" + integrity sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-template-literals@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.23.3.tgz#5f0f028eb14e50b5d0f76be57f90045757539d07" @@ -976,6 +1694,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-template-literals@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz#ada97d8e0832bca8edb315888aa654b1570f3835" + integrity sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-typeof-symbol@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.23.3.tgz#9dfab97acc87495c0c449014eb9c547d8966bca4" @@ -983,6 +1708,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-typeof-symbol@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz#d848a4677c1ee3485ab017f4018f04597798911c" + integrity sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-typescript@^7.23.3": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz#aa36a94e5da8d94339ae3a4e22d40ed287feb34c" @@ -993,6 +1725,17 @@ "@babel/helper-plugin-utils" "^7.22.5" "@babel/plugin-syntax-typescript" "^7.23.3" +"@babel/plugin-transform-typescript@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz#f0449c3df7037bbe232043476851c38f5e4a7615" + integrity sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.29.7" + "@babel/helper-create-class-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.29.7" + "@babel/plugin-syntax-typescript" "^7.29.7" + "@babel/plugin-transform-unicode-escapes@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.23.3.tgz#1f66d16cab01fab98d784867d24f70c1ca65b925" @@ -1000,6 +1743,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-unicode-escapes@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz#1e99554b0cddfd650d649a9f2b996049893e5720" + integrity sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-unicode-property-regex@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.23.3.tgz#19e234129e5ffa7205010feec0d94c251083d7ad" @@ -1008,6 +1758,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-unicode-property-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz#44444afc73768c2190fac4d95f7716817b7f204a" + integrity sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-unicode-regex@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.23.3.tgz#26897708d8f42654ca4ce1b73e96140fbad879dc" @@ -1016,6 +1774,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-unicode-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz#c3064b293ff7f1794b71f7650eec8db9896d3e59" + integrity sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/plugin-transform-unicode-sets-regex@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.23.3.tgz#4fb6f0a719c2c5859d11f6b55a050cc987f3799e" @@ -1024,6 +1790,14 @@ "@babel/helper-create-regexp-features-plugin" "^7.22.15" "@babel/helper-plugin-utils" "^7.22.5" +"@babel/plugin-transform-unicode-sets-regex@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz#b03ac9f27326f6197e8e574add83bbf33fc34ecd" + integrity sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/preset-env@^7.19.4", "@babel/preset-env@^7.22.9": version "7.23.8" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.23.8.tgz#7d6f8171ea7c221ecd28059e65ad37c20e441e3e" @@ -1110,6 +1884,83 @@ core-js-compat "^3.31.0" semver "^6.3.1" +"@babel/preset-env@^7.20.2", "@babel/preset-env@^7.25.9": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.7.tgz#5e2ab5e764b493fdefc99c43aeaa70a9533a37fd" + integrity sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA== + dependencies: + "@babel/compat-data" "^7.29.7" + "@babel/helper-compilation-targets" "^7.29.7" + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-validator-option" "^7.29.7" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.29.7" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.29.7" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.29.7" + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array" "^7.29.7" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.29.7" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.29.7" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.29.7" + "@babel/plugin-syntax-import-attributes" "^7.29.7" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.29.7" + "@babel/plugin-transform-async-generator-functions" "^7.29.7" + "@babel/plugin-transform-async-to-generator" "^7.29.7" + "@babel/plugin-transform-block-scoped-functions" "^7.29.7" + "@babel/plugin-transform-block-scoping" "^7.29.7" + "@babel/plugin-transform-class-properties" "^7.29.7" + "@babel/plugin-transform-class-static-block" "^7.29.7" + "@babel/plugin-transform-classes" "^7.29.7" + "@babel/plugin-transform-computed-properties" "^7.29.7" + "@babel/plugin-transform-destructuring" "^7.29.7" + "@babel/plugin-transform-dotall-regex" "^7.29.7" + "@babel/plugin-transform-duplicate-keys" "^7.29.7" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.29.7" + "@babel/plugin-transform-dynamic-import" "^7.29.7" + "@babel/plugin-transform-explicit-resource-management" "^7.29.7" + "@babel/plugin-transform-exponentiation-operator" "^7.29.7" + "@babel/plugin-transform-export-namespace-from" "^7.29.7" + "@babel/plugin-transform-for-of" "^7.29.7" + "@babel/plugin-transform-function-name" "^7.29.7" + "@babel/plugin-transform-json-strings" "^7.29.7" + "@babel/plugin-transform-literals" "^7.29.7" + "@babel/plugin-transform-logical-assignment-operators" "^7.29.7" + "@babel/plugin-transform-member-expression-literals" "^7.29.7" + "@babel/plugin-transform-modules-amd" "^7.29.7" + "@babel/plugin-transform-modules-commonjs" "^7.29.7" + "@babel/plugin-transform-modules-systemjs" "^7.29.7" + "@babel/plugin-transform-modules-umd" "^7.29.7" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.29.7" + "@babel/plugin-transform-new-target" "^7.29.7" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.29.7" + "@babel/plugin-transform-numeric-separator" "^7.29.7" + "@babel/plugin-transform-object-rest-spread" "^7.29.7" + "@babel/plugin-transform-object-super" "^7.29.7" + "@babel/plugin-transform-optional-catch-binding" "^7.29.7" + "@babel/plugin-transform-optional-chaining" "^7.29.7" + "@babel/plugin-transform-parameters" "^7.29.7" + "@babel/plugin-transform-private-methods" "^7.29.7" + "@babel/plugin-transform-private-property-in-object" "^7.29.7" + "@babel/plugin-transform-property-literals" "^7.29.7" + "@babel/plugin-transform-regenerator" "^7.29.7" + "@babel/plugin-transform-regexp-modifiers" "^7.29.7" + "@babel/plugin-transform-reserved-words" "^7.29.7" + "@babel/plugin-transform-shorthand-properties" "^7.29.7" + "@babel/plugin-transform-spread" "^7.29.7" + "@babel/plugin-transform-sticky-regex" "^7.29.7" + "@babel/plugin-transform-template-literals" "^7.29.7" + "@babel/plugin-transform-typeof-symbol" "^7.29.7" + "@babel/plugin-transform-unicode-escapes" "^7.29.7" + "@babel/plugin-transform-unicode-property-regex" "^7.29.7" + "@babel/plugin-transform-unicode-regex" "^7.29.7" + "@babel/plugin-transform-unicode-sets-regex" "^7.29.7" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.15" + babel-plugin-polyfill-corejs3 "^0.14.0" + babel-plugin-polyfill-regenerator "^0.6.6" + core-js-compat "^3.48.0" + semver "^6.3.1" + "@babel/preset-modules@0.1.6-no-external-plugins": version "0.1.6-no-external-plugins" resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" @@ -1131,6 +1982,18 @@ "@babel/plugin-transform-react-jsx-development" "^7.22.5" "@babel/plugin-transform-react-pure-annotations" "^7.23.3" +"@babel/preset-react@^7.25.9": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.29.7.tgz#2ed18366e38c2081bbf1760dc01e88fa5674eb17" + integrity sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-validator-option" "^7.29.7" + "@babel/plugin-transform-react-display-name" "^7.29.7" + "@babel/plugin-transform-react-jsx" "^7.29.7" + "@babel/plugin-transform-react-jsx-development" "^7.29.7" + "@babel/plugin-transform-react-pure-annotations" "^7.29.7" + "@babel/preset-typescript@^7.18.6", "@babel/preset-typescript@^7.22.5": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz#14534b34ed5b6d435aa05f1ae1c5e7adcc01d913" @@ -1142,6 +2005,17 @@ "@babel/plugin-transform-modules-commonjs" "^7.23.3" "@babel/plugin-transform-typescript" "^7.23.3" +"@babel/preset-typescript@^7.21.0", "@babel/preset-typescript@^7.25.9": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz#de9be1f47b785c979ec7b3a71f4cd8bae5267b62" + integrity sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ== + dependencies: + "@babel/helper-plugin-utils" "^7.29.7" + "@babel/helper-validator-option" "^7.29.7" + "@babel/plugin-syntax-jsx" "^7.29.7" + "@babel/plugin-transform-modules-commonjs" "^7.29.7" + "@babel/plugin-transform-typescript" "^7.29.7" + "@babel/regjsgen@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" @@ -1162,6 +2036,11 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.25.9": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.29.7.tgz#12022450c45a4da6d8d8287b18a4ff2ddb23f768" + integrity sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw== + "@babel/template@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -1171,6 +2050,15 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" +"@babel/template@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.29.7.tgz#4d9d4004f645cdd304de958c725162784ecac700" + integrity sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/types" "^7.29.7" + "@babel/traverse@^7.22.8", "@babel/traverse@^7.23.7": version "7.23.7" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305" @@ -1187,6 +2075,19 @@ debug "^4.3.1" globals "^11.1.0" +"@babel/traverse@^7.25.9", "@babel/traverse@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.7.tgz#c47b07a41b95da0907d026b5dd894d98de7d2f2d" + integrity sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw== + dependencies: + "@babel/code-frame" "^7.29.7" + "@babel/generator" "^7.29.7" + "@babel/helper-globals" "^7.29.7" + "@babel/parser" "^7.29.7" + "@babel/template" "^7.29.7" + "@babel/types" "^7.29.7" + debug "^4.3.1" + "@babel/types@^7.20.0", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.4", "@babel/types@^7.23.6", "@babel/types@^7.4.4": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.6.tgz#be33fdb151e1f5a56877d704492c240fc71c7ccd" @@ -1196,49 +2097,505 @@ "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" +"@babel/types@^7.21.3", "@babel/types@^7.29.7": + version "7.29.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.7.tgz#8005e31d82712ee7adaef6e23c63b71a62770a92" + integrity sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA== + dependencies: + "@babel/helper-string-parser" "^7.29.7" + "@babel/helper-validator-identifier" "^7.29.7" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== -"@discoveryjs/json-ext@0.5.7": - version "0.5.7" - resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" - integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== +"@csstools/cascade-layer-name-parser@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz#43f962bebead0052a9fed1a2deeb11f85efcbc72" + integrity sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A== -"@docsearch/css@3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.5.2.tgz#610f47b48814ca94041df969d9fcc47b91fc5aac" - integrity sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA== +"@csstools/color-helpers@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz#106c54c808cabfd1ab4c602d8505ee584c2996ef" + integrity sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA== -"@docsearch/react@^3.5.2": - version "3.5.2" - resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.5.2.tgz#2e6bbee00eb67333b64906352734da6aef1232b9" - integrity sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng== - dependencies: - "@algolia/autocomplete-core" "1.9.3" - "@algolia/autocomplete-preset-algolia" "1.9.3" - "@docsearch/css" "3.5.2" - algoliasearch "^4.19.1" +"@csstools/css-calc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.4.tgz#8473f63e2fcd6e459838dd412401d5948f224c65" + integrity sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ== -"@docusaurus/core@3.1.0", "@docusaurus/core@^3.1.0": +"@csstools/css-color-parser@^3.1.0": version "3.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.1.0.tgz#b66e7eaf867c1f44738d725d217a1c0e879629d7" - integrity sha512-GWudMGYA9v26ssbAWJNfgeDZk+lrudUTclLPRsmxiknEBk7UMp7Rglonhqbsf3IKHOyHkMU4Fr5jFyg5SBx9jQ== + resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz#4e386af3a99dd36c46fef013cfe4c1c341eed6f0" + integrity sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA== dependencies: - "@babel/core" "^7.23.3" - "@babel/generator" "^7.23.3" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - "@babel/plugin-transform-runtime" "^7.22.9" - "@babel/preset-env" "^7.22.9" - "@babel/preset-react" "^7.22.5" - "@babel/preset-typescript" "^7.22.5" - "@babel/runtime" "^7.22.6" - "@babel/runtime-corejs3" "^7.22.6" - "@babel/traverse" "^7.22.8" - "@docusaurus/cssnano-preset" "3.1.0" - "@docusaurus/logger" "3.1.0" - "@docusaurus/mdx-loader" "3.1.0" + "@csstools/color-helpers" "^5.1.0" + "@csstools/css-calc" "^2.1.4" + +"@csstools/css-parser-algorithms@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz#5755370a9a29abaec5515b43c8b3f2cf9c2e3076" + integrity sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ== + +"@csstools/css-tokenizer@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz#333fedabc3fd1a8e5d0100013731cf19e6a8c5d3" + integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw== + +"@csstools/media-query-list-parser@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz#7aec77bcb89c2da80ef207e73f474ef9e1b3cdf1" + integrity sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ== + +"@csstools/postcss-alpha-function@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-alpha-function/-/postcss-alpha-function-1.0.1.tgz#7989605711de7831bc7cd75b94c9b5bac9c3728e" + integrity sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-cascade-layers@^5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz#dd2c70db3867b88975f2922da3bfbae7d7a2cae7" + integrity sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-color-function-display-p3-linear@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-1.0.1.tgz#3017ff5e1f65307d6083e58e93d76724fb1ebf9f" + integrity sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-color-function@^4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz#a7c85a98c77b522a194a1bbb00dd207f40c7a771" + integrity sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-color-mix-function@^3.0.12": + version "3.0.12" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.12.tgz#2f1ee9f8208077af069545c9bd79bb9733382c2a" + integrity sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-color-mix-variadic-function-arguments@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.2.tgz#b4012b62a4eaa24d694172bb7137f9d2319cb8f2" + integrity sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-content-alt-text@^2.0.8": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.8.tgz#1d52da1762893c32999ff76839e48d6ec7c7a4cb" + integrity sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-contrast-color-function@^2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-2.0.12.tgz#ca46986d095c60f208d9e3f24704d199c9172637" + integrity sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-exponential-functions@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz#fc03d1272888cb77e64cc1a7d8a33016e4f05c69" + integrity sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-font-format-keywords@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz#6730836eb0153ff4f3840416cc2322f129c086e6" + integrity sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-gamut-mapping@^2.0.11": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.11.tgz#be0e34c9f0142852cccfc02b917511f0d677db8b" + integrity sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-gradients-interpolation-method@^5.0.12": + version "5.0.12" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.12.tgz#0955cce4d97203b861bf66742bbec611b2f3661c" + integrity sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-hwb-function@^4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.12.tgz#07f7ecb08c50e094673bd20eaf7757db0162beee" + integrity sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-ic-unit@^4.0.4": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.4.tgz#2ee2da0690db7edfbc469279711b9e69495659d2" + integrity sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-initial@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz#c385bd9d8ad31ad159edd7992069e97ceea4d09a" + integrity sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg== + +"@csstools/postcss-is-pseudo-class@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz#d34e850bcad4013c2ed7abe948bfa0448aa8eb74" + integrity sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-light-dark-function@^2.0.11": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz#0df448aab9a33cb9a085264ff1f396fb80c4437d" + integrity sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-logical-float-and-clear@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz#62617564182cf86ab5d4e7485433ad91e4c58571" + integrity sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ== + +"@csstools/postcss-logical-overflow@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz#c6de7c5f04e3d4233731a847f6c62819bcbcfa1d" + integrity sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA== + +"@csstools/postcss-logical-overscroll-behavior@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz#43c03eaecdf34055ef53bfab691db6dc97a53d37" + integrity sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w== + +"@csstools/postcss-logical-resize@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz#4df0eeb1a61d7bd85395e56a5cce350b5dbfdca6" + integrity sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-logical-viewport-units@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz#016d98a8b7b5f969e58eb8413447eb801add16fc" + integrity sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ== + dependencies: + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-media-minmax@^2.0.9": + version "2.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz#184252d5b93155ae526689328af6bdf3fc113987" + integrity sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/media-query-list-parser" "^4.0.3" + +"@csstools/postcss-media-queries-aspect-ratio-number-values@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz#f485c31ec13d6b0fb5c528a3474334a40eff5f11" + integrity sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/media-query-list-parser" "^4.0.3" + +"@csstools/postcss-nested-calc@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz#754e10edc6958d664c11cde917f44ba144141c62" + integrity sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-normalize-display-values@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.1.tgz#3738ecadb38cd6521c9565635d61aa4bf5457d27" + integrity sha512-TQUGBuRvxdc7TgNSTevYqrL8oItxiwPDixk20qCB5me/W8uF7BPbhRrAvFuhEoywQp/woRsUZ6SJ+sU5idZAIA== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-oklab-function@^4.0.12": + version "4.0.12" + resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.12.tgz#416640ef10227eea1375b47b72d141495950971d" + integrity sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-position-area-property@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-position-area-property/-/postcss-position-area-property-1.0.0.tgz#41f0cbc737a81a42890d5ec035fa26a45f4f4ad4" + integrity sha512-fUP6KR8qV2NuUZV3Cw8itx0Ep90aRjAZxAEzC3vrl6yjFv+pFsQbR18UuQctEKmA72K9O27CoYiKEgXxkqjg8Q== + +"@csstools/postcss-progressive-custom-properties@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.2.1.tgz#c39780b9ff0d554efb842b6bd75276aa6f1705db" + integrity sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-property-rule-prelude-list@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-property-rule-prelude-list/-/postcss-property-rule-prelude-list-1.0.0.tgz#700b7aa41228c02281bda074ae778f36a09da188" + integrity sha512-IxuQjUXq19fobgmSSvUDO7fVwijDJaZMvWQugxfEUxmjBeDCVaDuMpsZ31MsTm5xbnhA+ElDi0+rQ7sQQGisFA== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-random-function@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz#3191f32fe72936e361dadf7dbfb55a0209e2691e" + integrity sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-relative-color-syntax@^3.0.12": + version "3.0.12" + resolved "https://registry.yarnpkg.com/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.12.tgz#ced792450102441f7c160e1d106f33e4b44181f8" + integrity sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-scope-pseudo-class@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz#9fe60e9d6d91d58fb5fc6c768a40f6e47e89a235" + integrity sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q== + dependencies: + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-sign-functions@^1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz#a9ac56954014ae4c513475b3f1b3e3424a1e0c12" + integrity sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-stepped-value-functions@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz#36036f1a0e5e5ee2308e72f3c9cb433567c387b9" + integrity sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-syntax-descriptor-syntax-production@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-syntax-descriptor-syntax-production/-/postcss-syntax-descriptor-syntax-production-1.0.1.tgz#98590e372e547cdae60aef47cfee11f3881307dd" + integrity sha512-GneqQWefjM//f4hJ/Kbox0C6f2T7+pi4/fqTqOFGTL3EjnvOReTqO1qUQ30CaUjkwjYq9qZ41hzarrAxCc4gow== + dependencies: + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-system-ui-font-family@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-system-ui-font-family/-/postcss-system-ui-font-family-1.0.0.tgz#bd65b79078debf6f67b318dc9b71a8f9fa16f8c8" + integrity sha512-s3xdBvfWYfoPSBsikDXbuorcMG1nN1M6GdU0qBsGfcmNR0A/qhloQZpTxjA3Xsyrk1VJvwb2pOfiOT3at/DuIQ== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-text-decoration-shorthand@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.3.tgz#fae1b70f07d1b7beb4c841c86d69e41ecc6f743c" + integrity sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA== + dependencies: + "@csstools/color-helpers" "^5.1.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-trigonometric-functions@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz#3f94ed2e319b57f2c59720b64e4d0a8a6fb8c3b2" + integrity sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A== + dependencies: + "@csstools/css-calc" "^2.1.4" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + +"@csstools/postcss-unset-value@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz#7caa981a34196d06a737754864baf77d64de4bba" + integrity sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA== + +"@csstools/selector-resolve-nested@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz#848c6f44cb65e3733e478319b9342b7aa436fac7" + integrity sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g== + +"@csstools/selector-specificity@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz#037817b574262134cabd68fc4ec1a454f168407b" + integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw== + +"@csstools/utilities@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/utilities/-/utilities-2.0.0.tgz#f7ff0fee38c9ffb5646d47b6906e0bc8868bde60" + integrity sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ== + +"@discoveryjs/json-ext@0.5.7": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@docsearch/core@4.6.3": + version "4.6.3" + resolved "https://registry.yarnpkg.com/@docsearch/core/-/core-4.6.3.tgz#9954c75c3ae28418e06f8e7537a920d6cd2bc22e" + integrity sha512-rUOujwIpxJRgD7+kicVsI3D5sqBvdiRTquzWBpTEXZs8ZXfGbfzpus5HqumaNYTppN2HvH8E2yNuRwYdHJeOlA== + +"@docsearch/css@4.6.3": + version "4.6.3" + resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-4.6.3.tgz#a94065af4a996dd927dc5dda383395e583dbd638" + integrity sha512-nlOwcXcsNAptQl4vlL4MA78qNJKO0Qlds5GuBjCoePgkebTXLSf8Qt1oyZ3YBshYupKXG9VRGEsk1zr23d+bzQ== + +"@docsearch/react@^3.9.0 || ^4.3.2": + version "4.6.3" + resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-4.6.3.tgz#80df785f9c5e484c960b914a22ea2a3e4c7210ad" + integrity sha512-Bg2wdDsoQVlNCcEKuEJAU04tvHCqgx8rIu+uIoM4pRtcx3TBKJuXutJik3LTA8LRc9YEyHkrYUrmcC0D7BYf+g== + dependencies: + "@algolia/autocomplete-core" "1.19.2" + "@docsearch/core" "4.6.3" + "@docsearch/css" "4.6.3" + +"@docusaurus/babel@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.10.1.tgz#2f714f682117658ba43d308e9b35b6a73a105227" + integrity sha512-DZzFO1K3v/GoEt1fx1DiYHF4en+PuhtQf1AkQJa5zu3CoeKSpr5cpQRUlz3jr0m44wyzmSXu9bVpfir+N4+8bg== + dependencies: + "@babel/core" "^7.25.9" + "@babel/generator" "^7.25.9" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-transform-runtime" "^7.25.9" + "@babel/preset-env" "^7.25.9" + "@babel/preset-react" "^7.25.9" + "@babel/preset-typescript" "^7.25.9" + "@babel/runtime" "^7.25.9" + "@babel/traverse" "^7.25.9" + "@docusaurus/logger" "3.10.1" + "@docusaurus/utils" "3.10.1" + babel-plugin-dynamic-import-node "^2.3.3" + fs-extra "^11.1.1" + tslib "^2.6.0" + +"@docusaurus/bundler@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.10.1.tgz#82fa5079f3787a67502e25f82d37d05ec5de0cc3" + integrity sha512-HIqQPvbqnnQRe4NsBd1774KRarjXqS6wHsWELtyuSs1gCfvixJO2jUGH/OEBtr1Gvzpw+ze5CjGMvSJ8UE1KUw== + dependencies: + "@babel/core" "^7.25.9" + "@docusaurus/babel" "3.10.1" + "@docusaurus/cssnano-preset" "3.10.1" + "@docusaurus/logger" "3.10.1" + "@docusaurus/types" "3.10.1" + "@docusaurus/utils" "3.10.1" + babel-loader "^9.2.1" + clean-css "^5.3.3" + copy-webpack-plugin "^11.0.0" + css-loader "^6.11.0" + css-minimizer-webpack-plugin "^5.0.1" + cssnano "^6.1.2" + file-loader "^6.2.0" + html-minifier-terser "^7.2.0" + mini-css-extract-plugin "^2.9.2" + null-loader "^4.0.1" + postcss "^8.5.4" + postcss-loader "^7.3.4" + postcss-preset-env "^10.2.1" + terser-webpack-plugin "^5.3.9" + tslib "^2.6.0" + url-loader "^4.1.1" + webpack "^5.95.0" + webpackbar "^7.0.0" + +"@docusaurus/core@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.1.0.tgz#b66e7eaf867c1f44738d725d217a1c0e879629d7" + integrity sha512-GWudMGYA9v26ssbAWJNfgeDZk+lrudUTclLPRsmxiknEBk7UMp7Rglonhqbsf3IKHOyHkMU4Fr5jFyg5SBx9jQ== + dependencies: + "@babel/core" "^7.23.3" + "@babel/generator" "^7.23.3" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-transform-runtime" "^7.22.9" + "@babel/preset-env" "^7.22.9" + "@babel/preset-react" "^7.22.5" + "@babel/preset-typescript" "^7.22.5" + "@babel/runtime" "^7.22.6" + "@babel/runtime-corejs3" "^7.22.6" + "@babel/traverse" "^7.22.8" + "@docusaurus/cssnano-preset" "3.1.0" + "@docusaurus/logger" "3.1.0" + "@docusaurus/mdx-loader" "3.1.0" "@docusaurus/react-loadable" "5.5.2" "@docusaurus/utils" "3.1.0" "@docusaurus/utils-common" "3.1.0" @@ -1296,6 +2653,54 @@ webpack-merge "^5.9.0" webpackbar "^5.0.2" +"@docusaurus/core@3.10.1", "@docusaurus/core@^3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.10.1.tgz#3f8bdb97451b4df14f2a3b39ab0186366fbf8fbe" + integrity sha512-3pf2fXXw0eVk8WnC3T4LIigRDupcpvngpKo9Vy7mYyBhuddc0klDUuZAIfzMoK6z05pdlk6EFC/vBSX43+1O5w== + dependencies: + "@docusaurus/babel" "3.10.1" + "@docusaurus/bundler" "3.10.1" + "@docusaurus/logger" "3.10.1" + "@docusaurus/mdx-loader" "3.10.1" + "@docusaurus/utils" "3.10.1" + "@docusaurus/utils-common" "3.10.1" + "@docusaurus/utils-validation" "3.10.1" + boxen "^6.2.1" + chalk "^4.1.2" + chokidar "^3.5.3" + cli-table3 "^0.6.3" + combine-promises "^1.1.0" + commander "^5.1.0" + core-js "^3.31.1" + detect-port "^1.5.1" + escape-html "^1.0.3" + eta "^2.2.0" + eval "^0.1.8" + execa "^5.1.1" + fs-extra "^11.1.1" + html-tags "^3.3.1" + html-webpack-plugin "^5.6.0" + leven "^3.1.0" + lodash "^4.17.21" + open "^8.4.0" + p-map "^4.0.0" + prompts "^2.4.2" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" + react-loadable "npm:@docusaurus/react-loadable@6.0.0" + react-loadable-ssr-addon-v5-slorber "^1.0.3" + react-router "^5.3.4" + react-router-config "^5.1.1" + react-router-dom "^5.3.4" + semver "^7.5.4" + serve-handler "^6.1.7" + tinypool "^1.0.2" + tslib "^2.6.0" + update-notifier "^6.0.2" + webpack "^5.95.0" + webpack-bundle-analyzer "^4.10.2" + webpack-dev-server "^5.2.2" + webpack-merge "^6.0.1" + "@docusaurus/cssnano-preset@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.1.0.tgz#b3fe7134cc4d0c1950eeb1c940089a190591ad4e" @@ -1306,6 +2711,16 @@ postcss-sort-media-queries "^4.4.1" tslib "^2.6.0" +"@docusaurus/cssnano-preset@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.10.1.tgz#4b6bafeca8bb9423364d2fd6683c28e2f85a4665" + integrity sha512-eNfHGcTKCSq6xmcavAkX3RRclHaE2xRCMParlDXLdXVP01/a2e/jKXMj/0ULnLFQSNwwuI62L0Ge8J+nZsR7UQ== + dependencies: + cssnano-preset-advanced "^6.1.2" + postcss "^8.5.4" + postcss-sort-media-queries "^5.2.0" + tslib "^2.6.0" + "@docusaurus/logger@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.1.0.tgz#eef6475c2d59a3ae7e138ac1f60007d6fafd76b0" @@ -1314,6 +2729,14 @@ chalk "^4.1.2" tslib "^2.6.0" +"@docusaurus/logger@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.10.1.tgz#34c964e32e18f120e30f80171a38cfefe72cfb4b" + integrity sha512-oPjNFnfJsRCkePVjkGrxWGq4MvJKRQT0r9jOP0eRBTZ7Wr9FAbzdP/Gjs0I2Ss6YRkPoEgygKG112OkE6skvJw== + dependencies: + chalk "^4.1.2" + tslib "^2.6.0" + "@docusaurus/mdx-loader@3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.1.0.tgz#61d562ff442f62ef04cc31d3f0d5865a8dd390e4" @@ -1346,6 +2769,36 @@ vfile "^6.0.1" webpack "^5.88.1" +"@docusaurus/mdx-loader@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.10.1.tgz#050ae9bc614158a4ec07a628aa75fa9ae90d7e82" + integrity sha512-GRmeb/wQ+iXRrFwcHBfgQhrJxGElgCsoTWZYDhccjsZVne1p8MK/EpQVIloXttz76TCe78kKD5AEG9n1xc1oxQ== + dependencies: + "@docusaurus/logger" "3.10.1" + "@docusaurus/utils" "3.10.1" + "@docusaurus/utils-validation" "3.10.1" + "@mdx-js/mdx" "^3.0.0" + "@slorber/remark-comment" "^1.0.0" + escape-html "^1.0.3" + estree-util-value-to-estree "^3.0.1" + file-loader "^6.2.0" + fs-extra "^11.1.1" + image-size "^2.0.2" + mdast-util-mdx "^3.0.0" + mdast-util-to-string "^4.0.0" + rehype-raw "^7.0.0" + remark-directive "^3.0.0" + remark-emoji "^4.0.0" + remark-frontmatter "^5.0.0" + remark-gfm "^4.0.0" + stringify-object "^3.3.0" + tslib "^2.6.0" + unified "^11.0.3" + unist-util-visit "^5.0.0" + url-loader "^4.1.1" + vfile "^6.0.1" + webpack "^5.88.1" + "@docusaurus/module-type-aliases@3.1.0", "@docusaurus/module-type-aliases@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.1.0.tgz#f3451702f143557bfde0502287713a08086a0415" @@ -1360,30 +2813,69 @@ react-helmet-async "*" react-loadable "npm:@docusaurus/react-loadable@5.5.2" -"@docusaurus/plugin-content-blog@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.1.0.tgz#d2102e9286486e526dbc0dfc741e53dc5cee0ff0" - integrity sha512-iMa6WBaaEdYuxckvJtLcq/HQdlA4oEbCXf/OFfsYJCCULcDX7GDZpKxLF3X1fLsax3sSm5bmsU+CA0WD+R1g3A== +"@docusaurus/module-type-aliases@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.10.1.tgz#22d39177c296786eb6e0d940699cd590cc93ca77" + integrity sha512-YoOZKUdGlp8xSYhuAkGdSo5Ydkbq4V4eK3sD8v0a2hloxCWdQbNBhkc+Ko9QyjpESc0BYcIGM5iHVAy5hdFV6w== dependencies: - "@docusaurus/core" "3.1.0" - "@docusaurus/logger" "3.1.0" - "@docusaurus/mdx-loader" "3.1.0" - "@docusaurus/types" "3.1.0" - "@docusaurus/utils" "3.1.0" - "@docusaurus/utils-common" "3.1.0" - "@docusaurus/utils-validation" "3.1.0" - cheerio "^1.0.0-rc.12" + "@docusaurus/types" "3.10.1" + "@types/history" "^4.7.11" + "@types/react" "*" + "@types/react-router-config" "*" + "@types/react-router-dom" "*" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" + react-loadable "npm:@docusaurus/react-loadable@6.0.0" + +"@docusaurus/plugin-content-blog@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.10.1.tgz#0bd8de700ccbd8e95d920df2613304ef59abe72b" + integrity sha512-mmkgE6Q2+K74tnkou7tXlpDLvoCU/qkSa2GSQ3XUiHWvcebCoDQzS670RR3tO8PmaWlIyWWISYWzZLuMfxunRA== + dependencies: + "@docusaurus/core" "3.10.1" + "@docusaurus/logger" "3.10.1" + "@docusaurus/mdx-loader" "3.10.1" + "@docusaurus/theme-common" "3.10.1" + "@docusaurus/types" "3.10.1" + "@docusaurus/utils" "3.10.1" + "@docusaurus/utils-common" "3.10.1" + "@docusaurus/utils-validation" "3.10.1" + cheerio "1.0.0-rc.12" + combine-promises "^1.1.0" feed "^4.2.2" fs-extra "^11.1.1" lodash "^4.17.21" - reading-time "^1.5.0" + schema-dts "^1.1.2" srcset "^4.0.0" tslib "^2.6.0" unist-util-visit "^5.0.0" utility-types "^3.10.0" webpack "^5.88.1" -"@docusaurus/plugin-content-docs@3.1.0", "@docusaurus/plugin-content-docs@^2 || ^3": +"@docusaurus/plugin-content-docs@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.1.tgz#261e0e982e4a937c05b462e3c5729374f433b752" + integrity sha512-2jRVrtzjf8LClGTHQlwlwuD3wQXRx3WEoF7XUarJ8Ou+0onV+SLtejsyfY9JLpfUh9hPhXM4pbBGkyAY4Bi3HQ== + dependencies: + "@docusaurus/core" "3.10.1" + "@docusaurus/logger" "3.10.1" + "@docusaurus/mdx-loader" "3.10.1" + "@docusaurus/module-type-aliases" "3.10.1" + "@docusaurus/theme-common" "3.10.1" + "@docusaurus/types" "3.10.1" + "@docusaurus/utils" "3.10.1" + "@docusaurus/utils-common" "3.10.1" + "@docusaurus/utils-validation" "3.10.1" + "@types/react-router-config" "^5.0.7" + combine-promises "^1.1.0" + fs-extra "^11.1.1" + js-yaml "^4.1.0" + lodash "^4.17.21" + schema-dts "^1.1.2" + tslib "^2.6.0" + utility-types "^3.10.0" + webpack "^5.88.1" + +"@docusaurus/plugin-content-docs@^2 || ^3": version "3.1.0" resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.1.0.tgz#55d7bdb8e14f854ea6c6e256f1b51b8c17963c19" integrity sha512-el5GxhT8BLrsWD0qGa8Rq+Ttb/Ni6V3DGT2oAPio0qcs/mUAxeyXEAmihkvmLCnAgp6xD27Ce7dISZ5c6BXeqA== @@ -1404,98 +2896,125 @@ utility-types "^3.10.0" webpack "^5.88.1" -"@docusaurus/plugin-content-pages@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.1.0.tgz#cdb73c804ded307e81ceea39874dc0bb540c2dc4" - integrity sha512-9gntYQFpk+93+Xl7gYczJu8I9uWoyRLnRwS0+NUFcs9iZtHKsdqKWPRrONC9elfN3wJ9ORwTbcVzsTiB8jvYlg== +"@docusaurus/plugin-content-pages@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.10.1.tgz#8c6ffc2079ed0262548ecc4df1dea6add6aa9673" + integrity sha512-huJpaRPMl42nsFwuCXvV8bVDj2MazuwRJIUylI/RSlmZeJssVoZXeCjVf1y+1Drtpa9SKcdGn8yoJ76IRJijtw== dependencies: - "@docusaurus/core" "3.1.0" - "@docusaurus/mdx-loader" "3.1.0" - "@docusaurus/types" "3.1.0" - "@docusaurus/utils" "3.1.0" - "@docusaurus/utils-validation" "3.1.0" + "@docusaurus/core" "3.10.1" + "@docusaurus/mdx-loader" "3.10.1" + "@docusaurus/types" "3.10.1" + "@docusaurus/utils" "3.10.1" + "@docusaurus/utils-validation" "3.10.1" fs-extra "^11.1.1" tslib "^2.6.0" webpack "^5.88.1" -"@docusaurus/plugin-debug@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.1.0.tgz#545872bc8f9cd697d9f1d6c695f8a1674bfc149c" - integrity sha512-AbvJwCVRbmQ8w9d8QXbF4Iq/ui0bjPZNYFIhtducGFnm2YQRN1mraK8mCEQb0Aq0T8SqRRvSfC/far4n/s531w== +"@docusaurus/plugin-css-cascade-layers@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.10.1.tgz#440578d95cbe1a6120936fa83df868d2626cd1d8" + integrity sha512-r//fn+MNHkE1wCof8T29VAQezt1enGCpsFxoziBbvLgBM4JfXN2P3rxrBaavHmvLvm7lYkpJeitcDthwnmWCTw== dependencies: - "@docusaurus/core" "3.1.0" - "@docusaurus/types" "3.1.0" - "@docusaurus/utils" "3.1.0" - fs-extra "^11.1.1" - react-json-view-lite "^1.2.0" + "@docusaurus/core" "3.10.1" + "@docusaurus/types" "3.10.1" + "@docusaurus/utils" "3.10.1" + "@docusaurus/utils-validation" "3.10.1" tslib "^2.6.0" -"@docusaurus/plugin-google-analytics@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.1.0.tgz#3035eace3121db16aec7c10852ebb4cd860f4434" - integrity sha512-zvUOMzu9Uhz0ciqnSbtnp/5i1zEYlzarQrOXG90P3Is3efQI43p2YLW/rzSGdLb5MfQo2HvKT6Q5+tioMO045Q== +"@docusaurus/plugin-debug@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.10.1.tgz#b8b7b24d9a7d185fd8a56a030f90145d3bfd8239" + integrity sha512-9KqOpKNfAyqGZykRb9LhIT/vyRF6sm/ykhjj/39JvaJahDS+jZJE0Z1Wfz9q3DUNDTMNN0Q7u/kk4rKKU+IJuA== dependencies: - "@docusaurus/core" "3.1.0" - "@docusaurus/types" "3.1.0" - "@docusaurus/utils-validation" "3.1.0" + "@docusaurus/core" "3.10.1" + "@docusaurus/types" "3.10.1" + "@docusaurus/utils" "3.10.1" + fs-extra "^11.1.1" + react-json-view-lite "^2.3.0" tslib "^2.6.0" -"@docusaurus/plugin-google-gtag@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.1.0.tgz#2f4040da81d36bfc6324abc1a12b258e6c7f202a" - integrity sha512-0txshvaY8qIBdkk2UATdVcfiCLGq3KAUfuRQD2cRNgO39iIf4/ihQxH9NXcRTwKs4Q5d9yYHoix3xT6pFuEYOg== +"@docusaurus/plugin-google-analytics@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.10.1.tgz#ac15afc77386e0352edb8a1698d993aa5de36ffc" + integrity sha512-8o0P1KtmgdYQHH+oInitPpRWI0Of5XednAX4+DMhQNSmGSRNrsEEHg1ebv35m9AgRClfAytCJ5jA9KvcASTyuA== dependencies: - "@docusaurus/core" "3.1.0" - "@docusaurus/types" "3.1.0" - "@docusaurus/utils-validation" "3.1.0" - "@types/gtag.js" "^0.0.12" + "@docusaurus/core" "3.10.1" + "@docusaurus/types" "3.10.1" + "@docusaurus/utils-validation" "3.10.1" tslib "^2.6.0" -"@docusaurus/plugin-google-tag-manager@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.1.0.tgz#4c026e9f65468a332326770f95ccd9c6e12d564b" - integrity sha512-zOWPEi8kMyyPtwG0vhyXrdbLs8fIZmY5vlbi9lUU+v8VsroO5iHmfR2V3SMsrsfOanw5oV/ciWqbxezY00qEZg== +"@docusaurus/plugin-google-gtag@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.10.1.tgz#0482b83b9bc411aa99a432be2b39d2e53a00e2e0" + integrity sha512-pu3xIUo5o/zCMLfUY9BO5KOwSH0zIsAGyFRPvXHayFSA5XIhCU/SFuB0g0ZNjFn9niZLCaNvoeAuOGFJZq0fdw== dependencies: - "@docusaurus/core" "3.1.0" - "@docusaurus/types" "3.1.0" - "@docusaurus/utils-validation" "3.1.0" + "@docusaurus/core" "3.10.1" + "@docusaurus/types" "3.10.1" + "@docusaurus/utils-validation" "3.10.1" + "@types/gtag.js" "^0.0.20" tslib "^2.6.0" -"@docusaurus/plugin-sitemap@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.1.0.tgz#9d9dbb4d87e6dc46ae9321badf6ac7cd9aa96b23" - integrity sha512-TkR5vGBpUooEB9SoW42thahqqwKzfHrQQhkB+JrEGERsl4bKODSuJNle4aA4h6LSkg4IyfXOW8XOI0NIPWb9Cg== +"@docusaurus/plugin-google-tag-manager@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.10.1.tgz#eaf5765d6f82b4fb661d92a793d1883f9d1ec106" + integrity sha512-f6fyGHiCm7kJHBtAisGQS5oNBnpnMTYQZxDXeVrnw/3zWU+LMA22pr6UHGYkBKDbN+qPC5QHG3NuOfzQLq3+Lw== dependencies: - "@docusaurus/core" "3.1.0" - "@docusaurus/logger" "3.1.0" - "@docusaurus/types" "3.1.0" - "@docusaurus/utils" "3.1.0" - "@docusaurus/utils-common" "3.1.0" - "@docusaurus/utils-validation" "3.1.0" + "@docusaurus/core" "3.10.1" + "@docusaurus/types" "3.10.1" + "@docusaurus/utils-validation" "3.10.1" + tslib "^2.6.0" + +"@docusaurus/plugin-sitemap@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.10.1.tgz#66a6974bb2fd1b9d8f5cb0f3c5ecd2201c118565" + integrity sha512-C26MbmmqgdjkDq1htaZ3aD7LzEDKFWXfpyQpt0EOUThuq5nV77zDaedV20yHcVo9p+3ey9aZ4pbHA0D3QcZTzg== + dependencies: + "@docusaurus/core" "3.10.1" + "@docusaurus/logger" "3.10.1" + "@docusaurus/types" "3.10.1" + "@docusaurus/utils" "3.10.1" + "@docusaurus/utils-common" "3.10.1" + "@docusaurus/utils-validation" "3.10.1" fs-extra "^11.1.1" sitemap "^7.1.1" tslib "^2.6.0" -"@docusaurus/preset-classic@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.1.0.tgz#ca67d5e416c211b4c23f0fb01f0e3e36b759dfa0" - integrity sha512-xGLQRFmmT9IinAGUDVRYZ54Ys28USNbA3OTXQXnSJLPr1rCY7CYnHI4XoOnKWrNnDiAI4ruMzunXWyaElUYCKQ== - dependencies: - "@docusaurus/core" "3.1.0" - "@docusaurus/plugin-content-blog" "3.1.0" - "@docusaurus/plugin-content-docs" "3.1.0" - "@docusaurus/plugin-content-pages" "3.1.0" - "@docusaurus/plugin-debug" "3.1.0" - "@docusaurus/plugin-google-analytics" "3.1.0" - "@docusaurus/plugin-google-gtag" "3.1.0" - "@docusaurus/plugin-google-tag-manager" "3.1.0" - "@docusaurus/plugin-sitemap" "3.1.0" - "@docusaurus/theme-classic" "3.1.0" - "@docusaurus/theme-common" "3.1.0" - "@docusaurus/theme-search-algolia" "3.1.0" - "@docusaurus/types" "3.1.0" +"@docusaurus/plugin-svgr@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-svgr/-/plugin-svgr-3.10.1.tgz#c217c24d6d23fd2bc6f54d44c040635b49d6b36e" + integrity sha512-6SFxsmjWFkVLDmBUvFK6i72QjUwqyQFe4Ovz+SUJophJjOyVG3ZZG5IQpBC/kX/Gfv1yWeU9nWauH6F6Q7QX/Q== + dependencies: + "@docusaurus/core" "3.10.1" + "@docusaurus/types" "3.10.1" + "@docusaurus/utils" "3.10.1" + "@docusaurus/utils-validation" "3.10.1" + "@svgr/core" "8.1.0" + "@svgr/webpack" "^8.1.0" + tslib "^2.6.0" + webpack "^5.88.1" -"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2": +"@docusaurus/preset-classic@^3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.10.1.tgz#faf330d96aedc9083a59bec09d966ae4dfc8b2fb" + integrity sha512-YO/FL8v1zmbxoTso6mjMz/RDjhaTJxb1UpFFTDdY5847LLDCeyYiYlrhyTbgN1RIN3xnkLKZ9Lj1x8hUzI4JOg== + dependencies: + "@docusaurus/core" "3.10.1" + "@docusaurus/plugin-content-blog" "3.10.1" + "@docusaurus/plugin-content-docs" "3.10.1" + "@docusaurus/plugin-content-pages" "3.10.1" + "@docusaurus/plugin-css-cascade-layers" "3.10.1" + "@docusaurus/plugin-debug" "3.10.1" + "@docusaurus/plugin-google-analytics" "3.10.1" + "@docusaurus/plugin-google-gtag" "3.10.1" + "@docusaurus/plugin-google-tag-manager" "3.10.1" + "@docusaurus/plugin-sitemap" "3.10.1" + "@docusaurus/plugin-svgr" "3.10.1" + "@docusaurus/theme-classic" "3.10.1" + "@docusaurus/theme-common" "3.10.1" + "@docusaurus/theme-search-algolia" "3.10.1" + "@docusaurus/types" "3.10.1" + +"@docusaurus/react-loadable@5.5.2": version "5.5.2" resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce" integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ== @@ -1503,30 +3022,31 @@ "@types/react" "*" prop-types "^15.6.2" -"@docusaurus/theme-classic@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.1.0.tgz#6ee68bf4d4db53c8d9b18d4866512abadb00a802" - integrity sha512-/+jMl2Z9O8QQxves5AtHdt91gWsEZFgOV3La/6eyKEd7QLqQUtM5fxEJ40rq9NKYjqCd1HzZ9egIMeJoWwillw== - dependencies: - "@docusaurus/core" "3.1.0" - "@docusaurus/mdx-loader" "3.1.0" - "@docusaurus/module-type-aliases" "3.1.0" - "@docusaurus/plugin-content-blog" "3.1.0" - "@docusaurus/plugin-content-docs" "3.1.0" - "@docusaurus/plugin-content-pages" "3.1.0" - "@docusaurus/theme-common" "3.1.0" - "@docusaurus/theme-translations" "3.1.0" - "@docusaurus/types" "3.1.0" - "@docusaurus/utils" "3.1.0" - "@docusaurus/utils-common" "3.1.0" - "@docusaurus/utils-validation" "3.1.0" +"@docusaurus/theme-classic@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.10.1.tgz#deed8cf73cc0f56113e53775cbb3b168c3c61566" + integrity sha512-VU1RK0qb2pab0si4r7HFK37cYco8VzqLj3u1PspVipSr/z/GPVKHO4/HXbnePqHoWDk8urjyGSeatH0NIMBM1A== + dependencies: + "@docusaurus/core" "3.10.1" + "@docusaurus/logger" "3.10.1" + "@docusaurus/mdx-loader" "3.10.1" + "@docusaurus/module-type-aliases" "3.10.1" + "@docusaurus/plugin-content-blog" "3.10.1" + "@docusaurus/plugin-content-docs" "3.10.1" + "@docusaurus/plugin-content-pages" "3.10.1" + "@docusaurus/theme-common" "3.10.1" + "@docusaurus/theme-translations" "3.10.1" + "@docusaurus/types" "3.10.1" + "@docusaurus/utils" "3.10.1" + "@docusaurus/utils-common" "3.10.1" + "@docusaurus/utils-validation" "3.10.1" "@mdx-js/react" "^3.0.0" clsx "^2.0.0" copy-text-to-clipboard "^3.2.0" - infima "0.2.0-alpha.43" + infima "0.2.0-alpha.45" lodash "^4.17.21" nprogress "^0.2.0" - postcss "^8.4.26" + postcss "^8.5.4" prism-react-renderer "^2.3.0" prismjs "^1.29.0" react-router-dom "^5.3.4" @@ -1534,18 +3054,15 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-common@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.1.0.tgz#d176af1d0fc8ea27fea0afb298157f9a75256336" - integrity sha512-YGwEFALLIbF5ocW/Fy6Ae7tFWUOugEN3iwxTx8UkLAcLqYUboDSadesYtVBmRCEB4FVA2qoP7YaW3lu3apUPPw== +"@docusaurus/theme-common@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.10.1.tgz#cbfec82b1b107be5c229811ed9caae14a501361c" + integrity sha512-0YtmIeoNo1fIw65LO8+/1dPgmDV86UmhMkow37gzjytuiCSQm9xob6PJy0L4kuQEMTLfUOGvkXvZr7GPrHquMA== dependencies: - "@docusaurus/mdx-loader" "3.1.0" - "@docusaurus/module-type-aliases" "3.1.0" - "@docusaurus/plugin-content-blog" "3.1.0" - "@docusaurus/plugin-content-docs" "3.1.0" - "@docusaurus/plugin-content-pages" "3.1.0" - "@docusaurus/utils" "3.1.0" - "@docusaurus/utils-common" "3.1.0" + "@docusaurus/mdx-loader" "3.10.1" + "@docusaurus/module-type-aliases" "3.10.1" + "@docusaurus/utils" "3.10.1" + "@docusaurus/utils-common" "3.10.1" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" @@ -1555,21 +3072,22 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-search-algolia@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.1.0.tgz#3cdb1f0e8d15698a60110856ca5a06f10d3b049d" - integrity sha512-8cJH0ZhPsEDjq3jR3I+wHmWzVY2bXMQJ59v2QxUmsTZxbWA4u+IzccJMIJx4ooFl9J6iYynwYsFuHxyx/KUmfQ== - dependencies: - "@docsearch/react" "^3.5.2" - "@docusaurus/core" "3.1.0" - "@docusaurus/logger" "3.1.0" - "@docusaurus/plugin-content-docs" "3.1.0" - "@docusaurus/theme-common" "3.1.0" - "@docusaurus/theme-translations" "3.1.0" - "@docusaurus/utils" "3.1.0" - "@docusaurus/utils-validation" "3.1.0" - algoliasearch "^4.18.0" - algoliasearch-helper "^3.13.3" +"@docusaurus/theme-search-algolia@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.10.1.tgz#6f422058711629ce8d7c2f17e1e54efa075c626e" + integrity sha512-OTaARARVZj2GvkJQjB+1jOIxntRaXea+G+fMsNqrZBAU1O1vJKDW22R7kECOHW27oJCLFN9HKaZeRrfAUyviug== + dependencies: + "@algolia/autocomplete-core" "^1.19.2" + "@docsearch/react" "^3.9.0 || ^4.3.2" + "@docusaurus/core" "3.10.1" + "@docusaurus/logger" "3.10.1" + "@docusaurus/plugin-content-docs" "3.10.1" + "@docusaurus/theme-common" "3.10.1" + "@docusaurus/theme-translations" "3.10.1" + "@docusaurus/utils" "3.10.1" + "@docusaurus/utils-validation" "3.10.1" + algoliasearch "^5.37.0" + algoliasearch-helper "^3.26.0" clsx "^2.0.0" eta "^2.2.0" fs-extra "^11.1.1" @@ -1577,7 +3095,15 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-translations@3.1.0", "@docusaurus/theme-translations@^2 || ^3": +"@docusaurus/theme-translations@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.10.1.tgz#c3119a015652290eea560ca45ac775963d6eb75b" + integrity sha512-cLMyaKivjBVWKMJuWqyFVVgtqe8DPJNPkog0bn8W1MDVAKcPdxRFycBfC1We1RaNp7Rdk513bmtW78RR6OBxBw== + dependencies: + fs-extra "^11.1.1" + tslib "^2.6.0" + +"@docusaurus/theme-translations@^2 || ^3": version "3.1.0" resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.1.0.tgz#1c6bdc19723a87e042b5e89b6cdc8b747fdcbc13" integrity sha512-DApE4AbDI+WBajihxB54L4scWQhVGNZAochlC9fkbciPuFAgdRBD3NREb0rgfbKexDC/rioppu/WJA0u8tS+yA== @@ -1600,6 +3126,22 @@ webpack "^5.88.1" webpack-merge "^5.9.0" +"@docusaurus/types@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.10.1.tgz#d42837938ae43ca2be0ca47e63e00476b5eb94be" + integrity sha512-XYMK8k1szDCFMw2V+Xyen0g7Kee1sP3dtFnl7vkGkZOkeAJ/oPDQPL8iz4HBKOo/cwU8QeV6onVjMqtP+tFzsw== + dependencies: + "@mdx-js/mdx" "^3.0.0" + "@types/history" "^4.7.11" + "@types/mdast" "^4.0.2" + "@types/react" "*" + commander "^5.1.0" + joi "^17.9.2" + react-helmet-async "npm:@slorber/react-helmet-async@1.3.0" + utility-types "^3.10.0" + webpack "^5.95.0" + webpack-merge "^5.9.0" + "@docusaurus/utils-common@3.1.0", "@docusaurus/utils-common@^2 || ^3": version "3.1.0" resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.1.0.tgz#846ce9c12b9ebf1ebf513e65303fb8158dcd2e1b" @@ -1607,6 +3149,14 @@ dependencies: tslib "^2.6.0" +"@docusaurus/utils-common@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.10.1.tgz#6350b4898691e765de750f90eade0e0fa7902d99" + integrity sha512-5mFSgEADtnFxFH7RLw02QA5MpU5JVUCj0MPeIvi/aF4Fi45tQRIuTwXoXDqJ+1VfQJuYJGz3SI63wmGz4HvXzA== + dependencies: + "@docusaurus/types" "3.10.1" + tslib "^2.6.0" + "@docusaurus/utils-validation@3.1.0", "@docusaurus/utils-validation@^2 || ^3": version "3.1.0" resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.1.0.tgz#3e88c42caec29cd3eedbbd17af97f88719613340" @@ -1618,6 +3168,20 @@ js-yaml "^4.1.0" tslib "^2.6.0" +"@docusaurus/utils-validation@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.10.1.tgz#ddbcce997a5506424cdd16abf6845cc51692acae" + integrity sha512-cRv1X69jwaWv47waglllgZVWzeBFLhl53XT/XED/83BerVBTC5FTP8WTcVl8Z6sZOegDSwitu/wpCSPCDOT6lg== + dependencies: + "@docusaurus/logger" "3.10.1" + "@docusaurus/utils" "3.10.1" + "@docusaurus/utils-common" "3.10.1" + fs-extra "^11.2.0" + joi "^17.9.2" + js-yaml "^4.1.0" + lodash "^4.17.21" + tslib "^2.6.0" + "@docusaurus/utils@3.1.0", "@docusaurus/utils@^2 || ^3": version "3.1.0" resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.1.0.tgz#6ef821bc4c40a91586835a385110b5c0082c590c" @@ -1641,6 +3205,33 @@ url-loader "^4.1.1" webpack "^5.88.1" +"@docusaurus/utils@3.10.1": + version "3.10.1" + resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.10.1.tgz#535968caa2c9bff69f997a081b98b95b3c5d3785" + integrity sha512-3ojeJry9xBYdJO6qoyyzqeJFSJBVx2mXhyDzSdjwL2+URFQMf+h25gG38iswGImicK0ELjTd1EL2xzk8hf3QPw== + dependencies: + "@docusaurus/logger" "3.10.1" + "@docusaurus/types" "3.10.1" + "@docusaurus/utils-common" "3.10.1" + escape-string-regexp "^4.0.0" + execa "^5.1.1" + file-loader "^6.2.0" + fs-extra "^11.1.1" + github-slugger "^1.5.0" + globby "^11.1.0" + gray-matter "^4.0.3" + jiti "^1.20.0" + js-yaml "^4.1.0" + lodash "^4.17.21" + micromatch "^4.0.5" + p-queue "^6.6.2" + prompts "^2.4.2" + resolve-pathname "^3.0.0" + tslib "^2.6.0" + url-loader "^4.1.1" + utility-types "^3.10.0" + webpack "^5.88.1" + "@easyops-cn/autocomplete.js@^0.38.1": version "0.38.1" resolved "https://registry.yarnpkg.com/@easyops-cn/autocomplete.js/-/autocomplete.js-0.38.1.tgz#46dff5795a9a032fa9b9250fdf63ca6c61c07629" @@ -1725,6 +3316,22 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + "@jridgewell/resolve-uri@^3.1.0": version "3.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" @@ -1748,6 +3355,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.9": version "0.3.22" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz#72a621e5de59f5f1ef792d0793a82ee20f645e4c" @@ -1756,6 +3368,174 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@jsonjoy.com/base64@17.67.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-17.67.0.tgz#7eeda3cb41138d77a90408fd2e42b2aba10576d7" + integrity sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw== + +"@jsonjoy.com/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz#cf8ea9dcb849b81c95f14fc0aaa151c6b54d2578" + integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== + +"@jsonjoy.com/buffers@17.67.0", "@jsonjoy.com/buffers@^17.65.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/buffers/-/buffers-17.67.0.tgz#5c58dbcdeea8824ce296bd1cfce006c2eb167b3d" + integrity sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw== + +"@jsonjoy.com/buffers@^1.0.0", "@jsonjoy.com/buffers@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz#8d99c7f67eaf724d3428dfd9826c6455266a5c83" + integrity sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA== + +"@jsonjoy.com/codegen@17.67.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/codegen/-/codegen-17.67.0.tgz#3635fd8769d77e19b75dc5574bc9756019b2e591" + integrity sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q== + +"@jsonjoy.com/codegen@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz#5c23f796c47675f166d23b948cdb889184b93207" + integrity sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g== + +"@jsonjoy.com/fs-core@4.58.0": + version "4.58.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-core/-/fs-core-4.58.0.tgz#10572cecb73d823e51d85063e53e42b5bea7e211" + integrity sha512-K82t5e9w3NYQeIcw129f0SCH/Rn8m8GKPJBYge4W/sA4hhE5h7I8YQhs2HRpUQyUpi+qk7I9tbp1/b87eCM/PA== + dependencies: + "@jsonjoy.com/fs-node-builtins" "4.58.0" + "@jsonjoy.com/fs-node-utils" "4.58.0" + thingies "^2.5.0" + +"@jsonjoy.com/fs-fsa@4.58.0": + version "4.58.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-fsa/-/fs-fsa-4.58.0.tgz#fd7272e5e72893a9c8a97e2ac0fe0e049dd02cc1" + integrity sha512-1/FdsxQao9pBo2OJLEfKrujHdVyGxXvDTT7EtNh9fvp3MK42nB+xHh1EJr2L1c0a05eCMyFCYZPID3KjX9EAng== + dependencies: + "@jsonjoy.com/fs-core" "4.58.0" + "@jsonjoy.com/fs-node-builtins" "4.58.0" + "@jsonjoy.com/fs-node-utils" "4.58.0" + thingies "^2.5.0" + +"@jsonjoy.com/fs-node-builtins@4.58.0": + version "4.58.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.58.0.tgz#25c1fda79c51a6f9dc4cbe59efd9a2037f67e059" + integrity sha512-M1sAhjR1de4CL7ivQAFYqJRDVE3krwVOmfcAgG5YQJiDWxYfcGDMLUNcx0p/+Rcs31/Inh1m9aUDk/0jfR1T5w== + +"@jsonjoy.com/fs-node-to-fsa@4.58.0": + version "4.58.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.58.0.tgz#be011d218acb977bf8d949b4108b75f6f692c7f0" + integrity sha512-erivc39YHoqWY0U8q5IlEcn238AUkIBByMvM0w7OO/FU9hEH3P7oNkPpYWIMpEMncbePk672GiC8ZSwYc2m7fg== + dependencies: + "@jsonjoy.com/fs-fsa" "4.58.0" + "@jsonjoy.com/fs-node-builtins" "4.58.0" + "@jsonjoy.com/fs-node-utils" "4.58.0" + +"@jsonjoy.com/fs-node-utils@4.58.0": + version "4.58.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.58.0.tgz#10d5d50878f371d04563b66781e5fd7de71aead5" + integrity sha512-IrsAFMFBFQXjpS/u3jOY4DMIbRDg94KxzadddDlJv9xodxZ6/Y3ji91uMwrRqklyu+KoquO/Vxihf3/QrQiuOA== + dependencies: + "@jsonjoy.com/fs-node-builtins" "4.58.0" + +"@jsonjoy.com/fs-node@4.58.0": + version "4.58.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-node/-/fs-node-4.58.0.tgz#7c404c014102844dabb03ed2f31528d55c8a53aa" + integrity sha512-bOqoLND5b5xyM+1zro63kClBnB05HTIX0RzOSHy4kAjfnec+CTdV742H/nsDmbkDaaDLHMmYqxAw09NQ8yVIrQ== + dependencies: + "@jsonjoy.com/fs-core" "4.58.0" + "@jsonjoy.com/fs-node-builtins" "4.58.0" + "@jsonjoy.com/fs-node-utils" "4.58.0" + "@jsonjoy.com/fs-print" "4.58.0" + "@jsonjoy.com/fs-snapshot" "4.58.0" + glob-to-regex.js "^1.0.0" + thingies "^2.5.0" + +"@jsonjoy.com/fs-print@4.58.0": + version "4.58.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-print/-/fs-print-4.58.0.tgz#5800d9523f997d96593d5b373d8d63b555707edd" + integrity sha512-jbAYbeuzPXIzvWOuuFMoQgrWaQU5hBqevloebLaiAimoBYLjrjiLPdOaaLHepWcZzOq/7Us8x2F22YlsTPbwmw== + dependencies: + "@jsonjoy.com/fs-node-utils" "4.58.0" + tree-dump "^1.1.0" + +"@jsonjoy.com/fs-snapshot@4.58.0": + version "4.58.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.58.0.tgz#829709f7d4a8d275fc3a071b711207a7a6bdf617" + integrity sha512-DkozLuMgzfpWZO8o8tSlFJIpUQauG7/sgjH0JrqsbLkmjdoInn/o1W7P9FZQSZlR9lOjDgSXs2MfE/sAUi2JRQ== + dependencies: + "@jsonjoy.com/buffers" "^17.65.0" + "@jsonjoy.com/fs-node-utils" "4.58.0" + "@jsonjoy.com/json-pack" "^17.65.0" + "@jsonjoy.com/util" "^17.65.0" + +"@jsonjoy.com/json-pack@^1.11.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz#93f8dd57fe3a3a92132b33d1eb182dcd9e7629fa" + integrity sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg== + dependencies: + "@jsonjoy.com/base64" "^1.1.2" + "@jsonjoy.com/buffers" "^1.2.0" + "@jsonjoy.com/codegen" "^1.0.0" + "@jsonjoy.com/json-pointer" "^1.0.2" + "@jsonjoy.com/util" "^1.9.0" + hyperdyperid "^1.2.0" + thingies "^2.5.0" + tree-dump "^1.1.0" + +"@jsonjoy.com/json-pack@^17.65.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pack/-/json-pack-17.67.0.tgz#8dd8ff65dd999c5d4d26df46c63915c7bdec093a" + integrity sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w== + dependencies: + "@jsonjoy.com/base64" "17.67.0" + "@jsonjoy.com/buffers" "17.67.0" + "@jsonjoy.com/codegen" "17.67.0" + "@jsonjoy.com/json-pointer" "17.67.0" + "@jsonjoy.com/util" "17.67.0" + hyperdyperid "^1.2.0" + thingies "^2.5.0" + tree-dump "^1.1.0" + +"@jsonjoy.com/json-pointer@17.67.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pointer/-/json-pointer-17.67.0.tgz#74439573dc046e0c9a3a552fb94b391bc75313b8" + integrity sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA== + dependencies: + "@jsonjoy.com/util" "17.67.0" + +"@jsonjoy.com/json-pointer@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz#049cb530ac24e84cba08590c5e36b431c4843408" + integrity sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg== + dependencies: + "@jsonjoy.com/codegen" "^1.0.0" + "@jsonjoy.com/util" "^1.9.0" + +"@jsonjoy.com/util@17.67.0", "@jsonjoy.com/util@^17.65.0": + version "17.67.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-17.67.0.tgz#7c4288fc3808233e55c7610101e7bb4590cddd3f" + integrity sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew== + dependencies: + "@jsonjoy.com/buffers" "17.67.0" + "@jsonjoy.com/codegen" "17.67.0" + +"@jsonjoy.com/util@^1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@jsonjoy.com/util/-/util-1.9.0.tgz#7ee95586aed0a766b746cd8d8363e336c3c47c46" + integrity sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ== + dependencies: + "@jsonjoy.com/buffers" "^1.0.0" + "@jsonjoy.com/codegen" "^1.0.0" + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -1806,6 +3586,11 @@ "@emnapi/runtime" "^0.45.0" "@tybys/wasm-util" "^0.8.1" +"@noble/hashes@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" + integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== + "@node-rs/jieba-android-arm-eabi@1.9.2": version "1.9.2" resolved "https://registry.yarnpkg.com/@node-rs/jieba-android-arm-eabi/-/jieba-android-arm-eabi-1.9.2.tgz#ff52e9fa89929fe3ac3ed2c208328ab2689d634b" @@ -1916,8 +3701,138 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@peculiar/asn1-cms@^2.6.0", "@peculiar/asn1-cms@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-cms/-/asn1-cms-2.8.0.tgz#b48a8389319228f929e9acd8cee8da6c858738de" + integrity sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + "@peculiar/asn1-x509-attr" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-csr@^2.6.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-csr/-/asn1-csr-2.8.0.tgz#c9bb5dec2eaff824a705e82a4a58d45e6d2c35d0" + integrity sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-ecc@^2.6.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-ecc/-/asn1-ecc-2.8.0.tgz#d51ab2b07eca98e0cf492d051e98bbd0a071305a" + integrity sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-pfx@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pfx/-/asn1-pfx-2.8.0.tgz#8e189b6455e2bf9e5f921bb150ea86d7e7d1875d" + integrity sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg== + dependencies: + "@peculiar/asn1-cms" "^2.8.0" + "@peculiar/asn1-pkcs8" "^2.8.0" + "@peculiar/asn1-rsa" "^2.8.0" + "@peculiar/asn1-schema" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-pkcs8@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.8.0.tgz#a46cf8857b9b063896afa41d2b8b2aa6a07a70a2" + integrity sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-pkcs9@^2.6.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.8.0.tgz#6d62697af0bbd4f30fdf0d23b4018f3f09620de3" + integrity sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ== + dependencies: + "@peculiar/asn1-cms" "^2.8.0" + "@peculiar/asn1-pfx" "^2.8.0" + "@peculiar/asn1-pkcs8" "^2.8.0" + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + "@peculiar/asn1-x509-attr" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-rsa@^2.6.0", "@peculiar/asn1-rsa@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-rsa/-/asn1-rsa-2.8.0.tgz#9d98d0fc42fec50119d2881b8a9925d36daaea73" + integrity sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-schema@^2.6.0", "@peculiar/asn1-schema@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.8.0.tgz#69699f84259b2161607cabfc34e512a4023dbef9" + integrity sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q== + dependencies: + "@peculiar/utils" "^2.0.2" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-x509-attr@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.8.0.tgz#bd168e3f5e8bc23e56b1a97891f9f2fb7f730204" + integrity sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/asn1-x509" "^2.8.0" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/asn1-x509@^2.6.0", "@peculiar/asn1-x509@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@peculiar/asn1-x509/-/asn1-x509-2.8.0.tgz#9958a9ef35dec8426aabad78ffe8798e318b06e2" + integrity sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg== + dependencies: + "@peculiar/asn1-schema" "^2.8.0" + "@peculiar/utils" "^2.0.2" + asn1js "^3.0.10" + tslib "^2.8.1" + +"@peculiar/utils@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@peculiar/utils/-/utils-2.0.3.tgz#a27ca4c4b73652e110f19a7d16d664f458a5528e" + integrity sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ== + dependencies: + tslib "^2.8.1" + +"@peculiar/x509@^1.14.2": + version "1.14.3" + resolved "https://registry.yarnpkg.com/@peculiar/x509/-/x509-1.14.3.tgz#2c44c2b89474346afec38a0c2803ec4fb8ce959e" + integrity sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA== + dependencies: + "@peculiar/asn1-cms" "^2.6.0" + "@peculiar/asn1-csr" "^2.6.0" + "@peculiar/asn1-ecc" "^2.6.0" + "@peculiar/asn1-pkcs9" "^2.6.0" + "@peculiar/asn1-rsa" "^2.6.0" + "@peculiar/asn1-schema" "^2.6.0" + "@peculiar/asn1-x509" "^2.6.0" + pvtsutils "^1.3.6" + reflect-metadata "^0.2.2" + tslib "^2.8.1" + tsyringe "^4.10.0" "@pnpm/config.env-replace@^1.1.0": version "1.1.0" @@ -1995,46 +3910,90 @@ p-map "^4.0.0" webpack-sources "^3.2.2" +"@svgr/babel-plugin-add-jsx-attribute@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz#4001f5d5dd87fa13303e36ee106e3ff3a7eb8b22" + integrity sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g== + "@svgr/babel-plugin-add-jsx-attribute@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz#74a5d648bd0347bda99d82409d87b8ca80b9a1ba" integrity sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ== -"@svgr/babel-plugin-remove-jsx-attribute@*": +"@svgr/babel-plugin-remove-jsx-attribute@*", "@svgr/babel-plugin-remove-jsx-attribute@8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz#69177f7937233caca3a1afb051906698f2f59186" integrity sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA== -"@svgr/babel-plugin-remove-jsx-empty-expression@*": +"@svgr/babel-plugin-remove-jsx-empty-expression@*", "@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0": version "8.0.0" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz#c2c48104cfd7dcd557f373b70a56e9e3bdae1d44" integrity sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA== +"@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz#8fbb6b2e91fa26ac5d4aa25c6b6e4f20f9c0ae27" + integrity sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ== + "@svgr/babel-plugin-replace-jsx-attribute-value@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz#fb9d22ea26d2bc5e0a44b763d4c46d5d3f596c60" integrity sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg== +"@svgr/babel-plugin-svg-dynamic-title@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz#1d5ba1d281363fc0f2f29a60d6d936f9bbc657b0" + integrity sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og== + "@svgr/babel-plugin-svg-dynamic-title@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz#01b2024a2b53ffaa5efceaa0bf3e1d5a4c520ce4" integrity sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw== +"@svgr/babel-plugin-svg-em-dimensions@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz#35e08df300ea8b1d41cb8f62309c241b0369e501" + integrity sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g== + "@svgr/babel-plugin-svg-em-dimensions@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz#dd3fa9f5b24eb4f93bcf121c3d40ff5facecb217" integrity sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA== +"@svgr/babel-plugin-transform-react-native-svg@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz#90a8b63998b688b284f255c6a5248abd5b28d754" + integrity sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q== + "@svgr/babel-plugin-transform-react-native-svg@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz#1d8e945a03df65b601551097d8f5e34351d3d305" integrity sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg== +"@svgr/babel-plugin-transform-svg-component@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz#013b4bfca88779711f0ed2739f3f7efcefcf4f7e" + integrity sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw== + "@svgr/babel-plugin-transform-svg-component@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz#48620b9e590e25ff95a80f811544218d27f8a250" integrity sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ== +"@svgr/babel-preset@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-8.1.0.tgz#0e87119aecdf1c424840b9d4565b7137cabf9ece" + integrity sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-attribute" "8.0.0" + "@svgr/babel-plugin-remove-jsx-empty-expression" "8.0.0" + "@svgr/babel-plugin-replace-jsx-attribute-value" "8.0.0" + "@svgr/babel-plugin-svg-dynamic-title" "8.0.0" + "@svgr/babel-plugin-svg-em-dimensions" "8.0.0" + "@svgr/babel-plugin-transform-react-native-svg" "8.1.0" + "@svgr/babel-plugin-transform-svg-component" "8.0.0" + "@svgr/babel-preset@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-6.5.1.tgz#b90de7979c8843c5c580c7e2ec71f024b49eb828" @@ -2049,6 +4008,17 @@ "@svgr/babel-plugin-transform-react-native-svg" "^6.5.1" "@svgr/babel-plugin-transform-svg-component" "^6.5.1" +"@svgr/core@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/core/-/core-8.1.0.tgz#41146f9b40b1a10beaf5cc4f361a16a3c1885e88" + integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA== + dependencies: + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" + camelcase "^6.2.0" + cosmiconfig "^8.1.3" + snake-case "^3.0.4" + "@svgr/core@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/core/-/core-6.5.1.tgz#d3e8aa9dbe3fbd747f9ee4282c1c77a27410488a" @@ -2060,6 +4030,14 @@ camelcase "^6.2.0" cosmiconfig "^7.0.1" +"@svgr/hast-util-to-babel-ast@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz#6952fd9ce0f470e1aded293b792a2705faf4ffd4" + integrity sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q== + dependencies: + "@babel/types" "^7.21.3" + entities "^4.4.0" + "@svgr/hast-util-to-babel-ast@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz#81800bd09b5bcdb968bf6ee7c863d2288fdb80d2" @@ -2068,6 +4046,16 @@ "@babel/types" "^7.20.0" entities "^4.4.0" +"@svgr/plugin-jsx@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz#96969f04a24b58b174ee4cd974c60475acbd6928" + integrity sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA== + dependencies: + "@babel/core" "^7.21.3" + "@svgr/babel-preset" "8.1.0" + "@svgr/hast-util-to-babel-ast" "8.0.0" + svg-parser "^2.0.4" + "@svgr/plugin-jsx@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz#0e30d1878e771ca753c94e69581c7971542a7072" @@ -2078,6 +4066,15 @@ "@svgr/hast-util-to-babel-ast" "^6.5.1" svg-parser "^2.0.4" +"@svgr/plugin-svgo@8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz#b115b7b967b564f89ac58feae89b88c3decd0f00" + integrity sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA== + dependencies: + cosmiconfig "^8.1.3" + deepmerge "^4.3.1" + svgo "^3.0.2" + "@svgr/plugin-svgo@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-6.5.1.tgz#0f91910e988fc0b842f88e0960c2862e022abe84" @@ -2101,6 +4098,20 @@ "@svgr/plugin-jsx" "^6.5.1" "@svgr/plugin-svgo" "^6.5.1" +"@svgr/webpack@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-8.1.0.tgz#16f1b5346f102f89fda6ec7338b96a701d8be0c2" + integrity sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA== + dependencies: + "@babel/core" "^7.21.3" + "@babel/plugin-transform-react-constant-elements" "^7.21.3" + "@babel/preset-env" "^7.20.2" + "@babel/preset-react" "^7.18.6" + "@babel/preset-typescript" "^7.21.0" + "@svgr/core" "8.1.0" + "@svgr/plugin-jsx" "8.1.0" + "@svgr/plugin-svgo" "8.1.0" + "@szmarczak/http-timer@^5.0.1": version "5.0.1" resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" @@ -2140,14 +4151,14 @@ "@types/connect" "*" "@types/node" "*" -"@types/bonjour@^3.5.9": +"@types/bonjour@^3.5.13", "@types/bonjour@^3.5.9": version "3.5.13" resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== dependencies: "@types/node" "*" -"@types/connect-history-api-fallback@^1.3.5": +"@types/connect-history-api-fallback@^1.3.5", "@types/connect-history-api-fallback@^1.5.4": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== @@ -2197,6 +4208,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/estree@^1.0.8": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.9.tgz#cf3f0e876d7bee15a93ab925b82bf570a3904a24" + integrity sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== + "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": version "4.17.41" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz#5077defa630c2e8d28aa9ffc2c01c157c305bef6" @@ -2207,6 +4223,16 @@ "@types/range-parser" "*" "@types/send" "*" +"@types/express-serve-static-core@^4.17.21": + version "4.19.8" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz#99b960322a4d576b239a640ab52ef191989b036f" + integrity sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + "@types/express@*", "@types/express@^4.17.13": version "4.17.21" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" @@ -2217,10 +4243,20 @@ "@types/qs" "*" "@types/serve-static" "*" -"@types/gtag.js@^0.0.12": - version "0.0.12" - resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.12.tgz#095122edca896689bdfcdd73b057e23064d23572" - integrity sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg== +"@types/express@^4.17.25": + version "4.17.25" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.25.tgz#070c8c73a6fee6936d65c195dbbfb7da5026649b" + integrity sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "^1" + +"@types/gtag.js@^0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@types/gtag.js/-/gtag.js-0.0.20.tgz#e47edabb4ed5ecac90a079275958e6c929d7c08a" + integrity sha512-wwAbk3SA2QeU67unN7zPxjEHmPmlXwZXZvQEpbEUQuMCRGgKyE1m6XDuTUA9b6pCGb/GqJmdfMOY5LuDjJSbbg== "@types/hast@^3.0.0": version "3.0.3" @@ -2275,7 +4311,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": +"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -2391,6 +4427,11 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== +"@types/retry@0.12.2": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a" + integrity sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow== + "@types/sax@^1.2.1": version "1.2.7" resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.7.tgz#ba5fe7df9aa9c89b6dff7688a19023dd2963091d" @@ -2411,7 +4452,15 @@ "@types/mime" "^1" "@types/node" "*" -"@types/serve-index@^1.9.1": +"@types/send@<1": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.6.tgz#aeb5385be62ff58a52cd5459daa509ae91651d25" + integrity sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.1", "@types/serve-index@^1.9.4": version "1.9.4" resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== @@ -2427,7 +4476,16 @@ "@types/mime" "*" "@types/node" "*" -"@types/sockjs@^0.3.33": +"@types/serve-static@^1", "@types/serve-static@^1.15.5": + version "1.15.10" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.10.tgz#768169145a778f8f5dfcb6360aead414a3994fee" + integrity sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "<1" + +"@types/sockjs@^0.3.33", "@types/sockjs@^0.3.36": version "0.3.36" resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== @@ -2444,6 +4502,13 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.10.tgz#04ffa7f406ab628f7f7e97ca23e290cd8ab15efc" integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== +"@types/ws@^8.5.10": + version "8.18.1" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" + integrity sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg== + dependencies: + "@types/node" "*" + "@types/ws@^8.5.5": version "8.5.10" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" @@ -2476,21 +4541,44 @@ "@webassemblyjs/helper-numbers" "1.11.6" "@webassemblyjs/helper-wasm-bytecode" "1.11.6" +"@webassemblyjs/ast@1.14.1", "@webassemblyjs/ast@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.14.1.tgz#a9f6a07f2b03c95c8d38c4536a1fdfb521ff55b6" + integrity sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ== + dependencies: + "@webassemblyjs/helper-numbers" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/floating-point-hex-parser@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== +"@webassemblyjs/floating-point-hex-parser@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz#fcca1eeddb1cc4e7b6eed4fc7956d6813b21b9fb" + integrity sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA== + "@webassemblyjs/helper-api-error@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== +"@webassemblyjs/helper-api-error@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz#e0a16152248bc38daee76dd7e21f15c5ef3ab1e7" + integrity sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ== + "@webassemblyjs/helper-buffer@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== +"@webassemblyjs/helper-buffer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz#822a9bc603166531f7d5df84e67b5bf99b72b96b" + integrity sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA== + "@webassemblyjs/helper-numbers@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" @@ -2500,11 +4588,25 @@ "@webassemblyjs/helper-api-error" "1.11.6" "@xtuc/long" "4.2.2" +"@webassemblyjs/helper-numbers@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz#dbd932548e7119f4b8a7877fd5a8d20e63490b2d" + integrity sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.13.2" + "@webassemblyjs/helper-api-error" "1.13.2" + "@xtuc/long" "4.2.2" + "@webassemblyjs/helper-wasm-bytecode@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== +"@webassemblyjs/helper-wasm-bytecode@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz#e556108758f448aae84c850e593ce18a0eb31e0b" + integrity sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA== + "@webassemblyjs/helper-wasm-section@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" @@ -2515,6 +4617,16 @@ "@webassemblyjs/helper-wasm-bytecode" "1.11.6" "@webassemblyjs/wasm-gen" "1.11.6" +"@webassemblyjs/helper-wasm-section@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz#9629dda9c4430eab54b591053d6dc6f3ba050348" + integrity sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/ieee754@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" @@ -2522,6 +4634,13 @@ dependencies: "@xtuc/ieee754" "^1.2.0" +"@webassemblyjs/ieee754@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz#1c5eaace1d606ada2c7fd7045ea9356c59ee0dba" + integrity sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw== + dependencies: + "@xtuc/ieee754" "^1.2.0" + "@webassemblyjs/leb128@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" @@ -2529,11 +4648,23 @@ dependencies: "@xtuc/long" "4.2.2" +"@webassemblyjs/leb128@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.13.2.tgz#57c5c3deb0105d02ce25fa3fd74f4ebc9fd0bbb0" + integrity sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw== + dependencies: + "@xtuc/long" "4.2.2" + "@webassemblyjs/utf8@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== +"@webassemblyjs/utf8@1.13.2": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.13.2.tgz#917a20e93f71ad5602966c2d685ae0c6c21f60f1" + integrity sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ== + "@webassemblyjs/wasm-edit@^1.11.5": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" @@ -2548,6 +4679,20 @@ "@webassemblyjs/wasm-parser" "1.11.6" "@webassemblyjs/wast-printer" "1.11.6" +"@webassemblyjs/wasm-edit@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz#ac6689f502219b59198ddec42dcd496b1004d597" + integrity sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/helper-wasm-section" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-opt" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wast-printer" "1.14.1" + "@webassemblyjs/wasm-gen@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" @@ -2559,6 +4704,17 @@ "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" +"@webassemblyjs/wasm-gen@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz#991e7f0c090cb0bb62bbac882076e3d219da9570" + integrity sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + "@webassemblyjs/wasm-opt@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" @@ -2569,6 +4725,16 @@ "@webassemblyjs/wasm-gen" "1.11.6" "@webassemblyjs/wasm-parser" "1.11.6" +"@webassemblyjs/wasm-opt@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz#e6f71ed7ccae46781c206017d3c14c50efa8106b" + integrity sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-buffer" "1.14.1" + "@webassemblyjs/wasm-gen" "1.14.1" + "@webassemblyjs/wasm-parser" "1.14.1" + "@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" @@ -2581,6 +4747,18 @@ "@webassemblyjs/leb128" "1.11.6" "@webassemblyjs/utf8" "1.11.6" +"@webassemblyjs/wasm-parser@1.14.1", "@webassemblyjs/wasm-parser@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz#b3e13f1893605ca78b52c68e54cf6a865f90b9fb" + integrity sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@webassemblyjs/helper-api-error" "1.13.2" + "@webassemblyjs/helper-wasm-bytecode" "1.13.2" + "@webassemblyjs/ieee754" "1.13.2" + "@webassemblyjs/leb128" "1.13.2" + "@webassemblyjs/utf8" "1.13.2" + "@webassemblyjs/wast-printer@1.11.6": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" @@ -2589,6 +4767,14 @@ "@webassemblyjs/ast" "1.11.6" "@xtuc/long" "4.2.2" +"@webassemblyjs/wast-printer@1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz#3bb3e9638a8ae5fdaf9610e7a06b4d9f9aa6fe07" + integrity sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw== + dependencies: + "@webassemblyjs/ast" "1.14.1" + "@xtuc/long" "4.2.2" + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -2612,6 +4798,11 @@ acorn-import-assertions@^1.9.0: resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== +acorn-import-phases@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz#16eb850ba99a056cb7cbfe872ffb8972e18c8bd7" + integrity sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ== + acorn-jsx@^5.0.0: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -2627,6 +4818,11 @@ acorn@^8.0.0, acorn@^8.0.4, acorn@^8.7.1, acorn@^8.8.2: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +acorn@^8.15.0, acorn@^8.16.0: + version "8.17.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe" + integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg== + address@^1.0.1, address@^1.1.2: version "1.2.2" resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" @@ -2679,32 +4875,32 @@ ajv@^8.0.0, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.2.2" -algoliasearch-helper@^3.13.3: - version "3.16.2" - resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.16.2.tgz#edb8744d6ffe52e731387092fef7324d59214e2f" - integrity sha512-Yl/Gu5Cq4Z5s/AJ0jR37OPI1H3+z7PHz657ibyaXgMOaWvPlZ3OACN13N+7HCLPUlB0BN+8BtmrG/CqTilowBA== +algoliasearch-helper@^3.26.0: + version "3.29.1" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.29.1.tgz#4e764351493d461aa825d2a1209403a98e9b2477" + integrity sha512-6ck2YFudF2Pje7szQoPBiRFTGfd+1I+0I/WfLPGn0bj1kvrFoOQmNyedNiDxTk3/r4IfSLDYk+RA4G7u8H6+yA== dependencies: "@algolia/events" "^4.0.1" -algoliasearch@^4.18.0, algoliasearch@^4.19.1: - version "4.22.1" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.22.1.tgz#f10fbecdc7654639ec20d62f109c1b3a46bc6afc" - integrity sha512-jwydKFQJKIx9kIZ8Jm44SdpigFwRGPESaxZBaHSV0XWN2yBJAOT4mT7ppvlrpA4UGzz92pqFnVKr/kaZXrcreg== - dependencies: - "@algolia/cache-browser-local-storage" "4.22.1" - "@algolia/cache-common" "4.22.1" - "@algolia/cache-in-memory" "4.22.1" - "@algolia/client-account" "4.22.1" - "@algolia/client-analytics" "4.22.1" - "@algolia/client-common" "4.22.1" - "@algolia/client-personalization" "4.22.1" - "@algolia/client-search" "4.22.1" - "@algolia/logger-common" "4.22.1" - "@algolia/logger-console" "4.22.1" - "@algolia/requester-browser-xhr" "4.22.1" - "@algolia/requester-common" "4.22.1" - "@algolia/requester-node-http" "4.22.1" - "@algolia/transporter" "4.22.1" +algoliasearch@^5.37.0: + version "5.55.1" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-5.55.1.tgz#d9c8a4636c79946881802a0d7426fc33333873e1" + integrity sha512-FyaFnnsbVPtevQwqSj/SdxE3jAsSsY0BEH8IVLf9rXxEBdAhAmT6VKCVSMWoaPIHVN1Eufh/1w8q6k8URpIkWw== + dependencies: + "@algolia/abtesting" "1.21.1" + "@algolia/client-abtesting" "5.55.1" + "@algolia/client-analytics" "5.55.1" + "@algolia/client-common" "5.55.1" + "@algolia/client-insights" "5.55.1" + "@algolia/client-personalization" "5.55.1" + "@algolia/client-query-suggestions" "5.55.1" + "@algolia/client-search" "5.55.1" + "@algolia/ingestion" "1.55.1" + "@algolia/monitoring" "1.55.1" + "@algolia/recommend" "5.55.1" + "@algolia/requester-browser-xhr" "5.55.1" + "@algolia/requester-fetch" "5.55.1" + "@algolia/requester-node-http" "5.55.1" ansi-align@^3.0.1: version "3.0.1" @@ -2747,6 +4943,11 @@ ansi-styles@^6.1.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== +ansis@^3.2.0: + version "3.17.0" + resolved "https://registry.yarnpkg.com/ansis/-/ansis-3.17.0.tgz#fa8d9c2a93fe7d1177e0c17f9eeb562a58a832d7" + integrity sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg== + anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -2782,6 +4983,15 @@ array-union@^2.1.0: resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +asn1js@^3.0.10, asn1js@^3.0.6: + version "3.0.10" + resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.10.tgz#df26c874c8a8b41ca605efea47b2ad07551013dd" + integrity sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg== + dependencies: + pvtsutils "^1.3.6" + pvutils "^1.1.5" + tslib "^2.8.1" + astring@^1.8.0: version "1.8.6" resolved "https://registry.yarnpkg.com/astring/-/astring-1.8.6.tgz#2c9c157cf1739d67561c56ba896e6948f6b93731" @@ -2804,6 +5014,17 @@ autoprefixer@^10.4.12, autoprefixer@^10.4.14: picocolors "^1.0.0" postcss-value-parser "^4.2.0" +autoprefixer@^10.4.19, autoprefixer@^10.4.23: + version "10.5.2" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.5.2.tgz#5d7dcaab1c294038fe51e0fa3738d28e559caac0" + integrity sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q== + dependencies: + browserslist "^4.28.4" + caniuse-lite "^1.0.30001799" + fraction.js "^5.3.4" + picocolors "^1.1.1" + postcss-value-parser "^4.2.0" + babel-loader@^9.1.3: version "9.1.3" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.3.tgz#3d0e01b4e69760cc694ee306fe16d358aa1c6f9a" @@ -2812,6 +5033,14 @@ babel-loader@^9.1.3: find-cache-dir "^4.0.0" schema-utils "^4.0.0" +babel-loader@^9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.2.1.tgz#04c7835db16c246dd19ba0914418f3937797587b" + integrity sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA== + dependencies: + find-cache-dir "^4.0.0" + schema-utils "^4.0.0" + babel-plugin-dynamic-import-node@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" @@ -2819,6 +5048,15 @@ babel-plugin-dynamic-import-node@^2.3.3: dependencies: object.assign "^4.1.0" +babel-plugin-polyfill-corejs2@^0.4.14, babel-plugin-polyfill-corejs2@^0.4.15: + version "0.4.17" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz#198f970f1c99a856b466d1187e88ce30bd199d91" + integrity sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-define-polyfill-provider" "^0.6.8" + semver "^6.3.1" + babel-plugin-polyfill-corejs2@^0.4.7: version "0.4.8" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.8.tgz#dbcc3c8ca758a290d47c3c6a490d59429b0d2269" @@ -2828,6 +5066,22 @@ babel-plugin-polyfill-corejs2@^0.4.7: "@babel/helper-define-polyfill-provider" "^0.5.0" semver "^6.3.1" +babel-plugin-polyfill-corejs3@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz#bb7f6aeef7addff17f7602a08a6d19a128c30164" + integrity sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.5" + core-js-compat "^3.43.0" + +babel-plugin-polyfill-corejs3@^0.14.0: + version "0.14.2" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz#6ac08d2f312affb70c4c69c0fbba4cb417ee5587" + integrity sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.8" + core-js-compat "^3.48.0" + babel-plugin-polyfill-corejs3@^0.8.7: version "0.8.7" resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.7.tgz#941855aa7fdaac06ed24c730a93450d2b2b76d04" @@ -2843,6 +5097,13 @@ babel-plugin-polyfill-regenerator@^0.5.4: dependencies: "@babel/helper-define-polyfill-provider" "^0.5.0" +babel-plugin-polyfill-regenerator@^0.6.5, babel-plugin-polyfill-regenerator@^0.6.6: + version "0.6.8" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz#8a6bfd5dd54239362b3d06ce47ac52b2d95d7721" + integrity sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.8" + bail@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" @@ -2853,6 +5114,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +baseline-browser-mapping@^2.10.42: + version "2.10.42" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.42.tgz#195dcc76baa269a497f0b07decace169fee9ac58" + integrity sha512-c/jurFrDLyui7o1J86yLkRu4LMsTYcBohveus7/I2Hzdn9KIP2bdJPTue/lR1KH46enoPbD77GKeSYNdyPoD3Q== + batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" @@ -2886,6 +5152,24 @@ body-parser@1.20.1: type-is "~1.6.18" unpipe "1.0.0" +body-parser@~1.20.5: + version "1.20.5" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.5.tgz#303c8c34423d1d6fa799bc764e93c1e4dc6ebf64" + integrity sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA== + dependencies: + bytes "~3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "~1.2.0" + http-errors "~2.0.1" + iconv-lite "~0.4.24" + on-finished "~2.4.1" + qs "~6.15.1" + raw-body "~2.5.3" + type-is "~1.6.18" + unpipe "~1.0.0" + bonjour-service@^1.0.11: version "1.2.1" resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" @@ -2894,6 +5178,14 @@ bonjour-service@^1.0.11: fast-deep-equal "^3.1.3" multicast-dns "^7.2.5" +bonjour-service@^1.2.1: + version "1.4.2" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.4.2.tgz#33ce18eddf13047e5eeb08939018b20bae1a1473" + integrity sha512-lMskhnsW70yWHr4PhPeh2rvaIkLSaDpp+nmtbXBZaNKTXwxL73QOkW6HhbzqTImXjevn9TreGT4GACGBCGP9nQ== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + boolbase@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -2952,21 +5244,44 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.18.1, browserslist@^4 node-releases "^2.0.14" update-browserslist-db "^1.0.13" +browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.28.1, browserslist@^4.28.4: + version "4.28.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.5.tgz#438b7d38c0d4b47740bbb36778d5bdca01b37838" + integrity sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ== + dependencies: + baseline-browser-mapping "^2.10.42" + caniuse-lite "^1.0.30001800" + electron-to-chromium "^1.5.387" + node-releases "^2.0.50" + update-browserslist-db "^1.2.3" + buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +bundle-name@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889" + integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q== + dependencies: + run-applescript "^7.0.0" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== -bytes@3.1.2: +bytes@3.1.2, bytes@~3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +bytestreamjs@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/bytestreamjs/-/bytestreamjs-2.0.1.tgz#a32947c7ce389a6fa11a09a9a563d0a45889535e" + integrity sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ== + cacheable-lookup@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz#3476a8215d046e5a3202a9209dd13fec1f933a27" @@ -2985,6 +5300,14 @@ cacheable-request@^10.2.8: normalize-url "^8.0.0" responselike "^3.0.0" +call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + call-bind@^1.0.0, call-bind@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" @@ -2994,6 +5317,14 @@ call-bind@^1.0.0, call-bind@^1.0.5: get-intrinsic "^1.2.1" set-function-length "^1.1.1" +call-bound@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -3032,6 +5363,11 @@ caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001565, caniuse-lite@^1.0.30001578: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001579.tgz#45c065216110f46d6274311a4b3fcf6278e0852a" integrity sha512-u5AUVkixruKHJjw/pj9wISlcMpgFWzSrczLZbrqBSxukQixmg0SJ5sZTpvaFvxU0HoQKd4yoyAogyrAz9pzJnA== +caniuse-lite@^1.0.30001799, caniuse-lite@^1.0.30001800: + version "1.0.30001803" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001803.tgz#b2a5d696e042bc8304dcd4942c39fe330fbbcb24" + integrity sha512-g/uHREV2ZpK9qMalCsWaxmA6ol+DX8GYhuf3T40RKoP+oL7vhRJh8LNt73PCjpnR6l14FzfPrB5Yux4PKm2meg== + ccount@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" @@ -3096,7 +5432,7 @@ cheerio-select@^2.1.0: domhandler "^5.0.3" domutils "^3.0.1" -cheerio@^1.0.0-rc.12, cheerio@^1.0.0-rc.3: +cheerio@1.0.0-rc.12, cheerio@^1.0.0-rc.3: version "1.0.0-rc.12" resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.12.tgz#788bf7466506b1c6bf5fae51d24a2c4d62e47683" integrity sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q== @@ -3124,6 +5460,21 @@ chokidar@^3.4.2, chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -3134,7 +5485,7 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== -clean-css@^5.2.2, clean-css@^5.3.2, clean-css@~5.3.2: +clean-css@^5.2.2, clean-css@^5.3.2, clean-css@^5.3.3, clean-css@~5.3.2: version "5.3.3" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.3.tgz#b330653cd3bd6b75009cc25c714cae7b93351ccd" integrity sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg== @@ -3208,7 +5559,7 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colord@^2.9.1: +colord@^2.9.1, colord@^2.9.3: version "2.9.3" resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== @@ -3258,7 +5609,7 @@ common-path-prefix@^3.0.0: resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-3.0.0.tgz#7d007a7e07c58c4b4d5f433131a19141b29f11e0" integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== -compressible@~2.0.16: +compressible@~2.0.16, compressible@~2.0.18: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== @@ -3278,6 +5629,19 @@ compression@^1.7.4: safe-buffer "5.1.2" vary "~1.1.2" +compression@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79" + integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== + dependencies: + bytes "3.1.2" + compressible "~2.0.18" + debug "2.6.9" + negotiator "~0.6.4" + on-headers "~1.1.0" + safe-buffer "5.2.1" + vary "~1.1.2" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -3312,19 +5676,24 @@ consola@^2.15.3: resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== +consola@^3.2.3: + version "3.4.2" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.4.2.tgz#5af110145397bb67afdab77013fdc34cae590ea7" + integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== + content-disposition@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" integrity sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA== -content-disposition@0.5.4: +content-disposition@0.5.4, content-disposition@~0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== dependencies: safe-buffer "5.2.1" -content-type@~1.0.4: +content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== @@ -3339,11 +5708,21 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== +cookie-signature@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454" + integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA== + cookie@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@~0.7.1: + version "0.7.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" + integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w== + copy-text-to-clipboard@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.0.tgz#0202b2d9bdae30a49a53f898626dcc3b49ad960b" @@ -3368,6 +5747,13 @@ core-js-compat@^3.31.0, core-js-compat@^3.33.1: dependencies: browserslist "^4.22.2" +core-js-compat@^3.43.0, core-js-compat@^3.48.0: + version "3.49.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.49.0.tgz#06145447d92f4aaf258a0c44f24b47afaeaffef6" + integrity sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA== + dependencies: + browserslist "^4.28.1" + core-js-pure@^3.30.2: version "3.35.1" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.35.1.tgz#f33ad7fdf9dddae260339a30e5f8363f5c49a3bc" @@ -3405,7 +5791,7 @@ cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" -cosmiconfig@^8.3.5: +cosmiconfig@^8.1.3, cosmiconfig@^8.3.5: version "8.3.6" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== @@ -3431,11 +5817,46 @@ crypto-random-string@^4.0.0: dependencies: type-fest "^1.0.1" +css-blank-pseudo@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz#32020bff20a209a53ad71b8675852b49e8d57e46" + integrity sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag== + dependencies: + postcss-selector-parser "^7.0.0" + css-declaration-sorter@^6.3.1: version "6.4.1" resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71" integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g== +css-declaration-sorter@^7.2.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-7.4.0.tgz#9c215fbda2dcf4083bae69f125688158ae847deb" + integrity sha512-LTuzjPoyA2vMGKKcaOqKSp7Ub2eGrNfKiZH4LpezxpNrsICGCSFvsQOI29psISxNZtaXibkC2CXzrQ5enMeGGw== + +css-has-pseudo@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-7.0.3.tgz#a5ee2daf5f70a2032f3cefdf1e36e7f52a243873" + integrity sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-value-parser "^4.2.0" + +css-loader@^6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.11.0.tgz#33bae3bf6363d0a7c2cf9031c96c744ff54d85ba" + integrity sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g== + dependencies: + icss-utils "^5.1.0" + postcss "^8.4.33" + postcss-modules-extract-imports "^3.1.0" + postcss-modules-local-by-default "^4.0.5" + postcss-modules-scope "^3.2.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.2.0" + semver "^7.5.4" + css-loader@^6.8.1: version "6.9.1" resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.9.1.tgz#9ec9a434368f2bdfeffbf8f6901a1ce773586c6b" @@ -3462,6 +5883,23 @@ css-minimizer-webpack-plugin@^4.2.2: serialize-javascript "^6.0.0" source-map "^0.6.1" +css-minimizer-webpack-plugin@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz#33effe662edb1a0bf08ad633c32fa75d0f7ec565" + integrity sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + cssnano "^6.0.1" + jest-worker "^29.4.3" + postcss "^8.4.24" + schema-utils "^4.0.1" + serialize-javascript "^6.0.1" + +css-prefers-color-scheme@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz#ba001b99b8105b8896ca26fc38309ddb2278bd3c" + integrity sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ== + css-select@^4.1.3: version "4.3.0" resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" @@ -3492,11 +5930,32 @@ css-tree@^1.1.2, css-tree@^1.1.3: mdn-data "2.0.14" source-map "^0.6.1" +css-tree@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== + dependencies: + mdn-data "2.0.30" + source-map-js "^1.0.1" + +css-tree@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032" + integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA== + dependencies: + mdn-data "2.0.28" + source-map-js "^1.0.1" + css-what@^6.0.1, css-what@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +cssdb@^8.6.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.9.0.tgz#e24d44824895957a4a5c75ba72c910f94e7aed77" + integrity sha512-J8jOU/hLjaXcO1LldOLraJSQpfLXRKof0I7mtbRyOy2AAXgqst0x9rlgi2qXeD6d0ou3ZLqcPAMqYVbpCbrxEw== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" @@ -3514,6 +5973,19 @@ cssnano-preset-advanced@^5.3.10: postcss-reduce-idents "^5.2.0" postcss-zindex "^5.1.0" +cssnano-preset-advanced@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz#82b090872b8f98c471f681d541c735acf8b94d3f" + integrity sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ== + dependencies: + autoprefixer "^10.4.19" + browserslist "^4.23.0" + cssnano-preset-default "^6.1.2" + postcss-discard-unused "^6.0.5" + postcss-merge-idents "^6.0.3" + postcss-reduce-idents "^6.0.3" + postcss-zindex "^6.0.2" + cssnano-preset-default@^5.2.14: version "5.2.14" resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz#309def4f7b7e16d71ab2438052093330d9ab45d8" @@ -3549,11 +6021,52 @@ cssnano-preset-default@^5.2.14: postcss-svgo "^5.1.0" postcss-unique-selectors "^5.1.1" +cssnano-preset-default@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz#adf4b89b975aa775f2750c89dbaf199bbd9da35e" + integrity sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg== + dependencies: + browserslist "^4.23.0" + css-declaration-sorter "^7.2.0" + cssnano-utils "^4.0.2" + postcss-calc "^9.0.1" + postcss-colormin "^6.1.0" + postcss-convert-values "^6.1.0" + postcss-discard-comments "^6.0.2" + postcss-discard-duplicates "^6.0.3" + postcss-discard-empty "^6.0.3" + postcss-discard-overridden "^6.0.2" + postcss-merge-longhand "^6.0.5" + postcss-merge-rules "^6.1.1" + postcss-minify-font-values "^6.1.0" + postcss-minify-gradients "^6.0.3" + postcss-minify-params "^6.1.0" + postcss-minify-selectors "^6.0.4" + postcss-normalize-charset "^6.0.2" + postcss-normalize-display-values "^6.0.2" + postcss-normalize-positions "^6.0.2" + postcss-normalize-repeat-style "^6.0.2" + postcss-normalize-string "^6.0.2" + postcss-normalize-timing-functions "^6.0.2" + postcss-normalize-unicode "^6.1.0" + postcss-normalize-url "^6.0.2" + postcss-normalize-whitespace "^6.0.2" + postcss-ordered-values "^6.0.2" + postcss-reduce-initial "^6.1.0" + postcss-reduce-transforms "^6.0.2" + postcss-svgo "^6.0.3" + postcss-unique-selectors "^6.0.4" + cssnano-utils@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== +cssnano-utils@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-4.0.2.tgz#56f61c126cd0f11f2eef1596239d730d9fceff3c" + integrity sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ== + cssnano@^5.1.15, cssnano@^5.1.8: version "5.1.15" resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.15.tgz#ded66b5480d5127fcb44dac12ea5a983755136bf" @@ -3563,6 +6076,14 @@ cssnano@^5.1.15, cssnano@^5.1.8: lilconfig "^2.0.3" yaml "^1.10.2" +cssnano@^6.0.1, cssnano@^6.1.2: + version "6.1.2" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-6.1.2.tgz#4bd19e505bd37ee7cf0dc902d3d869f6d79c66b8" + integrity sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA== + dependencies: + cssnano-preset-default "^6.1.2" + lilconfig "^3.1.1" + csso@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" @@ -3570,6 +6091,13 @@ csso@^4.2.0: dependencies: css-tree "^1.1.2" +csso@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" + integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ== + dependencies: + css-tree "~2.2.0" + csstype@^3.0.2: version "3.1.3" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" @@ -3594,6 +6122,13 @@ debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1: dependencies: ms "2.1.2" +debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + decode-named-character-reference@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" @@ -3613,11 +6148,24 @@ deep-extend@^0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deepmerge@^4.2.2: +deepmerge@^4.2.2, deepmerge@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== +default-browser-id@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.1.tgz#f7a7ccb8f5104bf8e0f71ba3b1ccfa5eafdb21e8" + integrity sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q== + +default-browser@^5.2.1: + version "5.5.0" + resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.5.0.tgz#2792e886f2422894545947cc80e1a444496c5976" + integrity sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw== + dependencies: + bundle-name "^4.1.0" + default-browser-id "^5.0.0" + default-gateway@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" @@ -3644,6 +6192,11 @@ define-lazy-prop@^2.0.0: resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== +define-lazy-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f" + integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg== + define-properties@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" @@ -3667,7 +6220,7 @@ del@^6.1.1: rimraf "^3.0.2" slash "^3.0.0" -depd@2.0.0: +depd@2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -3682,7 +6235,7 @@ dequal@^2.0.0: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== -destroy@1.2.0: +destroy@1.2.0, destroy@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== @@ -3806,6 +6359,15 @@ dot-prop@^6.0.1: dependencies: is-obj "^2.0.0" +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + duplexer@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -3826,6 +6388,11 @@ electron-to-chromium@^1.4.601: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.643.tgz#081a20c5534db91e66ef094f68624960f674768f" integrity sha512-QHscvvS7gt155PtoRC0dR2ilhL8E9LHhfTQEq1uD5AL0524rBLAwpAREFH06f87/e45B9XkR6Ki5dbhbCsVEIg== +electron-to-chromium@^1.5.387: + version "1.5.388" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.388.tgz#002666dfda32e087c44fd01b7ce1719b6ec89c57" + integrity sha512-Pl/aJaqOOxYxda3vcx1IKSJimwYXHDkEnGn0F+kG2EE68dDtx2uCinaS+Vih8Z91B9t8CSAbiF/HKyWcnXjhzw== + emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -3856,6 +6423,11 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + enhanced-resolve@^5.15.0: version "5.15.0" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" @@ -3864,6 +6436,14 @@ enhanced-resolve@^5.15.0: graceful-fs "^4.2.4" tapable "^2.2.0" +enhanced-resolve@^5.22.2: + version "5.24.2" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz#f25d703a24431cb1e02f944adb74aefa4fcb8d7e" + integrity sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.3.3" + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -3881,16 +6461,43 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es-module-lexer@^1.2.1: version "1.4.1" resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.4.1.tgz#41ea21b43908fe6a287ffcbe4300f790555331f5" integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w== +es-module-lexer@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-2.3.0.tgz#fda770234c345064c122eb905e1c4200ffa4ce7e" + integrity sha512-KLdwQm2NvGLDkQDCGvmiQrhkd0JbMzXthwQAUgWjQuQdBLFa3eiBP5arXZyA+f8x+x7OXgud6bq2rxjGtHV2tw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz#a2d0b373205724dfa525d23b0c3e1b1ca582c99b" + integrity sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw== + dependencies: + es-errors "^1.3.0" + escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + escape-goat@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-4.0.0.tgz#9424820331b510b0666b98f7873fe11ac4aa8081" @@ -4023,7 +6630,7 @@ eval@^0.1.8: "@types/node" "*" require-like ">= 0.1.1" -eventemitter3@^4.0.0: +eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -4033,7 +6640,7 @@ events@^3.2.0: resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -execa@^5.0.0: +execa@^5.0.0, execa@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== @@ -4085,6 +6692,43 @@ express@^4.17.3: utils-merge "1.0.1" vary "~1.1.2" +express@^4.22.1: + version "4.22.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.22.2.tgz#c17ae0981e5efc24b22272f0e041c4662503b700" + integrity sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "~1.20.5" + content-disposition "~0.5.4" + content-type "~1.0.4" + cookie "~0.7.1" + cookie-signature "~1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.3.1" + fresh "~0.5.2" + http-errors "~2.0.0" + merge-descriptors "1.0.3" + methods "~1.1.2" + on-finished "~2.4.1" + parseurl "~1.3.3" + path-to-regexp "~0.1.12" + proxy-addr "~2.0.7" + qs "~6.15.1" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "~0.19.0" + serve-static "~1.16.2" + setprototypeof "1.2.0" + statuses "~2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" @@ -4179,11 +6823,24 @@ finalhandler@1.2.0: integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== dependencies: debug "2.6.9" - encodeurl "~1.0.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +finalhandler@~1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.2.tgz#1ebc2228fc7673aac4a472c310cc05b77d852b88" + integrity sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg== + dependencies: + debug "2.6.9" + encodeurl "~2.0.0" escape-html "~1.0.3" - on-finished "2.4.1" + on-finished "~2.4.1" parseurl "~1.3.3" - statuses "2.0.1" + statuses "~2.0.2" unpipe "~1.0.0" find-cache-dir@^4.0.0: @@ -4266,7 +6923,12 @@ fraction.js@^4.3.7: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== -fresh@0.5.2: +fraction.js@^5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-5.3.4.tgz#8c0fcc6a9908262df4ed197427bdeef563e0699a" + integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ== + +fresh@0.5.2, fresh@~0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== @@ -4289,6 +6951,15 @@ fs-extra@^11.1.1: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^11.2.0: + version "11.3.6" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.6.tgz#f7cb80e9df550cd1db6f537fa5cdd568d3e70d10" + integrity sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^9.0.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" @@ -4334,11 +7005,35 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -4363,6 +7058,11 @@ glob-parent@^6.0.1: dependencies: is-glob "^4.0.3" +glob-to-regex.js@^1.0.0, glob-to-regex.js@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz#2b323728271d133830850e32311f40766c5f6413" + integrity sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ== + glob-to-regexp@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" @@ -4438,6 +7138,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + got@^12.1.0: version "12.6.1" resolved "https://registry.yarnpkg.com/got/-/got-12.6.1.tgz#8869560d1383353204b5a9435f782df9c091f549" @@ -4460,7 +7165,7 @@ graceful-fs@4.2.10: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -4514,6 +7219,11 @@ has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + has-yarn@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-3.0.0.tgz#c3c21e559730d1d3b57e28af1f30d06fac38147d" @@ -4526,6 +7236,13 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" +hasown@^2.0.2, hasown@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.4.tgz#8c62d8cb90beb2aad5d0a5b67581ad9854c3f003" + integrity sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== + dependencies: + function-bind "^1.1.2" + hast-util-from-parse5@^8.0.0: version "8.0.1" resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz#654a5676a41211e14ee80d1b1758c399a0327651" @@ -4731,6 +7448,17 @@ html-webpack-plugin@^5.5.3: pretty-error "^4.0.0" tapable "^2.0.0" +html-webpack-plugin@^5.6.0: + version "5.6.7" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.7.tgz#429bab4e12abf3c07e1c608886608e2df2c06b11" + integrity sha512-md+vXtdCAe60s1k6AU3dUyMJnDxUyQAwfwPKoLisvgUF1IXjtlLsk2se54+qfL9Mdm26bbwvjJybpNx48NKRLw== + dependencies: + "@types/html-minifier-terser" "^6.0.0" + html-minifier-terser "^6.0.2" + lodash "^4.17.21" + pretty-error "^4.0.0" + tapable "^2.0.0" + htmlparser2@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" @@ -4782,6 +7510,17 @@ http-errors@~1.6.2: setprototypeof "1.1.0" statuses ">= 1.4.0 < 2" +http-errors@~2.0.0, http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + http-parser-js@>=0.5.1: version "0.5.8" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" @@ -4798,6 +7537,17 @@ http-proxy-middleware@^2.0.3: is-plain-obj "^3.0.0" micromatch "^4.0.2" +http-proxy-middleware@^2.0.9: + version "2.0.10" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.10.tgz#b2df7b705203d7a8c269ac8450cf96b00c532f94" + integrity sha512-RKzRWNPxUZqbuk3BC5mGVJbBnWgr+diEnjJexIOytFbBzDy88Fbh/YvBr3DsNrl1jYAfjWfpATEv0NO35FDuPQ== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + http-proxy@^1.18.1: version "1.18.1" resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" @@ -4820,7 +7570,12 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -iconv-lite@0.4.24: +hyperdyperid@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b" + integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A== + +iconv-lite@0.4.24, iconv-lite@~0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -4844,6 +7599,11 @@ image-size@^1.0.2: dependencies: queue "6.0.2" +image-size@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-2.0.2.tgz#84a7b43704db5736f364bf0d1b029821299b4bdc" + integrity sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w== + immediate@^3.2.3: version "3.3.0" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" @@ -4877,10 +7637,10 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== -infima@0.2.0-alpha.43: - version "0.2.0-alpha.43" - resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.43.tgz#f7aa1d7b30b6c08afef441c726bac6150228cbe0" - integrity sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ== +infima@0.2.0-alpha.45: + version "0.2.0-alpha.45" + resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.45.tgz#542aab5a249274d81679631b492973dd2c1e7466" + integrity sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw== inflight@^1.0.4: version "1.0.6" @@ -4890,7 +7650,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3, inherits@~2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -4942,6 +7702,11 @@ ipaddr.js@^2.0.1: resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.1.0.tgz#2119bc447ff8c257753b196fc5f1ce08a4cdf39f" integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== +ipaddr.js@^2.1.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.4.0.tgz#038e9ceaf8219efc5bb76347b7eb787875d5095b" + integrity sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ== + is-alphabetical@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" @@ -4981,6 +7746,13 @@ is-core-module@^2.13.0: dependencies: hasown "^2.0.0" +is-core-module@^2.16.1: + version "2.16.2" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.2.tgz#3e07450a8080ebce3fbf0cac494f4d2ab324e082" + integrity sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA== + dependencies: + hasown "^2.0.3" + is-decimal@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" @@ -4991,6 +7763,11 @@ is-docker@^2.0.0, is-docker@^2.1.1: resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== +is-docker@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" + integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== + is-extendable@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -5018,6 +7795,13 @@ is-hexadecimal@^2.0.0: resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== +is-inside-container@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4" + integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA== + dependencies: + is-docker "^3.0.0" + is-installed-globally@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" @@ -5026,6 +7810,11 @@ is-installed-globally@^0.4.0: global-dirs "^3.0.0" is-path-inside "^3.0.2" +is-network-error@^1.0.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/is-network-error/-/is-network-error-1.3.2.tgz#9460bc30f8419a4bca77114f4de88a3ee5e0c519" + integrity sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA== + is-npm@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-6.0.0.tgz#b59e75e8915543ca5d881ecff864077cba095261" @@ -5112,6 +7901,13 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +is-wsl@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.1.tgz#327897b26832a3eb117da6c27492d04ca132594f" + integrity sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw== + dependencies: + is-inside-container "^1.0.0" + is-yarn-global@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.4.1.tgz#b312d902b313f81e4eaf98b6361ba2b45cd694bb" @@ -5158,7 +7954,7 @@ jest-worker@^27.4.5: merge-stream "^2.0.0" supports-color "^8.0.0" -jest-worker@^29.1.2: +jest-worker@^29.1.2, jest-worker@^29.4.3: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== @@ -5209,6 +8005,11 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@^3.0.2, jsesc@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" @@ -5279,6 +8080,14 @@ latest-version@^7.0.0: dependencies: package-json "^8.1.0" +launch-editor@^2.14.1: + version "2.14.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.14.1.tgz#f7e0da3f58aaea03fea01074d840b5f739ed7ddc" + integrity sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA== + dependencies: + picocolors "^1.1.1" + shell-quote "^1.8.4" + launch-editor@^2.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.1.tgz#f259c9ef95cbc9425620bbbd14b468fcdb4ffe3c" @@ -5297,6 +8106,11 @@ lilconfig@^2.0.3: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== +lilconfig@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" + integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -5307,6 +8121,11 @@ loader-runner@^4.2.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== +loader-runner@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.2.tgz#9913d3a15971f8f635915e601fb5c9d495d918e9" + integrity sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w== + loader-utils@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" @@ -5426,6 +8245,11 @@ markdown-table@^3.0.0: resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + mdast-util-directive@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz#3fb1764e705bbdf0afb0d3f889e4404c3e82561f" @@ -5648,6 +8472,16 @@ mdn-data@2.0.14: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== +mdn-data@2.0.28: + version "2.0.28" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" + integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== + +mdn-data@2.0.30: + version "2.0.30" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -5660,11 +8494,36 @@ memfs@^3.1.2, memfs@^3.4.3: dependencies: fs-monkey "^1.0.4" +memfs@^4.43.1: + version "4.58.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-4.58.0.tgz#b995e7183c09d71f28f803fbfd326704c9216390" + integrity sha512-0n6CDBqIT/eGrbWdDVNkLjasjupnEpOFjFOtXrS5p6EYYXGXBn5ZIMLetBlVdQYpP0hGK2MEOgYAC0/+NOr91w== + dependencies: + "@jsonjoy.com/fs-core" "4.58.0" + "@jsonjoy.com/fs-fsa" "4.58.0" + "@jsonjoy.com/fs-node" "4.58.0" + "@jsonjoy.com/fs-node-builtins" "4.58.0" + "@jsonjoy.com/fs-node-to-fsa" "4.58.0" + "@jsonjoy.com/fs-node-utils" "4.58.0" + "@jsonjoy.com/fs-print" "4.58.0" + "@jsonjoy.com/fs-snapshot" "4.58.0" + "@jsonjoy.com/json-pack" "^1.11.0" + "@jsonjoy.com/util" "^1.9.0" + glob-to-regex.js "^1.0.1" + thingies "^2.5.0" + tree-dump "^1.0.3" + tslib "^2.0.0" + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge-descriptors@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5" + integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -6109,6 +8968,11 @@ mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + mime-db@~1.33.0: version "1.33.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" @@ -6128,6 +8992,13 @@ mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, dependencies: mime-db "1.52.0" +mime-types@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + dependencies: + mime-db "^1.54.0" + mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -6155,6 +9026,14 @@ mini-css-extract-plugin@^2.7.6: dependencies: schema-utils "^4.0.0" +mini-css-extract-plugin@^2.9.2: + version "2.10.2" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz#5c85ec9450c05d26e32531b465a15a08c3a57253" + integrity sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg== + dependencies: + schema-utils "^4.0.0" + tapable "^2.2.1" + minimalistic-assert@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -6167,11 +9046,28 @@ minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1: dependencies: brace-expansion "^1.1.7" +minimatch@3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== + dependencies: + brace-expansion "^1.1.7" + minimist@^1.2.0: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +minimizer-webpack-plugin@^5.6.1: + version "5.6.1" + resolved "https://registry.yarnpkg.com/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz#289922a4c96c4ed1ddb76b8a00bd8074e89a2f7f" + integrity sha512-DoeAZz8Q1C1znwsUzej1fdoi4jCf7/+Em27ouLqfK/+3m8G+D7yDhUwrc3CNhjSzGUN1kn7Iv4sWmjflQHenpw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.25" + jest-worker "^27.4.5" + schema-utils "^4.3.0" + terser "^5.31.1" + mrmime@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.0.tgz#151082a6e06e59a9a39b46b3e14d5cfe92b3abb4" @@ -6187,7 +9083,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3: +ms@2.1.3, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -6200,6 +9096,11 @@ multicast-dns@^7.2.5: dns-packet "^5.2.2" thunky "^1.0.2" +nanoid@^3.3.12: + version "3.3.15" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.15.tgz#36c490fad8c6e86c824c940dfdde999b69ed4316" + integrity sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA== + nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -6210,6 +9111,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -6243,6 +9149,11 @@ node-releases@^2.0.14: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== +node-releases@^2.0.50: + version "2.0.50" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.50.tgz#597197a852071ce42fc2550e58e223242bcba969" + integrity sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg== + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -6282,11 +9193,24 @@ nth-check@^2.0.1: dependencies: boolbase "^1.0.0" +null-loader@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/null-loader/-/null-loader-4.0.1.tgz#8e63bd3a2dd3c64236a4679428632edd0a6dbc6a" + integrity sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== +object-inspect@^1.13.3, object-inspect@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + object-inspect@^1.9.0: version "1.13.1" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" @@ -6312,7 +9236,7 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -on-finished@2.4.1: +on-finished@2.4.1, on-finished@^2.4.1, on-finished@~2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== @@ -6324,6 +9248,11 @@ on-headers@~1.0.2: resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== +on-headers@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -6338,6 +9267,16 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" +open@^10.0.3: + version "10.2.0" + resolved "https://registry.yarnpkg.com/open/-/open-10.2.0.tgz#b9d855be007620e80b6fb05fac98141fe62db73c" + integrity sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA== + dependencies: + default-browser "^5.2.1" + define-lazy-prop "^3.0.0" + is-inside-container "^1.0.0" + wsl-utils "^0.1.0" + open@^8.0.9, open@^8.4.0: version "8.4.2" resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" @@ -6357,6 +9296,11 @@ p-cancelable@^3.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow== + p-limit@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -6406,6 +9350,14 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" +p-queue@^6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + p-retry@^4.5.0: version "4.6.2" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" @@ -6414,6 +9366,22 @@ p-retry@^4.5.0: "@types/retry" "0.12.0" retry "^0.13.1" +p-retry@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-6.2.1.tgz#81828f8dc61c6ef5a800585491572cc9892703af" + integrity sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ== + dependencies: + "@types/retry" "0.12.2" + is-network-error "^1.0.0" + retry "^0.13.1" + +p-timeout@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -6546,6 +9514,11 @@ path-to-regexp@2.2.1: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" integrity sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ== +path-to-regexp@3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" + integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== + path-to-regexp@^1.7.0: version "1.8.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" @@ -6553,6 +9526,11 @@ path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +path-to-regexp@~0.1.12: + version "0.1.13" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz#9b22ec16bc3ab88d05a0c7e369869421401ab17d" + integrity sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -6572,6 +9550,11 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -6591,6 +9574,25 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" +pkijs@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/pkijs/-/pkijs-3.4.0.tgz#d9164def30ff6d97be2d88966d5e36192499ca9c" + integrity sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw== + dependencies: + "@noble/hashes" "1.4.0" + asn1js "^3.0.6" + bytestreamjs "^2.0.1" + pvtsutils "^1.3.6" + pvutils "^1.1.3" + tslib "^2.8.1" + +postcss-attribute-case-insensitive@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz#0c4500e3bcb2141848e89382c05b5a31c23033a3" + integrity sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-calc@^8.2.3: version "8.2.4" resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" @@ -6599,6 +9601,48 @@ postcss-calc@^8.2.3: postcss-selector-parser "^6.0.9" postcss-value-parser "^4.2.0" +postcss-calc@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-9.0.1.tgz#a744fd592438a93d6de0f1434c572670361eb6c6" + integrity sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ== + dependencies: + postcss-selector-parser "^6.0.11" + postcss-value-parser "^4.2.0" + +postcss-clamp@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz#7263e95abadd8c2ba1bd911b0b5a5c9c93e02363" + integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-functional-notation@^7.0.12: + version "7.0.12" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz#9a3df2296889e629fde18b873bb1f50a4ecf4b83" + integrity sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +postcss-color-hex-alpha@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz#5dd3eba1f8facb4ea306cba6e3f7712e876b0c76" + integrity sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-color-rebeccapurple@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz#5ada28406ac47e0796dff4056b0a9d5a6ecead98" + integrity sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + postcss-colormin@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz#86c27c26ed6ba00d96c79e08f3ffb418d1d1988f" @@ -6609,6 +9653,16 @@ postcss-colormin@^5.3.1: colord "^2.9.1" postcss-value-parser "^4.2.0" +postcss-colormin@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-6.1.0.tgz#076e8d3fb291fbff7b10e6b063be9da42ff6488d" + integrity sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw== + dependencies: + browserslist "^4.23.0" + caniuse-api "^3.0.0" + colord "^2.9.3" + postcss-value-parser "^4.2.0" + postcss-convert-values@^5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz#04998bb9ba6b65aa31035d669a6af342c5f9d393" @@ -6617,26 +9671,92 @@ postcss-convert-values@^5.1.3: browserslist "^4.21.4" postcss-value-parser "^4.2.0" +postcss-convert-values@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz#3498387f8efedb817cbc63901d45bd1ceaa40f48" + integrity sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w== + dependencies: + browserslist "^4.23.0" + postcss-value-parser "^4.2.0" + +postcss-custom-media@^11.0.6: + version "11.0.6" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz#6b450e5bfa209efb736830066682e6567bd04967" + integrity sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.5" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/media-query-list-parser" "^4.0.3" + +postcss-custom-properties@^14.0.6: + version "14.0.6" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz#1af73a650bf115ba052cf915287c9982825fc90e" + integrity sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.5" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-custom-selectors@^8.0.5: + version "8.0.5" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz#9448ed37a12271d7ab6cb364b6f76a46a4a323e8" + integrity sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.5" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + postcss-selector-parser "^7.0.0" + +postcss-dir-pseudo-class@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz#80d9e842c9ae9d29f6bf5fd3cf9972891d6cc0ca" + integrity sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-discard-comments@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz#8df5e81d2925af2780075840c1526f0660e53696" integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ== +postcss-discard-comments@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz#e768dcfdc33e0216380623652b0a4f69f4678b6c" + integrity sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw== + postcss-discard-duplicates@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== +postcss-discard-duplicates@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz#d121e893c38dc58a67277f75bb58ba43fce4c3eb" + integrity sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw== + postcss-discard-empty@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c" integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== +postcss-discard-empty@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz#ee39c327219bb70473a066f772621f81435a79d9" + integrity sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ== + postcss-discard-overridden@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e" integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== +postcss-discard-overridden@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz#4e9f9c62ecd2df46e8fdb44dc17e189776572e2d" + integrity sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ== + postcss-discard-unused@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-5.1.0.tgz#8974e9b143d887677304e558c1166d3762501142" @@ -6644,7 +9764,66 @@ postcss-discard-unused@^5.1.0: dependencies: postcss-selector-parser "^6.0.5" -postcss-loader@^7.3.3: +postcss-discard-unused@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz#c1b0e8c032c6054c3fbd22aaddba5b248136f338" + integrity sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA== + dependencies: + postcss-selector-parser "^6.0.16" + +postcss-double-position-gradients@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz#b482d08b5ced092b393eb297d07976ab482d4cad" + integrity sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-focus-visible@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz#1f7904904368a2d1180b220595d77b6f8a957868" + integrity sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-focus-within@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz#ac01ce80d3f2e8b2b3eac4ff84f8e15cd0057bc7" + integrity sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-font-variant@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz#efd59b4b7ea8bb06127f2d031bfbb7f24d32fa66" + integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== + +postcss-gap-properties@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz#d5ff0bdf923c06686499ed2b12e125fe64054fed" + integrity sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw== + +postcss-image-set-function@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz#538e94e16716be47f9df0573b56bbaca86e1da53" + integrity sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-lab-function@^7.0.12: + version "7.0.12" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz#eb555ac542607730eb0a87555074e4a5c6eef6e4" + integrity sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +postcss-loader@^7.3.3, postcss-loader@^7.3.4: version "7.3.4" resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.4.tgz#aed9b79ce4ed7e9e89e56199d25ad1ec8f606209" integrity sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A== @@ -6653,6 +9832,13 @@ postcss-loader@^7.3.3: jiti "^1.20.0" semver "^7.5.4" +postcss-logical@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-8.1.0.tgz#4092b16b49e3ecda70c4d8945257da403d167228" + integrity sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA== + dependencies: + postcss-value-parser "^4.2.0" + postcss-merge-idents@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-5.1.1.tgz#7753817c2e0b75d0853b56f78a89771e15ca04a1" @@ -6661,6 +9847,14 @@ postcss-merge-idents@^5.1.1: cssnano-utils "^3.1.0" postcss-value-parser "^4.2.0" +postcss-merge-idents@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz#7b9c31c7bc823c94bec50f297f04e3c2b838ea65" + integrity sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g== + dependencies: + cssnano-utils "^4.0.2" + postcss-value-parser "^4.2.0" + postcss-merge-longhand@^5.1.7: version "5.1.7" resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz#24a1bdf402d9ef0e70f568f39bdc0344d568fb16" @@ -6669,6 +9863,14 @@ postcss-merge-longhand@^5.1.7: postcss-value-parser "^4.2.0" stylehacks "^5.1.1" +postcss-merge-longhand@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz#ba8a8d473617c34a36abbea8dda2b215750a065a" + integrity sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w== + dependencies: + postcss-value-parser "^4.2.0" + stylehacks "^6.1.1" + postcss-merge-rules@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz#2f26fa5cacb75b1402e213789f6766ae5e40313c" @@ -6679,6 +9881,16 @@ postcss-merge-rules@^5.1.4: cssnano-utils "^3.1.0" postcss-selector-parser "^6.0.5" +postcss-merge-rules@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz#7aa539dceddab56019469c0edd7d22b64c3dea9d" + integrity sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ== + dependencies: + browserslist "^4.23.0" + caniuse-api "^3.0.0" + cssnano-utils "^4.0.2" + postcss-selector-parser "^6.0.16" + postcss-minify-font-values@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b" @@ -6686,6 +9898,13 @@ postcss-minify-font-values@^5.1.0: dependencies: postcss-value-parser "^4.2.0" +postcss-minify-font-values@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz#a0e574c02ee3f299be2846369211f3b957ea4c59" + integrity sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg== + dependencies: + postcss-value-parser "^4.2.0" + postcss-minify-gradients@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz#f1fe1b4f498134a5068240c2f25d46fcd236ba2c" @@ -6695,6 +9914,15 @@ postcss-minify-gradients@^5.1.1: cssnano-utils "^3.1.0" postcss-value-parser "^4.2.0" +postcss-minify-gradients@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz#ca3eb55a7bdb48a1e187a55c6377be918743dbd6" + integrity sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q== + dependencies: + colord "^2.9.3" + cssnano-utils "^4.0.2" + postcss-value-parser "^4.2.0" + postcss-minify-params@^5.1.4: version "5.1.4" resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz#c06a6c787128b3208b38c9364cfc40c8aa5d7352" @@ -6704,6 +9932,15 @@ postcss-minify-params@^5.1.4: cssnano-utils "^3.1.0" postcss-value-parser "^4.2.0" +postcss-minify-params@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz#54551dec77b9a45a29c3cb5953bf7325a399ba08" + integrity sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA== + dependencies: + browserslist "^4.23.0" + cssnano-utils "^4.0.2" + postcss-value-parser "^4.2.0" + postcss-minify-selectors@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz#d4e7e6b46147b8117ea9325a915a801d5fe656c6" @@ -6711,11 +9948,23 @@ postcss-minify-selectors@^5.2.1: dependencies: postcss-selector-parser "^6.0.5" +postcss-minify-selectors@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz#197f7d72e6dd19eed47916d575d69dc38b396aff" + integrity sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ== + dependencies: + postcss-selector-parser "^6.0.16" + postcss-modules-extract-imports@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== +postcss-modules-extract-imports@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== + postcss-modules-local-by-default@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.4.tgz#7cbed92abd312b94aaea85b68226d3dec39a14e6" @@ -6725,6 +9974,15 @@ postcss-modules-local-by-default@^4.0.4: postcss-selector-parser "^6.0.2" postcss-value-parser "^4.1.0" +postcss-modules-local-by-default@^4.0.5: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz#d150f43837831dae25e4085596e84f6f5d6ec368" + integrity sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-value-parser "^4.1.0" + postcss-modules-scope@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.1.1.tgz#32cfab55e84887c079a19bbb215e721d683ef134" @@ -6732,6 +9990,13 @@ postcss-modules-scope@^3.1.1: dependencies: postcss-selector-parser "^6.0.4" +postcss-modules-scope@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz#1bbccddcb398f1d7a511e0a2d1d047718af4078c" + integrity sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-modules-values@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" @@ -6739,11 +10004,25 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" +postcss-nesting@^13.0.2: + version "13.0.2" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-13.0.2.tgz#fde0d4df772b76d03b52eccc84372e8d1ca1402e" + integrity sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ== + dependencies: + "@csstools/selector-resolve-nested" "^3.1.0" + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-normalize-charset@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== +postcss-normalize-charset@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz#1ec25c435057a8001dac942942a95ffe66f721e1" + integrity sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ== + postcss-normalize-display-values@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8" @@ -6751,6 +10030,13 @@ postcss-normalize-display-values@^5.1.0: dependencies: postcss-value-parser "^4.2.0" +postcss-normalize-display-values@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz#54f02764fed0b288d5363cbb140d6950dbbdd535" + integrity sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg== + dependencies: + postcss-value-parser "^4.2.0" + postcss-normalize-positions@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz#ef97279d894087b59325b45c47f1e863daefbb92" @@ -6758,6 +10044,13 @@ postcss-normalize-positions@^5.1.1: dependencies: postcss-value-parser "^4.2.0" +postcss-normalize-positions@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz#e982d284ec878b9b819796266f640852dbbb723a" + integrity sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q== + dependencies: + postcss-value-parser "^4.2.0" + postcss-normalize-repeat-style@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz#e9eb96805204f4766df66fd09ed2e13545420fb2" @@ -6765,6 +10058,13 @@ postcss-normalize-repeat-style@^5.1.1: dependencies: postcss-value-parser "^4.2.0" +postcss-normalize-repeat-style@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz#f8006942fd0617c73f049dd8b6201c3a3040ecf3" + integrity sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ== + dependencies: + postcss-value-parser "^4.2.0" + postcss-normalize-string@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228" @@ -6772,6 +10072,13 @@ postcss-normalize-string@^5.1.0: dependencies: postcss-value-parser "^4.2.0" +postcss-normalize-string@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz#e3cc6ad5c95581acd1fc8774b309dd7c06e5e363" + integrity sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ== + dependencies: + postcss-value-parser "^4.2.0" + postcss-normalize-timing-functions@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb" @@ -6779,6 +10086,13 @@ postcss-normalize-timing-functions@^5.1.0: dependencies: postcss-value-parser "^4.2.0" +postcss-normalize-timing-functions@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz#40cb8726cef999de984527cbd9d1db1f3e9062c0" + integrity sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA== + dependencies: + postcss-value-parser "^4.2.0" + postcss-normalize-unicode@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz#f67297fca3fea7f17e0d2caa40769afc487aa030" @@ -6787,12 +10101,27 @@ postcss-normalize-unicode@^5.1.1: browserslist "^4.21.4" postcss-value-parser "^4.2.0" +postcss-normalize-unicode@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz#aaf8bbd34c306e230777e80f7f12a4b7d27ce06e" + integrity sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg== + dependencies: + browserslist "^4.23.0" + postcss-value-parser "^4.2.0" + postcss-normalize-url@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc" integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== dependencies: - normalize-url "^6.0.1" + normalize-url "^6.0.1" + postcss-value-parser "^4.2.0" + +postcss-normalize-url@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz#292792386be51a8de9a454cb7b5c58ae22db0f79" + integrity sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ== + dependencies: postcss-value-parser "^4.2.0" postcss-normalize-whitespace@^5.1.1: @@ -6802,6 +10131,18 @@ postcss-normalize-whitespace@^5.1.1: dependencies: postcss-value-parser "^4.2.0" +postcss-normalize-whitespace@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz#fbb009e6ebd312f8b2efb225c2fcc7cf32b400cd" + integrity sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-opacity-percentage@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz#0b0db5ed5db5670e067044b8030b89c216e1eb0a" + integrity sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ== + postcss-ordered-values@^5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz#b6fd2bd10f937b23d86bc829c69e7732ce76ea38" @@ -6810,6 +10151,117 @@ postcss-ordered-values@^5.1.3: cssnano-utils "^3.1.0" postcss-value-parser "^4.2.0" +postcss-ordered-values@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz#366bb663919707093451ab70c3f99c05672aaae5" + integrity sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q== + dependencies: + cssnano-utils "^4.0.2" + postcss-value-parser "^4.2.0" + +postcss-overflow-shorthand@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz#f5252b4a2ee16c68cd8a9029edb5370c4a9808af" + integrity sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-page-break@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz#7fbf741c233621622b68d435babfb70dd8c1ee5f" + integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== + +postcss-place@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-10.0.0.tgz#ba36ee4786ca401377ced17a39d9050ed772e5a9" + integrity sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-preset-env@^10.2.1: + version "10.6.1" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-10.6.1.tgz#df30cfc54e90af2dcff5f94104e6f272359c9f65" + integrity sha512-yrk74d9EvY+W7+lO9Aj1QmjWY9q5NsKjK2V9drkOPZB/X6KZ0B3igKsHUYakb7oYVhnioWypQX3xGuePf89f3g== + dependencies: + "@csstools/postcss-alpha-function" "^1.0.1" + "@csstools/postcss-cascade-layers" "^5.0.2" + "@csstools/postcss-color-function" "^4.0.12" + "@csstools/postcss-color-function-display-p3-linear" "^1.0.1" + "@csstools/postcss-color-mix-function" "^3.0.12" + "@csstools/postcss-color-mix-variadic-function-arguments" "^1.0.2" + "@csstools/postcss-content-alt-text" "^2.0.8" + "@csstools/postcss-contrast-color-function" "^2.0.12" + "@csstools/postcss-exponential-functions" "^2.0.9" + "@csstools/postcss-font-format-keywords" "^4.0.0" + "@csstools/postcss-gamut-mapping" "^2.0.11" + "@csstools/postcss-gradients-interpolation-method" "^5.0.12" + "@csstools/postcss-hwb-function" "^4.0.12" + "@csstools/postcss-ic-unit" "^4.0.4" + "@csstools/postcss-initial" "^2.0.1" + "@csstools/postcss-is-pseudo-class" "^5.0.3" + "@csstools/postcss-light-dark-function" "^2.0.11" + "@csstools/postcss-logical-float-and-clear" "^3.0.0" + "@csstools/postcss-logical-overflow" "^2.0.0" + "@csstools/postcss-logical-overscroll-behavior" "^2.0.0" + "@csstools/postcss-logical-resize" "^3.0.0" + "@csstools/postcss-logical-viewport-units" "^3.0.4" + "@csstools/postcss-media-minmax" "^2.0.9" + "@csstools/postcss-media-queries-aspect-ratio-number-values" "^3.0.5" + "@csstools/postcss-nested-calc" "^4.0.0" + "@csstools/postcss-normalize-display-values" "^4.0.1" + "@csstools/postcss-oklab-function" "^4.0.12" + "@csstools/postcss-position-area-property" "^1.0.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/postcss-property-rule-prelude-list" "^1.0.0" + "@csstools/postcss-random-function" "^2.0.1" + "@csstools/postcss-relative-color-syntax" "^3.0.12" + "@csstools/postcss-scope-pseudo-class" "^4.0.1" + "@csstools/postcss-sign-functions" "^1.1.4" + "@csstools/postcss-stepped-value-functions" "^4.0.9" + "@csstools/postcss-syntax-descriptor-syntax-production" "^1.0.1" + "@csstools/postcss-system-ui-font-family" "^1.0.0" + "@csstools/postcss-text-decoration-shorthand" "^4.0.3" + "@csstools/postcss-trigonometric-functions" "^4.0.9" + "@csstools/postcss-unset-value" "^4.0.0" + autoprefixer "^10.4.23" + browserslist "^4.28.1" + css-blank-pseudo "^7.0.1" + css-has-pseudo "^7.0.3" + css-prefers-color-scheme "^10.0.0" + cssdb "^8.6.0" + postcss-attribute-case-insensitive "^7.0.1" + postcss-clamp "^4.1.0" + postcss-color-functional-notation "^7.0.12" + postcss-color-hex-alpha "^10.0.0" + postcss-color-rebeccapurple "^10.0.0" + postcss-custom-media "^11.0.6" + postcss-custom-properties "^14.0.6" + postcss-custom-selectors "^8.0.5" + postcss-dir-pseudo-class "^9.0.1" + postcss-double-position-gradients "^6.0.4" + postcss-focus-visible "^10.0.1" + postcss-focus-within "^9.0.1" + postcss-font-variant "^5.0.0" + postcss-gap-properties "^6.0.0" + postcss-image-set-function "^7.0.0" + postcss-lab-function "^7.0.12" + postcss-logical "^8.1.0" + postcss-nesting "^13.0.2" + postcss-opacity-percentage "^3.0.0" + postcss-overflow-shorthand "^6.0.0" + postcss-page-break "^3.0.4" + postcss-place "^10.0.0" + postcss-pseudo-class-any-link "^10.0.1" + postcss-replace-overflow-wrap "^4.0.0" + postcss-selector-not "^8.0.1" + +postcss-pseudo-class-any-link@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz#06455431171bf44b84d79ebaeee9fd1c05946544" + integrity sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-reduce-idents@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-5.2.0.tgz#c89c11336c432ac4b28792f24778859a67dfba95" @@ -6817,6 +10269,13 @@ postcss-reduce-idents@^5.2.0: dependencies: postcss-value-parser "^4.2.0" +postcss-reduce-idents@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz#b0d9c84316d2a547714ebab523ec7d13704cd486" + integrity sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA== + dependencies: + postcss-value-parser "^4.2.0" + postcss-reduce-initial@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz#798cd77b3e033eae7105c18c9d371d989e1382d6" @@ -6825,6 +10284,14 @@ postcss-reduce-initial@^5.1.2: browserslist "^4.21.4" caniuse-api "^3.0.0" +postcss-reduce-initial@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz#4401297d8e35cb6e92c8e9586963e267105586ba" + integrity sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw== + dependencies: + browserslist "^4.23.0" + caniuse-api "^3.0.0" + postcss-reduce-transforms@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9" @@ -6832,6 +10299,33 @@ postcss-reduce-transforms@^5.1.0: dependencies: postcss-value-parser "^4.2.0" +postcss-reduce-transforms@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz#6fa2c586bdc091a7373caeee4be75a0f3e12965d" + integrity sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-replace-overflow-wrap@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz#d2df6bed10b477bf9c52fab28c568b4b29ca4319" + integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== + +postcss-selector-not@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz#f2df9c6ac9f95e9fe4416ca41a957eda16130172" + integrity sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16: + version "6.1.4" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz#fdec4ca80f5781bd216ca9bf89a2a0fccfffa5f0" + integrity sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: version "6.0.15" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz#11cc2b21eebc0b99ea374ffb9887174855a01535" @@ -6840,6 +10334,14 @@ postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^7.0.0: + version "7.1.4" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz#69dc7a526517572ff6b150e352b36a016017b485" + integrity sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-sort-media-queries@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/postcss-sort-media-queries/-/postcss-sort-media-queries-4.4.1.tgz#04a5a78db3921eb78f28a1a781a2e68e65258128" @@ -6847,6 +10349,13 @@ postcss-sort-media-queries@^4.4.1: dependencies: sort-css-media-queries "2.1.0" +postcss-sort-media-queries@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz#4556b3f982ef27d3bac526b99b6c0d3359a6cf97" + integrity sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA== + dependencies: + sort-css-media-queries "2.2.0" + postcss-svgo@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" @@ -6855,6 +10364,14 @@ postcss-svgo@^5.1.0: postcss-value-parser "^4.2.0" svgo "^2.7.0" +postcss-svgo@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-6.0.3.tgz#1d6e180d6df1fa8a3b30b729aaa9161e94f04eaa" + integrity sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g== + dependencies: + postcss-value-parser "^4.2.0" + svgo "^3.2.0" + postcss-unique-selectors@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6" @@ -6862,6 +10379,13 @@ postcss-unique-selectors@^5.1.1: dependencies: postcss-selector-parser "^6.0.5" +postcss-unique-selectors@^6.0.4: + version "6.0.4" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz#983ab308896b4bf3f2baaf2336e14e52c11a2088" + integrity sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg== + dependencies: + postcss-selector-parser "^6.0.16" + postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" @@ -6872,6 +10396,11 @@ postcss-zindex@^5.1.0: resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-5.1.0.tgz#4a5c7e5ff1050bd4c01d95b1847dfdcc58a496ff" integrity sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A== +postcss-zindex@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/postcss-zindex/-/postcss-zindex-6.0.2.tgz#e498304b83a8b165755f53db40e2ea65a99b56e1" + integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg== + postcss@^8.4.17, postcss@^8.4.21, postcss@^8.4.26, postcss@^8.4.33: version "8.4.33" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" @@ -6881,6 +10410,15 @@ postcss@^8.4.17, postcss@^8.4.21, postcss@^8.4.26, postcss@^8.4.33: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.24, postcss@^8.5.4: + version "8.5.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.16.tgz#1230ce0b5df354c24c0ea45f99ce5f6a88279d28" + integrity sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg== + dependencies: + nanoid "^3.3.12" + picocolors "^1.1.1" + source-map-js "^1.2.1" + pretty-error@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-4.0.0.tgz#90a703f46dd7234adb46d0f84823e9d1cb8f10d6" @@ -6964,6 +10502,18 @@ pupa@^3.1.0: dependencies: escape-goat "^4.0.0" +pvtsutils@^1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.6.tgz#ec46e34db7422b9e4fdc5490578c1883657d6001" + integrity sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg== + dependencies: + tslib "^2.8.1" + +pvutils@^1.1.3, pvutils@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.5.tgz#84b0dea4a5d670249aa9800511804ee0b7c2809c" + integrity sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA== + qs@6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" @@ -6971,6 +10521,14 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@~6.15.1: + version "6.15.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.3.tgz#76852132a58ed5c7c0ef67e4441b9bb5d6061b3b" + integrity sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A== + dependencies: + es-define-property "^1.0.1" + side-channel "^1.1.1" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -7015,6 +10573,16 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@~2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2" + integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.4.24" + unpipe "~1.0.0" + rc@1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -7093,15 +10661,26 @@ react-helmet-async@^1.3.0: react-fast-compare "^3.2.0" shallowequal "^1.1.0" +"react-helmet-async@npm:@slorber/react-helmet-async@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz#11fbc6094605cf60aa04a28c17e0aab894b4ecff" + integrity sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A== + dependencies: + "@babel/runtime" "^7.12.5" + invariant "^2.2.4" + prop-types "^15.7.2" + react-fast-compare "^3.2.0" + shallowequal "^1.1.0" + react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-json-view-lite@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-1.2.1.tgz#c59a0bea4ede394db331d482ee02e293d38f8218" - integrity sha512-Itc0g86fytOmKZoIoJyGgvNqohWSbh3NXIKNgH6W6FT9PC1ck4xas1tT3Rr/b3UlFXyA9Jjaw9QSXdZy2JwGMQ== +react-json-view-lite@^2.3.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz#c7ff011c7cc80e9900abc7aa4916c6a5c6d6c1c6" + integrity sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g== react-loadable-ssr-addon-v5-slorber@^1.0.1: version "1.0.1" @@ -7110,6 +10689,28 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1: dependencies: "@babel/runtime" "^7.10.3" +react-loadable-ssr-addon-v5-slorber@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.3.tgz#bb3791bf481222c63a5bc6b96ee23f68cb5614b9" + integrity sha512-GXfh9VLwB5ERaCsU6RULh7tkemeX15aNh6wuMEBtfdyMa7fFG8TXrhXlx1SoEK2Ty/l6XIkzzYIQmyaWW3JgdQ== + dependencies: + "@babel/runtime" "^7.10.3" + +"react-loadable@npm:@docusaurus/react-loadable@5.5.2": + version "5.5.2" + resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce" + integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ== + dependencies: + "@types/react" "*" + prop-types "^15.6.2" + +"react-loadable@npm:@docusaurus/react-loadable@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz#de6c7f73c96542bd70786b8e522d535d69069dc4" + integrity sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ== + dependencies: + "@types/react" "*" + react-router-config@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" @@ -7181,11 +10782,6 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -reading-time@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/reading-time/-/reading-time-1.5.0.tgz#d2a7f1b6057cb2e169beaf87113cc3411b5bc5bb" - integrity sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg== - rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -7200,6 +10796,11 @@ recursive-readdir@^2.2.2: dependencies: minimatch "^3.0.5" +reflect-metadata@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== + regenerate-unicode-properties@^10.1.0: version "10.1.1" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" @@ -7207,6 +10808,13 @@ regenerate-unicode-properties@^10.1.0: dependencies: regenerate "^1.4.2" +regenerate-unicode-properties@^10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz#aa113812ba899b630658c7623466be71e1f86f66" + integrity sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g== + dependencies: + regenerate "^1.4.2" + regenerate@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" @@ -7236,6 +10844,18 @@ regexpu-core@^5.3.1: unicode-match-property-ecmascript "^2.0.0" unicode-match-property-value-ecmascript "^2.1.0" +regexpu-core@^6.3.1: + version "6.4.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.4.0.tgz#3580ce0c4faedef599eccb146612436b62a176e5" + integrity sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.2" + regjsgen "^0.8.0" + regjsparser "^0.13.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.2.1" + registry-auth-token@^5.0.1: version "5.0.2" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-5.0.2.tgz#8b026cc507c8552ebbe06724136267e63302f756" @@ -7250,6 +10870,18 @@ registry-url@^6.0.0: dependencies: rc "1.2.8" +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.13.0: + version "0.13.2" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.13.2.tgz#f654734b5c588b22ba3e21693b30523417180808" + integrity sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ== + dependencies: + jsesc "~3.1.0" + regjsparser@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" @@ -7402,6 +11034,16 @@ resolve@^1.1.6, resolve@^1.14.2: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^1.22.11: + version "1.22.12" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f" + integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + responselike@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/responselike/-/responselike-3.0.0.tgz#20decb6c298aff0dbee1c355ca95461d42823626" @@ -7441,6 +11083,11 @@ rtlcss@^4.1.0: postcss "^8.4.21" strip-json-comments "^3.1.1" +run-applescript@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911" + integrity sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -7468,6 +11115,11 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== +sax@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.6.0.tgz#da59637629307b97e7c4cb28e080a7bc38560d5b" + integrity sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA== + scheduler@^0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" @@ -7475,6 +11127,11 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" +schema-dts@^1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/schema-dts/-/schema-dts-1.1.5.tgz#9237725d305bac3469f02b292a035107595dc324" + integrity sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg== + schema-utils@2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" @@ -7503,6 +11160,16 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.1.0" +schema-utils@^4.0.1, schema-utils@^4.2.0, schema-utils@^4.3.0, schema-utils@^4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.3.3.tgz#5b1850912fa31df90716963d45d9121fdfc09f46" + integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + section-matter@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" @@ -7524,6 +11191,14 @@ selfsigned@^2.1.1: "@types/node-forge" "^1.3.0" node-forge "^1" +selfsigned@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-5.5.0.tgz#4c9ab7c7c9f35f18fb6a9882c253eb0e6bd6557b" + integrity sha512-ftnu3TW4+3eBfLRFnDEkzGxSF/10BJBkaLJuBHZX0kiPS7bRdlpZGu6YGt4KngMkdTwJE6MbjavFpqHvqVt+Ew== + dependencies: + "@peculiar/x509" "^1.14.2" + pkijs "^3.3.3" + semver-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-4.0.0.tgz#3afcf5ed6d62259f5c72d0d5d50dffbdc9680df5" @@ -7562,6 +11237,25 @@ send@0.18.0: range-parser "~1.2.1" statuses "2.0.1" +send@~0.19.0, send@~0.19.1: + version "0.19.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29" + integrity sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "~0.5.2" + http-errors "~2.0.1" + mime "1.6.0" + ms "2.1.3" + on-finished "~2.4.1" + range-parser "~1.2.1" + statuses "~2.0.2" + serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" @@ -7583,6 +11277,19 @@ serve-handler@^6.1.5: path-to-regexp "2.2.1" range-parser "1.2.0" +serve-handler@^6.1.7: + version "6.1.7" + resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.7.tgz#e9bb864e87ee71e8dab874cde44d146b77e3fb78" + integrity sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg== + dependencies: + bytes "3.0.0" + content-disposition "0.5.2" + mime-types "2.1.18" + minimatch "3.1.5" + path-is-inside "1.0.2" + path-to-regexp "3.3.0" + range-parser "1.2.0" + serve-index@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" @@ -7606,6 +11313,16 @@ serve-static@1.15.0: parseurl "~1.3.3" send "0.18.0" +serve-static@~1.16.2: + version "1.16.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.3.tgz#a97b74d955778583f3862a4f0b841eb4d5d78cf9" + integrity sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "~0.19.1" + set-function-length@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.0.tgz#2f81dc6c16c7059bda5ab7c82c11f03a515ed8e1" @@ -7622,7 +11339,7 @@ setprototypeof@1.1.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== -setprototypeof@1.2.0: +setprototypeof@1.2.0, setprototypeof@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== @@ -7656,6 +11373,11 @@ shell-quote@^1.7.3, shell-quote@^1.8.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== +shell-quote@^1.8.4: + version "1.9.0" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.9.0.tgz#e108b1a136586d5964edb3300016d4bedba0fe57" + integrity sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA== + shelljs@^0.8.5: version "0.8.5" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" @@ -7665,6 +11387,35 @@ shelljs@^0.8.5: interpret "^1.0.0" rechoir "^0.6.2" +side-channel-list@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.1.tgz#c2e0b5a14a540aebee3bbc6c3f8666cc9b509127" + integrity sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -7674,6 +11425,17 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" +side-channel@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.1.tgz#ea02c62e05dc4bea67d4442f0fb71ee192f8e0ab" + integrity sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.4" + side-channel-list "^1.0.1" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -7720,6 +11482,14 @@ slash@^4.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== +snake-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" + integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + sockjs@^0.3.24: version "0.3.24" resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" @@ -7734,6 +11504,16 @@ sort-css-media-queries@2.1.0: resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.1.0.tgz#7c85e06f79826baabb232f5560e9745d7a78c4ce" integrity sha512-IeWvo8NkNiY2vVYdPa27MCQiR0MN0M80johAYFVxWWXQ44KU84WNxjslwBHmc/7ZL2ccwkM7/e6S5aiKZXm7jA== +sort-css-media-queries@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz#aa33cf4a08e0225059448b6c40eddbf9f1c8334c" + integrity sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA== + +source-map-js@^1.0.1, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" @@ -7810,11 +11590,21 @@ statuses@2.0.1: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +statuses@~2.0.1, statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + std-env@^3.0.1: version "3.7.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== +std-env@^3.7.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.10.0.tgz#d810b27e3a073047b2b5e40034881f5ea6f9c83b" + integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== + string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -7920,6 +11710,14 @@ stylehacks@^5.1.1: browserslist "^4.21.4" postcss-selector-parser "^6.0.4" +stylehacks@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-6.1.1.tgz#543f91c10d17d00a440430362d419f79c25545a6" + integrity sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg== + dependencies: + browserslist "^4.23.0" + postcss-selector-parser "^6.0.16" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -7964,6 +11762,19 @@ svgo@^2.7.0, svgo@^2.8.0: picocolors "^1.0.0" stable "^0.1.8" +svgo@^3.0.2, svgo@^3.2.0: + version "3.3.3" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.3.3.tgz#8246aee0b08791fde3b0ed22b5661b471fadf58e" + integrity sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng== + dependencies: + commander "^7.2.0" + css-select "^5.1.0" + css-tree "^2.3.1" + css-what "^6.1.0" + csso "^5.0.5" + picocolors "^1.0.0" + sax "^1.5.0" + tapable@^1.0.0: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" @@ -7974,6 +11785,11 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== +tapable@^2.2.1, tapable@^2.3.0, tapable@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160" + integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A== + terser-webpack-plugin@^5.3.7, terser-webpack-plugin@^5.3.9: version "5.3.10" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" @@ -7995,11 +11811,26 @@ terser@^5.10.0, terser@^5.15.1, terser@^5.26.0: commander "^2.20.0" source-map-support "~0.5.20" +terser@^5.31.1: + version "5.48.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.48.0.tgz#8b391171cfbb7ac4a88f9f04ba1cfabc54f643db" + integrity sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.15.0" + commander "^2.20.0" + source-map-support "~0.5.20" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== +thingies@^2.5.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/thingies/-/thingies-2.6.0.tgz#e09b98b9e6f6caf8a759eca8481fea1de974d2b1" + integrity sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg== + thunky@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" @@ -8015,6 +11846,11 @@ tiny-warning@^1.0.0: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tinypool@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.1.1.tgz#059f2d042bd37567fbc017d3d426bdd2a2612591" + integrity sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg== + to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" @@ -8027,7 +11863,7 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toidentifier@1.0.1: +toidentifier@1.0.1, toidentifier@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== @@ -8037,6 +11873,11 @@ totalist@^3.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== +tree-dump@^1.0.3, tree-dump@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.1.0.tgz#ab29129169dc46004414f5a9d4a3c6e89f13e8a4" + integrity sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA== + trim-lines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" @@ -8047,11 +11888,28 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.0, tslib@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tslib@^2.0.3, tslib@^2.4.0, tslib@^2.6.0: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tsyringe@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/tsyringe/-/tsyringe-4.10.0.tgz#d0c95815d584464214060285eaaadd94aa03299c" + integrity sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw== + dependencies: + tslib "^1.9.3" + type-fest@^1.0.1: version "1.4.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" @@ -8110,6 +11968,11 @@ unicode-match-property-value-ecmascript@^2.1.0: resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== +unicode-match-property-value-ecmascript@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz#65a7adfad8574c219890e219285ce4c64ed67eaa" + integrity sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg== + unicode-property-aliases-ecmascript@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" @@ -8206,6 +12069,14 @@ update-browserslist-db@^1.0.13: escalade "^3.1.1" picocolors "^1.0.0" +update-browserslist-db@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" + integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + update-notifier@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-6.0.2.tgz#a6990253dfe6d5a02bd04fbb6a61543f55026b60" @@ -8310,6 +12181,13 @@ watchpack@^2.4.0: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" +watchpack@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.5.2.tgz#e12e82d84674266fc1c6dbfe38891b92ff0522ec" + integrity sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg== + dependencies: + graceful-fs "^4.1.2" + wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" @@ -8322,6 +12200,24 @@ web-namespaces@^2.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== +webpack-bundle-analyzer@^4.10.2: + version "4.10.2" + resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz#633af2862c213730be3dbdf40456db171b60d5bd" + integrity sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw== + dependencies: + "@discoveryjs/json-ext" "0.5.7" + acorn "^8.0.4" + acorn-walk "^8.0.0" + commander "^7.2.0" + debounce "^1.2.1" + escape-string-regexp "^4.0.0" + gzip-size "^6.0.0" + html-escaper "^2.0.2" + opener "^1.5.2" + picocolors "^1.0.0" + sirv "^2.0.3" + ws "^7.3.1" + webpack-bundle-analyzer@^4.9.0: version "4.10.1" resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz#84b7473b630a7b8c21c741f81d8fe4593208b454" @@ -8352,6 +12248,18 @@ webpack-dev-middleware@^5.3.1: range-parser "^1.2.1" schema-utils "^4.0.0" +webpack-dev-middleware@^7.4.2: + version "7.4.5" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-7.4.5.tgz#d4e8720aa29cb03bc158084a94edb4594e3b7ac0" + integrity sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA== + dependencies: + colorette "^2.0.10" + memfs "^4.43.1" + mime-types "^3.0.1" + on-finished "^2.4.1" + range-parser "^1.2.1" + schema-utils "^4.0.0" + webpack-dev-server@^4.15.1: version "4.15.1" resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz#8944b29c12760b3a45bdaa70799b17cb91b03df7" @@ -8388,6 +12296,40 @@ webpack-dev-server@^4.15.1: webpack-dev-middleware "^5.3.1" ws "^8.13.0" +webpack-dev-server@^5.2.2: + version "5.2.6" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-5.2.6.tgz#3a5d41233cbb7504f814d19e59a59173fb8ae23d" + integrity sha512-HNLRmamRvVavZQ+avceZifmv8hmdUjg43t6MI4SqJDwFdW7RPQwH5vzGhDRZSX59SgfbeHhLnq3g+uooWo7pVw== + dependencies: + "@types/bonjour" "^3.5.13" + "@types/connect-history-api-fallback" "^1.5.4" + "@types/express" "^4.17.25" + "@types/express-serve-static-core" "^4.17.21" + "@types/serve-index" "^1.9.4" + "@types/serve-static" "^1.15.5" + "@types/sockjs" "^0.3.36" + "@types/ws" "^8.5.10" + ansi-html-community "^0.0.8" + bonjour-service "^1.2.1" + chokidar "^3.6.0" + colorette "^2.0.10" + compression "^1.8.1" + connect-history-api-fallback "^2.0.0" + express "^4.22.1" + graceful-fs "^4.2.6" + http-proxy-middleware "^2.0.9" + ipaddr.js "^2.1.0" + launch-editor "^2.14.1" + open "^10.0.3" + p-retry "^6.2.0" + schema-utils "^4.2.0" + selfsigned "^5.5.0" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^7.4.2" + ws "^8.18.0" + webpack-merge@^5.9.0: version "5.10.0" resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" @@ -8397,11 +12339,25 @@ webpack-merge@^5.9.0: flat "^5.0.2" wildcard "^2.0.0" +webpack-merge@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-6.0.1.tgz#50c776868e080574725abc5869bd6e4ef0a16c6a" + integrity sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.1" + webpack-sources@^3.2.2, webpack-sources@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== +webpack-sources@^3.5.0: + version "3.5.1" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.5.1.tgz#76c2418486dcc02b2aa0694c104176c2858fe84a" + integrity sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw== + webpack@^5.88.1: version "5.89.0" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc" @@ -8432,6 +12388,34 @@ webpack@^5.88.1: watchpack "^2.4.0" webpack-sources "^3.2.3" +webpack@^5.95.0: + version "5.108.4" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.108.4.tgz#141818a411662773a0bb32dc5536acc5409943b7" + integrity sha512-yur8LyJoeiWh47dErD+Ok7vlbmDsJ3UbbRPAoxbGJ54WpE2y5yVo5G/inUzujnYgw3tPmBRdn+G7PoxXaYC33w== + dependencies: + "@types/estree" "^1.0.8" + "@types/json-schema" "^7.0.15" + "@webassemblyjs/ast" "^1.14.1" + "@webassemblyjs/wasm-edit" "^1.14.1" + "@webassemblyjs/wasm-parser" "^1.14.1" + acorn "^8.16.0" + acorn-import-phases "^1.0.3" + browserslist "^4.28.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.22.2" + es-module-lexer "^2.1.0" + eslint-scope "5.1.1" + events "^3.2.0" + graceful-fs "^4.2.11" + loader-runner "^4.3.2" + mime-db "^1.54.0" + minimizer-webpack-plugin "^5.6.1" + neo-async "^2.6.2" + schema-utils "^4.3.3" + tapable "^2.3.0" + watchpack "^2.5.2" + webpack-sources "^3.5.0" + webpackbar@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-5.0.2.tgz#d3dd466211c73852741dfc842b7556dcbc2b0570" @@ -8442,6 +12426,16 @@ webpackbar@^5.0.2: pretty-time "^1.1.0" std-env "^3.0.1" +webpackbar@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-7.0.0.tgz#7228d32881af2392381b6514499ddea73cdf218a" + integrity sha512-aS9soqSO2iCHgqHoCrj4LbfGQUboDCYJPSFOAchEK+9psIjNrfSWW4Y0YEz67MKURNvMmfo0ycOg9d/+OOf9/Q== + dependencies: + ansis "^3.2.0" + consola "^3.2.3" + pretty-time "^1.1.0" + std-env "^3.7.0" + websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" @@ -8477,7 +12471,7 @@ widest-line@^4.0.1: dependencies: string-width "^5.0.1" -wildcard@^2.0.0: +wildcard@^2.0.0, wildcard@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== @@ -8516,6 +12510,18 @@ ws@^8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.16.0.tgz#d1cd774f36fbc07165066a60e40323eab6446fd4" integrity sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ== +ws@^8.18.0: + version "8.21.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.0.tgz#012e413fc07429945121b0c153158c4343086951" + integrity sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g== + +wsl-utils@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/wsl-utils/-/wsl-utils-0.1.0.tgz#8783d4df671d4d50365be2ee4c71917a0557baab" + integrity sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw== + dependencies: + is-wsl "^3.1.0" + xdg-basedir@^5.0.1, xdg-basedir@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz#1efba19425e73be1bc6f2a6ceb52a3d2c884c0c9" diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 1ba28556..e7ef8fe5 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1,26 +1,25 @@ include: package:solid_lints/analysis_options.yaml -analyzer: - plugins: - - custom_lint +plugins: + solid_lints: -custom_lint: - rules: - - cyclomatic_complexity: +solid_lints: + diagnostics: + cyclomatic_complexity: max_complexity: 4 - - number_of_parameters: + number_of_parameters: max_parameters: 2 - - function_lines_of_code: + function_lines_of_code: max_lines: 50 - - avoid_non_null_assertion - - avoid_late_keyword - - avoid_global_state - - avoid_returning_widgets - - avoid_unnecessary_setstate - - double_literal_format - - avoid_unnecessary_type_assertions - - avoid_debug_print_in_release - - avoid_using_api: + avoid_non_null_assertion: true + avoid_late_keyword: true + avoid_global_state: true + avoid_returning_widgets: true + avoid_unnecessary_setstate: true + double_literal_format: true + avoid_unnecessary_type_assertions: true + avoid_debug_print_in_release: true + avoid_using_api: severity: info entries: - class_name: Future diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0171dfc8..9fe0eae1 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -10,7 +10,6 @@ dependencies: sdk: flutter dev_dependencies: - custom_lint: ^0.8.1 solid_lints: path: ../ test: ^1.25.14 diff --git a/lib/analysis_options.yaml b/lib/analysis_options.yaml index dd2a9c95..f74ed9ed 100644 --- a/lib/analysis_options.yaml +++ b/lib/analysis_options.yaml @@ -1,6 +1,4 @@ analyzer: - plugins: - - custom_lint exclude: # General generated files - "**/*.g.dart" diff --git a/lib/src/common/parameters/excluded_annotations_list_parameter.dart b/lib/src/common/parameters/excluded_annotations_list_parameter.dart index 2f740d92..af899048 100644 --- a/lib/src/common/parameters/excluded_annotations_list_parameter.dart +++ b/lib/src/common/parameters/excluded_annotations_list_parameter.dart @@ -3,6 +3,8 @@ import 'package:analyzer/dart/ast/ast.dart'; /// A parameter model representing excluded annotations for linting. /// It defines class-level annotations that indicate when class members /// should be ignored during analysis. +/// +/// @docType String | List class ExcludedAnnotationsListParameter { /// The set of excluded annotation names. final Set excludedAnnotations; diff --git a/lib/src/common/parameters/excluded_entities_list_parameter.dart b/lib/src/common/parameters/excluded_entities_list_parameter.dart index 4c68b7d0..eb024b33 100644 --- a/lib/src/common/parameters/excluded_entities_list_parameter.dart +++ b/lib/src/common/parameters/excluded_entities_list_parameter.dart @@ -8,6 +8,8 @@ import 'package:analyzer/dart/ast/ast.dart'; /// - extension /// - extension_type /// - enum +/// +/// @docType String | List class ExcludedEntitiesListParameter { /// The parameter model final Set excludedEntityNames; diff --git a/lib/src/common/parameters/excluded_identifiers_list_parameter.dart b/lib/src/common/parameters/excluded_identifiers_list_parameter.dart index 484c49cc..d7ca8244 100644 --- a/lib/src/common/parameters/excluded_identifiers_list_parameter.dart +++ b/lib/src/common/parameters/excluded_identifiers_list_parameter.dart @@ -4,6 +4,8 @@ import 'package:solid_lints/src/common/parameters/excluded_identifier_parameter. /// A model representing "exclude" parameters for linting, defining /// identifiers (classes, methods, functions) to be ignored during analysis. +/// +/// @docType String | Map | List class ExcludedIdentifiersListParameter { /// A list of identifiers (classes, methods, functions) that should be /// excluded from the lint. diff --git a/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart b/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart index 0b288a0d..e1378562 100644 --- a/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart +++ b/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart @@ -14,13 +14,14 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// ### Example config: /// /// ```yaml -/// custom_lint: -/// rules: -/// - avoid_late_keyword: -/// allow_initialized: false -/// ignored_types: -/// - AnimationController -/// - ColorTween +/// plugins: +/// solid_lints: +/// diagnostics: +/// avoid_late_keyword: +/// allow_initialized: false +/// ignored_types: +/// - AnimationController +/// - ColorTween /// ``` /// /// ### Example @@ -47,17 +48,18 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// } /// ``` class AvoidLateKeywordRule extends SolidLintRule { - static const String _lintName = 'avoid_late_keyword'; + /// The lint rule name. Must be public to generate docs. + static const String lintName = 'avoid_late_keyword'; static const LintCode _code = LintCode( - _lintName, + lintName, 'Avoid using the "late" keyword. It may result in runtime exceptions.', ); /// Creates an instance of [AvoidLateKeywordRule]. AvoidLateKeywordRule({required super.analysisOptionsLoader}) : super.withParameters( - name: _lintName, + name: lintName, description: 'Warns against using the late keyword.', parametersParser: AvoidLateKeywordParameters.fromJson, ); diff --git a/lib/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart b/lib/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart index 9623cc4a..8073b70a 100644 --- a/lib/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart +++ b/lib/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart @@ -10,15 +10,15 @@ class AvoidLateKeywordParameters { final bool allowInitialized; /// Types that would be ignored by avoid-late rule - /// /// Example: /// /// ```yaml - /// custom_lint: - /// rules: - /// - avoid_late_keyword: - /// ignored_types: - /// - ColorTween + /// plugins: + /// solid_lints: + /// diagnostics: + /// avoid_late_keyword: + /// ignored_types: + /// - ColorTween /// ``` /// /// ```dart @@ -37,7 +37,8 @@ class AvoidLateKeywordParameters { factory AvoidLateKeywordParameters.fromJson(Map json) => AvoidLateKeywordParameters( allowInitialized: json['allow_initialized'] as bool? ?? false, - ignoredTypes: - List.from(json['ignored_types'] as Iterable? ?? []), + ignoredTypes: List.from( + json['ignored_types'] as Iterable? ?? [], + ), ); } diff --git a/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart b/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart index 171cbc29..70ba36be 100644 --- a/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart +++ b/lib/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart @@ -11,6 +11,18 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// "Bang" operator with Maps is allowed, as [Dart docs](https://dart.dev/null-safety/understanding-null-safety#the-map-index-operator-is-nullable) /// recommend using it for accessing Map values that are known to be present. /// +/// ### Example config: +/// +/// ```yaml +/// plugins: +/// solid_lints: +/// diagnostics: +/// avoid_non_null_assertion: +/// ignored_types: +/// - IMap +/// - BuiltMap +/// ``` +/// /// ### Example /// #### BAD: /// diff --git a/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart b/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart index be2c6ac3..4d1c1487 100644 --- a/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart +++ b/lib/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart @@ -15,6 +15,18 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// /// More details: https://github.com/flutter/flutter/issues/19269 /// +/// ### Example config: +/// +/// ```yaml +/// plugins: +/// solid_lints: +/// diagnostics: +/// avoid_returning_widgets: +/// exclude: +/// - class_name: MyWidget +/// method_name: buildCustomButton +/// ``` +/// /// ### Example /// /// #### BAD: diff --git a/lib/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart b/lib/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart index 4068d342..b8e85433 100644 --- a/lib/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart +++ b/lib/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart @@ -30,8 +30,8 @@ import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/visitors /// ``` /// class AvoidUnnecessaryReturnVariableRule extends AnalysisRule { - /// The name of the lint rule. - static const _lintName = 'avoid_unnecessary_return_variable'; + /// The lint rule name. Must be public to generate docs. + static const lintName = 'avoid_unnecessary_return_variable'; /// The message shown when the lint is triggered. static const String _lintMessage = """ @@ -40,14 +40,14 @@ Rewrite the variable evaluation into return statement instead."""; /// Lint code. static const LintCode _code = LintCode( - _lintName, + lintName, _lintMessage, ); /// Creates a new instance of [AvoidUnnecessaryReturnVariableRule] AvoidUnnecessaryReturnVariableRule() : super( - name: _lintName, + name: lintName, description: _lintMessage, ); diff --git a/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart b/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart index afdf2fdf..7ea8ac1d 100644 --- a/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart +++ b/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart @@ -56,8 +56,8 @@ import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/visitors/avoid_ /// } /// ``` class AvoidUnnecessarySetStateRule extends AnalysisRule { - /// The name of the lint rule. - static const _lintName = 'avoid_unnecessary_setstate'; + /// The lint rule name. Must be public to generate docs. + static const lintName = 'avoid_unnecessary_setstate'; /// The message shown when the lint rule is triggered. static const _lintMessage = 'Avoid calling unnecessary setState. ' @@ -65,14 +65,14 @@ class AvoidUnnecessarySetStateRule extends AnalysisRule { /// The lint code for this rule. static const _code = LintCode( - _lintName, + lintName, _lintMessage, ); /// Creates a new instance of [AvoidUnnecessarySetStateRule]. AvoidUnnecessarySetStateRule() : super( - name: _lintName, + name: lintName, description: _lintMessage, ); diff --git a/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_is_expression_visitor.dart b/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_is_expression_visitor.dart index 7fd7b1d2..5ba406b1 100644 --- a/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_is_expression_visitor.dart +++ b/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_is_expression_visitor.dart @@ -8,7 +8,9 @@ import 'package:solid_lints/src/utils/typecast_utils.dart'; /// /// ### Example: /// {@template solid_lints.avoid_unnecessary_type_assertions.example_is} -/// #### BAD: +/// #### `is` operator +/// +/// ##### BAD: /// ```dart /// final testList = [1.0, 2.0, 3.0]; /// final result = testList is List; // LINT @@ -18,7 +20,7 @@ import 'package:solid_lints/src/utils/typecast_utils.dart'; /// final casted = d is double; // LINT /// ``` /// -/// #### GOOD: +/// ##### GOOD: /// ```dart /// final double? nullableD = 2.0; /// // casting `Type? is Type` is allowed diff --git a/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_where_type_visitor.dart b/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_where_type_visitor.dart index 13bca7f5..6b39c3df 100644 --- a/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_where_type_visitor.dart +++ b/lib/src/lints/avoid_unnecessary_type_assertions/visitors/unnecessary_where_type_visitor.dart @@ -10,13 +10,15 @@ import 'package:solid_lints/src/utils/typecast_utils.dart'; /// /// ### Example: /// {@template solid_lints.avoid_unnecessary_type_assertions.example_where} -/// #### BAD: +/// #### `whereType` method +/// +/// ##### BAD: /// ```dart /// final testList = [1.0, 2.0, 3.0]; /// testList.whereType(); // LINT /// ``` /// -/// #### GOOD: +/// ##### GOOD: /// ```dart /// final dynamicList = [1.0, 2.0]; /// dynamicList.whereType(); diff --git a/lib/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart b/lib/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart index 6bb9c2d3..fa1709b8 100644 --- a/lib/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart +++ b/lib/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart @@ -7,8 +7,8 @@ import 'package:solid_lints/src/lints/avoid_unrelated_type_assertions/visitors/a /// A `avoid_unrelated_type_assertions` rule which /// warns about unnecessary usage of `as` operator class AvoidUnrelatedTypeAssertionsRule extends AnalysisRule { - /// The name of the lint rule. - static const _lintName = 'avoid_unrelated_type_assertions'; + /// The lint rule name. Must be public to generate docs. + static const lintName = 'avoid_unrelated_type_assertions'; /// The message shown when the lint rule is triggered. static const _lintMessage = @@ -16,14 +16,14 @@ class AvoidUnrelatedTypeAssertionsRule extends AnalysisRule { /// Lint code for this rule. static const LintCode _code = LintCode( - _lintName, + lintName, _lintMessage, ); /// Creates a new instance of [AvoidUnrelatedTypeAssertionsRule]. AvoidUnrelatedTypeAssertionsRule() : super( - name: _lintName, + name: lintName, description: _lintMessage, ); diff --git a/lib/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart b/lib/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart index a1858429..32aef04e 100644 --- a/lib/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart +++ b/lib/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule.dart @@ -11,12 +11,19 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Parameters whose names consist only of underscores are also ignored. /// Overridden methods and methods used as tear-offs are skipped. /// -/// ### Parameters +/// ### Example config: /// -/// #### exclude_annotation -/// A list of class annotations (such as `freezed` or `unfreezed`) whose -/// constructor parameters should be ignored by this rule (useful for code -/// generation). +/// ```yaml +/// plugins: +/// solid_lints: +/// diagnostics: +/// avoid_unused_parameters: +/// exclude: +/// - class_name: MyClass +/// method_name: myMethod +/// exclude_annotation: +/// - freezed +/// ``` /// /// {@template solid_lints.avoid_unused_parameters.example} /// ### Example diff --git a/lib/src/lints/avoid_using_api/models/avoid_using_api_entry_parameters.dart b/lib/src/lints/avoid_using_api/models/avoid_using_api_entry_parameters.dart index 03337134..9201cf38 100644 --- a/lib/src/lints/avoid_using_api/models/avoid_using_api_entry_parameters.dart +++ b/lib/src/lints/avoid_using_api/models/avoid_using_api_entry_parameters.dart @@ -32,6 +32,7 @@ import 'package:solid_lints/src/utils/parameter_utils.dart'; /// includes: [] /// excludes: [] /// ``` +/// @docType Map class AvoidUsingApiEntryParameters { /// Variable/method name final String? identifier; diff --git a/lib/src/lints/function_lines_of_code/function_lines_of_code_rule.dart b/lib/src/lints/function_lines_of_code/function_lines_of_code_rule.dart index 49dc0d42..bff7fa5d 100644 --- a/lib/src/lints/function_lines_of_code/function_lines_of_code_rule.dart +++ b/lib/src/lints/function_lines_of_code/function_lines_of_code_rule.dart @@ -17,7 +17,7 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// function_lines_of_code: /// max_lines: 100 /// exclude: -/// - "Build" +/// - build /// ``` class FunctionLinesOfCodeRule extends SolidLintRule { diff --git a/lib/src/lints/member_ordering/models/member_group/member_group.dart b/lib/src/lints/member_ordering/models/member_group/member_group.dart index e561590a..0d77c9bd 100644 --- a/lib/src/lints/member_ordering/models/member_group/member_group.dart +++ b/lib/src/lints/member_ordering/models/member_group/member_group.dart @@ -26,6 +26,8 @@ import 'package:solid_lints/src/lints/member_ordering/models/member_type.dart'; import 'package:solid_lints/src/lints/member_ordering/models/modifier.dart'; /// Abstract class representing class member group +/// +/// @docType String abstract class MemberGroup { /// Member annotation (e.g. override, protected) final Annotation annotation; diff --git a/lib/src/lints/named_parameters_ordering/models/parameter_type.dart b/lib/src/lints/named_parameters_ordering/models/parameter_type.dart index 93b29d47..a72ea2ac 100644 --- a/lib/src/lints/named_parameters_ordering/models/parameter_type.dart +++ b/lib/src/lints/named_parameters_ordering/models/parameter_type.dart @@ -2,6 +2,8 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:collection/collection.dart'; /// Represents a function parameter type +/// +/// @docType String enum ParameterType { /// Inherited (super) parameter type (super.parameterName) inherited('super'), diff --git a/lib/src/lints/newline_before_return/newline_before_return_rule.dart b/lib/src/lints/newline_before_return/newline_before_return_rule.dart index 68e8360e..5614cd62 100644 --- a/lib/src/lints/newline_before_return/newline_before_return_rule.dart +++ b/lib/src/lints/newline_before_return/newline_before_return_rule.dart @@ -73,22 +73,22 @@ import 'package:solid_lints/src/lints/newline_before_return/visitors/newline_bef /// } /// ``` class NewlineBeforeReturnRule extends AnalysisRule { - /// The name of the lint rule. - static const String _lintName = 'newline_before_return'; + /// The lint rule name. Must be public to generate docs. + static const String lintName = 'newline_before_return'; /// The message shown when the lint is triggered. static const String _lintMessage = 'Missing blank line before return.'; /// Lint code for this rule. static const LintCode _code = LintCode( - _lintName, + lintName, _lintMessage, ); /// Creates a new instance of [NewlineBeforeReturnRule]. NewlineBeforeReturnRule() : super( - name: _lintName, + name: lintName, description: _lintMessage, ); diff --git a/lib/src/lints/no_equal_then_else/no_equal_then_else_rule.dart b/lib/src/lints/no_equal_then_else/no_equal_then_else_rule.dart index 8099a2c8..80bd0acc 100644 --- a/lib/src/lints/no_equal_then_else/no_equal_then_else_rule.dart +++ b/lib/src/lints/no_equal_then_else/no_equal_then_else_rule.dart @@ -42,22 +42,22 @@ import 'package:solid_lints/src/lints/no_equal_then_else/visitors/no_equal_then_ /// selectedValue = condition ? valueA : valueB; /// ``` class NoEqualThenElseRule extends AnalysisRule { - /// The name of the lint rule. - static const String _lintName = 'no_equal_then_else'; + /// The lint rule name. Must be public to generate docs. + static const String lintName = 'no_equal_then_else'; /// The message shown when the lint is triggered. static const String _lintMessage = 'Then and else branches are equal.'; /// Lint code static const _code = LintCode( - _lintName, + lintName, _lintMessage, ); /// Create a new instance of [NoEqualThenElseRule] NoEqualThenElseRule() : super( - name: _lintName, + name: lintName, description: _lintMessage, ); diff --git a/lib/src/utils/docs_parser/main.dart b/lib/src/utils/docs_parser/main.dart index 3f087e0f..abf91055 100644 --- a/lib/src/utils/docs_parser/main.dart +++ b/lib/src/utils/docs_parser/main.dart @@ -2,22 +2,17 @@ import 'dart:io'; import 'package:path/path.dart'; import 'package:solid_lints/src/utils/docs_parser/output_formatters/markdown_formatter.dart'; -import 'package:solid_lints/src/utils/docs_parser/parser_utils.dart'; -import 'package:solid_lints/src/utils/docs_parser/parsers/rule_parser.dart'; +import 'package:solid_lints/src/utils/docs_parser/parsers/docs_parser.dart'; void main() async { final dir = - Directory(normalize(join(Directory.current.path, 'lib', 'lints'))); + Directory(normalize(join(Directory.current.path, 'lib', 'src', 'lints'))); - final ruleFiles = ParserUtils.findRuleFiles(dir); - final rulesDocs = ruleFiles - .map(RuleParser.new) - .map((parser) => parser.parse()) - .toList(growable: false); + final parser = DocsParser( + formatter: MarkdownFormatter(), + ruleFileSuffixes: const ['rule', 'metric'], + ); - // sort rules alphabetically by names - rulesDocs.sort((a, b) => a.name.compareTo(b.name)); - - final formatter = MarkdownFormatter(); - File('DOCUMENTATION.md').writeAsStringSync(formatter.format(rulesDocs)); + final result = parser.parse(dir); + File('DOCUMENTATION.md').writeAsStringSync(result); } diff --git a/lib/src/utils/docs_parser/output_formatters/markdown_formatter.dart b/lib/src/utils/docs_parser/output_formatters/markdown_formatter.dart index a679addd..64442503 100644 --- a/lib/src/utils/docs_parser/output_formatters/markdown_formatter.dart +++ b/lib/src/utils/docs_parser/output_formatters/markdown_formatter.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:solid_lints/src/utils/docs_parser/models/rule_doc.dart'; import 'package:solid_lints/src/utils/docs_parser/output_formatters/rules_documentation_formatter.dart'; +import 'package:solid_lints/src/utils/docs_parser/utils/parser_utils.dart'; /// Markdown output formatter class MarkdownFormatter implements RulesDocumentationFormatter { @@ -24,7 +25,7 @@ class MarkdownFormatter implements RulesDocumentationFormatter { ) .join('\n'); - /// + /// Format a single rule to Markdown string. String formatRuleToMarkdown( RuleDoc rule, { bool includeName = true, @@ -36,7 +37,7 @@ class MarkdownFormatter implements RulesDocumentationFormatter { formattedString.writeln('## ${rule.name}'); } - formattedString.writeln(rule.doc); + formattedString.writeln(ParserUtils.escapeMdx(rule.doc)); if (rule.parameters.isNotEmpty) { formattedString.writeln('### Parameters'); @@ -48,10 +49,11 @@ class MarkdownFormatter implements RulesDocumentationFormatter { ' (_${parameter.type}_) ', ), ); - formattedString.writeln(' ${parameter.doc}'); + formattedString.writeln(' ${ParserUtils.escapeMdx(parameter.doc)}'); } } return formattedString.toString(); } + } diff --git a/lib/src/utils/docs_parser/parser_utils.dart b/lib/src/utils/docs_parser/parser_utils.dart deleted file mode 100644 index 509b22ff..00000000 --- a/lib/src/utils/docs_parser/parser_utils.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:io'; - -import 'package:analyzer/dart/ast/ast.dart'; - -/// Utils used by parsers -class ParserUtils { - static const _ruleFileSuffixes = ['rule', 'metric']; - - /// Recursively get files dart with the `rule` suffix in the given [dir] - static List findRuleFiles(Directory dir) { - final rulesPaths = []; - for (final entity in dir.listSync()) { - if (entity is File) { - if (_ruleFileSuffixes.contains(fileNameSuffix(entity.uri))) { - rulesPaths.add(entity.path); - } - } else if (entity is Directory) { - rulesPaths.addAll(findRuleFiles(entity)); - } - } - - return rulesPaths; - } - - /// Format [documentationComment] - static String? formatDocumentationComment(Comment? documentationComment) => - documentationComment?.childEntities - .join('\n') - .replaceAll('///', '') - .trim(); - - /// Convert camelCase string to snake_case - static String camelCaseToSnakeCase(String camelCaseString) { - final exp = RegExp('(?<=[a-z])[A-Z]'); - - return camelCaseString - .replaceAllMapped(exp, (Match m) => '_${m.group(0)}') - .toLowerCase(); - } - - /// Get the dart file name suffix - static String fileNameSuffix(Uri uri) => - uri.pathSegments.last.replaceFirst('.dart', '').split('_').last; -} diff --git a/lib/src/utils/docs_parser/parsers/base_parser.dart b/lib/src/utils/docs_parser/parsers/base_parser.dart deleted file mode 100644 index 5f82cdc9..00000000 --- a/lib/src/utils/docs_parser/parsers/base_parser.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'package:analyzer/dart/ast/ast.dart'; - -/// -abstract class BaseParser { - /// - const BaseParser(); - - /// Format [documentationComment] - String? formatDocumentationComment(Comment? documentationComment) => - documentationComment?.childEntities - .join('\n') - .replaceAll('///', '') - .trim(); - - /// Convert camelCase string to snake_case - String camelCaseToSnakeCase(String camelCaseString) => camelCaseString - .replaceAllMapped( - RegExp('(?<=[a-z])[A-Z]'), - (Match m) => '_${m.group(0)}', - ) - .toLowerCase(); - - /// Get the dart file name suffix - String fileNameSuffix(Uri uri) => - uri.pathSegments.last.replaceFirst('.dart', '').split('_').last; -} diff --git a/lib/src/utils/docs_parser/parsers/docs_parser.dart b/lib/src/utils/docs_parser/parsers/docs_parser.dart index 53414249..810e7617 100644 --- a/lib/src/utils/docs_parser/parsers/docs_parser.dart +++ b/lib/src/utils/docs_parser/parsers/docs_parser.dart @@ -1,12 +1,14 @@ import 'dart:developer'; import 'dart:io'; +import 'package:analyzer/dart/ast/ast.dart'; import 'package:solid_lints/src/utils/docs_parser/output_formatters/rules_documentation_formatter.dart'; -import 'package:solid_lints/src/utils/docs_parser/parsers/base_parser.dart'; import 'package:solid_lints/src/utils/docs_parser/parsers/rule_parser.dart'; +import 'package:solid_lints/src/utils/docs_parser/utils/parser_extensions.dart'; +import 'package:solid_lints/src/utils/docs_parser/utils/parser_utils.dart'; -/// -class DocsParser extends BaseParser { +/// DocsParser orchestration class +class DocsParser { /// final List ruleFileSuffixes; @@ -21,35 +23,48 @@ class DocsParser extends BaseParser { /// T parse(Directory dir, {bool sortRulesAlphabetically = true}) { - final rulesDocs = _findRuleFiles(dir) - .map(RuleParser.new) - .map((parser) => parser.parse()) - .toList(growable: false); + final files = dir + .listSync(recursive: true) + .whereType() + .where( + (f) => ruleFileSuffixes.contains(ParserUtils.fileNameSuffix(f.uri)), + ) + .map((file) => file.path) + .toList(); - if (rulesDocs.isEmpty) { - throw 'Found no rules in specified directory'; - } - log('Parsed ${rulesDocs.length} rules'); + if (files.isEmpty) throw 'Found no rules in specified directory'; - if (sortRulesAlphabetically) { - rulesDocs.sort((a, b) => a.name.compareTo(b.name)); - } + final libDir = ParserUtils.findLibDir(dir); - return formatter.format(rulesDocs); - } + final customTypes = { + 'DiagnosticSeverity': 'String', + for (final entity in libDir.listSync(recursive: true)) + if (entity case File(hasDocStrings: true, :final declarations?)) + for (final declaration in declarations) + if (declaration case CompilationUnitMember( + :final name?, + :final type?, + )) + name: type.trim(), + }; - List _findRuleFiles(Directory dir) { - final rulesPaths = []; - for (final entity in dir.listSync()) { - if (entity is File) { - if (ruleFileSuffixes.contains(fileNameSuffix(entity.uri))) { - rulesPaths.add(entity.path); - } - } else if (entity is Directory) { - rulesPaths.addAll(_findRuleFiles(entity)); - } + final templates = ParserUtils.scanForTemplates(libDir); + + final docs = [ + for (final path in files) + RuleParser( + rulePath: path, + customTypes: customTypes, + templates: templates, + ).parse(), + ]; + + log('Parsed ${docs.length} rules'); + + if (sortRulesAlphabetically) { + docs.sort((a, b) => a.name.compareTo(b.name)); } - return rulesPaths; + return formatter.format(docs); } } diff --git a/lib/src/utils/docs_parser/parsers/parameters_parser.dart b/lib/src/utils/docs_parser/parsers/parameters_parser.dart index a3d97da0..f339e841 100644 --- a/lib/src/utils/docs_parser/parsers/parameters_parser.dart +++ b/lib/src/utils/docs_parser/parsers/parameters_parser.dart @@ -1,85 +1,65 @@ import 'dart:io'; -import 'package:analyzer/dart/analysis/features.dart'; -import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:collection/collection.dart'; import 'package:path/path.dart'; import 'package:solid_lints/src/utils/docs_parser/models/parameter_doc.dart'; -import 'package:solid_lints/src/utils/docs_parser/parsers/base_parser.dart'; +import 'package:solid_lints/src/utils/docs_parser/utils/parser_extensions.dart'; +import 'package:solid_lints/src/utils/docs_parser/utils/parser_regexes.dart'; +import 'package:solid_lints/src/utils/docs_parser/utils/parser_utils.dart'; -/// -class ParametersParser extends BaseParser { +/// ParametersParser class to parse parameters of rules +class ParametersParser { static const _parametersDir = 'models'; static const _parametersSuffix = 'parameters'; /// Directory containing rule file final Directory ruleDirectory; - /// - const ParametersParser({required this.ruleDirectory}); + /// Global map of custom types to their primitives + final Map customTypes; /// - List parse() { - final parametersPath = _getParametersFilePath(); - if (parametersPath == null) return []; - - final ast = parseFile( - path: parametersPath, - featureSet: FeatureSet.latestLanguageVersion(), - ); - - final parameterDocs = ast.unit.declarations - .whereType() - .map(_parseParametersDocs) - .firstWhereOrNull((docs) => docs.isNotEmpty) ?? - []; - - return parameterDocs; - } - - List _parseParametersDocs(ClassDeclaration declaration) { - final List parameterDocs = []; - - for (final member in declaration.members) { - if (member is FieldDeclaration) { - final variable = member.fields.variables.first.name; - final variableName = variable.lexeme.trim(); - if (variableName.startsWith('_')) continue; + const ParametersParser({ + required this.ruleDirectory, + required this.customTypes, + }); - final name = camelCaseToSnakeCase(variableName); - final type = member.fields.type.toString(); - final doc = formatDocumentationComment( - member.documentationComment, - ); - - if (doc == null) { - throw 'Documentation is not specified for class: ' - '${declaration.name.stringValue}'; - } - - parameterDocs.add( - ParameterDoc( - name: name, - type: type, - doc: doc, - ), - ); - } - } - - return parameterDocs; - } - - String? _getParametersFilePath() { - final dir = Directory(join(ruleDirectory.path, _parametersDir)); - if (!dir.existsSync()) return null; - - return dir - .listSync() - .firstWhereOrNull( - (entity) => fileNameSuffix(entity.uri) == _parametersSuffix, - ) - ?.path; - } + /// + List parse() => + File( + join( + ruleDirectory.path, + _parametersDir, + '${basename(ruleDirectory.path)}_$_parametersSuffix.dart', + ), + ).declarations + ?.whereType() + .map(_parseParametersDocs) + .firstWhereOrNull((docs) => docs.isNotEmpty) ?? + []; + + List _parseParametersDocs(ClassDeclaration declaration) => [ + for (final member in ParserUtils.getClassMembers(declaration)) + if (member case FieldDeclaration( + isStatic: false, + :final documentationComment, + )) + for (final v in member.fields.variables) + if (!v.nameString.startsWith('_')) + if (documentationComment?.formatted case final doc?) + ParameterDoc( + name: ParserUtils.camelCaseToSnakeCase(v.nameString), + type: (member.fields.type?.toSource() ?? 'dynamic') + .replaceAllMapped(ParserRegexes.wordRegex, (match) { + final word = match.group(0)!; + return customTypes[word] ?? word; + }), + doc: doc, + ) + else + throw 'Documentation is not specified for field ' + '"${v.nameString}" in class: ' + '${ParserUtils.getDeclarationName(declaration)}', + ]; } diff --git a/lib/src/utils/docs_parser/parsers/rule_parser.dart b/lib/src/utils/docs_parser/parsers/rule_parser.dart index ab73c57e..03398a9d 100644 --- a/lib/src/utils/docs_parser/parsers/rule_parser.dart +++ b/lib/src/utils/docs_parser/parsers/rule_parser.dart @@ -1,51 +1,59 @@ import 'dart:io'; -import 'package:analyzer/dart/analysis/features.dart'; -import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:collection/collection.dart'; import 'package:solid_lints/src/utils/docs_parser/models/rule_doc.dart'; -import 'package:solid_lints/src/utils/docs_parser/parsers/base_parser.dart'; import 'package:solid_lints/src/utils/docs_parser/parsers/parameters_parser.dart'; +import 'package:solid_lints/src/utils/docs_parser/utils/parser_extensions.dart'; +import 'package:solid_lints/src/utils/docs_parser/utils/parser_utils.dart'; -/// -class RuleParser extends BaseParser { - static const _lintNameVariable = 'lintName'; - +/// RuleParser class to parse lint rules +class RuleParser { /// Path to the rule file final String rulePath; + /// Global map of custom types to their primitives + final Map customTypes; + + /// Global map of template definitions + final Map templates; + /// [RuleParser] constructor - const RuleParser(this.rulePath); + const RuleParser({ + required this.rulePath, + required this.customTypes, + required this.templates, + }); /// RuleDoc parse() { - final ast = parseFile( - path: rulePath, - featureSet: FeatureSet.latestLanguageVersion(), - ); - final declaration = - ast.unit.declarations.whereType().firstWhereOrNull( - (declaration) => - declaration.documentationComment?.childEntities.isNotEmpty ?? - false, - ); + final ast = ParserUtils.parseAst(rulePath); - if (declaration == null) { - throw 'Rule at the path "$rulePath" does not have documentation string'; + ClassDeclaration? ruleClass; + String? name; + + for (final declaration in ast.declarations.whereType()) { + final parsedName = ParserUtils.getLintName(declaration); + if (parsedName != null) { + ruleClass = declaration; + name = parsedName; + break; + } } - final name = _parseClassName(declaration); - final doc = formatDocumentationComment( - declaration.documentationComment, - ); + if (ruleClass == null || name == null) { + throw 'Rule class with "lintName" not found in "$rulePath".'; + } - if (name == null || doc == null) { - throw 'Rule at the path "$rulePath" has invalid format.'; + var doc = ruleClass.documentationComment.formatted; + if (doc == null) { + throw 'Rule at the path "$rulePath" does not have documentation string'; } + doc = ParserUtils.expandMacros(doc, templates, source: rulePath); + final parameters = ParametersParser( ruleDirectory: File(rulePath).parent, + customTypes: customTypes, ).parse(); return RuleDoc( @@ -54,20 +62,4 @@ class RuleParser extends BaseParser { parameters: parameters, ); } - - String? _parseClassName(ClassDeclaration classDeclaration) { - for (final member in classDeclaration.members) { - if (member is FieldDeclaration && - member.isStatic && - member.fields.variables.first.name.lexeme == _lintNameVariable) { - final variableName = member.fields.variables.first.initializer; - - if (variableName is StringLiteral) { - return variableName.stringValue; - } - } - } - - return null; - } } diff --git a/lib/src/utils/docs_parser/utils/parser_extensions.dart b/lib/src/utils/docs_parser/utils/parser_extensions.dart new file mode 100644 index 00000000..1a22cbc7 --- /dev/null +++ b/lib/src/utils/docs_parser/utils/parser_extensions.dart @@ -0,0 +1,51 @@ +import 'dart:io'; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:solid_lints/src/utils/docs_parser/utils/parser_regexes.dart'; +import 'package:solid_lints/src/utils/docs_parser/utils/parser_utils.dart'; + +/// Extension on [File] to help with docs parsing. +extension FileDocsExtension on File { + /// Whether the file contains document types annotation. + bool get hasDocStrings => + path.endsWith('.dart') && readAsStringSync().contains('@docType'); + + /// The compilation unit declarations of the file. + NodeList? get declarations => + _tryOrNull(() => ParserUtils.parseAst(path).declarations); +} + +/// Extension on [CompilationUnitMember] to help with docs parsing. +extension CompilationUnitMemberDocsExtension on CompilationUnitMember { + /// The name of the declaration. + String? get name => + _tryOrNull(() => ParserUtils.getDeclarationName(this)); + + /// The formatted documentation comment. + String? get doc => documentationComment.formatted; + + /// The type corresponding to the documentation comment. + String? get type => doc == null + ? null + : ParserRegexes.docTypeRegex.firstMatch(doc ?? '')?.group(1); +} + +T? _tryOrNull(T Function() f) { + try { + return f(); + } catch (_) { + return null; + } +} + +/// Extension on [Comment] to format it easily. +extension CommentExtension on Comment? { + /// Format the documentation comment. + String? get formatted => ParserUtils.formatDocumentationComment(this); +} + +/// Extension on [VariableDeclaration] to clean up lexeme names. +extension VariableDeclarationExtension on VariableDeclaration { + /// The lexeme name of the variable, trimmed. + String get nameString => name.lexeme.trim(); +} diff --git a/lib/src/utils/docs_parser/utils/parser_regexes.dart b/lib/src/utils/docs_parser/utils/parser_regexes.dart new file mode 100644 index 00000000..95622283 --- /dev/null +++ b/lib/src/utils/docs_parser/utils/parser_regexes.dart @@ -0,0 +1,113 @@ +/// Regular expressions used by documentation parsers. +class ParserRegexes { + ParserRegexes._(); + + /// Pattern matching valid template/macro names. + static const namePattern = '[a-zA-Z0-9_.-]+'; + + /// Regex to find @docType annotations in doc comments. + /// + /// ### Example match: + /// `/// @docType rule` -> captures `rule` + /// + /// ``` + /// Regex Fragment | Meaning + /// ============================================================== + /// @docType | Match literal '@docType' + /// \s+ | Match one or more spaces + /// ( ) | Capture group 1: match... + /// .+ | ... one or more of any character + /// $ | Match the end of the line + /// ``` + static final docTypeRegex = RegExp(r'@docType\s+(.+)$', multiLine: true); + + /// Regex to match words/tokens in types. + /// + /// ### Example match: + /// `List` -> matches `List` and `double` + /// + /// ``` + /// Regex Fragment | Meaning + /// ============================================================== + /// \b | Match word boundary + /// \w+ | Match one or more word characters + /// \b | Match word boundary + /// ``` + static final wordRegex = RegExp(r'\b\w+\b'); + + /// Regex to find uppercase letters preceded by a lowercase letter + /// (for camelCase). + /// + /// ### Example match: + /// `camelCase` -> matches `C` + /// + /// ``` + /// Regex Fragment | Meaning + /// ============================================================== + /// (?<= ) | Assert preceding matches... + /// [a-z] | ... a lowercase letter + /// [A-Z] | Match an uppercase letter + /// ``` + static final camelCaseRegex = RegExp('(?<=[a-z])[A-Z]'); + + /// Regex to find `{@template}` blocks and capture the name + /// and raw content. + /// + /// ### Example match: + /// `/// {@template my_name}` + /// `/// content` + /// `/// {@endtemplate}` + /// -> captures name `my_name` and body `/// content` + /// + /// ``` + /// Regex Fragment | Meaning + /// ====================================================================== + /// {@template | Match literal '{@template' + /// \s+ | Match one or more spaces + /// ( ) | Capture group 1: template name + /// namePattern | ... valid name characters + /// \} | Match literal closing brace '}' + /// ( ) | Capture group 2: body content + /// [\s\S]*? | ... matches any chars lazily + /// {@endtemplate\} | Match literal closing tag + /// ``` + static final templateRegex = RegExp( + '{@template\\s+($namePattern)\\}([\\s\\S]*?){@endtemplate\\}', + ); + + /// Regex to match `{@macro}` invocations and capture the template name. + /// + /// ### Example match: + /// `{@macro my_name}` -> captures `my_name` + /// + /// ``` + /// Regex Fragment | Meaning + /// ====================================================================== + /// {@macro | Match literal '{@macro' + /// \s+ | Match one or more spaces + /// ( ) | Capture group 1: template name + /// namePattern | ... valid name characters + /// \} | Match literal closing brace '}' + /// ``` + static final macroRegex = RegExp('{@macro\\s+($namePattern)\\}'); + + /// Regex to match either template start or template end tags + /// to strip them. + /// + /// ### Example match: + /// Matches `{@template my_name}` and `{@endtemplate}` + /// + /// ``` + /// Regex Fragment | Meaning + /// ====================================================================== + /// {@template | Match literal '{@template' + /// \s+ | Match one or more spaces + /// namePattern | Match valid name characters + /// \} | Match literal closing brace '}' + /// | | OR + /// {@endtemplate\} | Match literal closing tag + /// ``` + static final templateTagRegex = RegExp( + '{@template\\s+$namePattern\\}|{@endtemplate\\}', + ); +} diff --git a/lib/src/utils/docs_parser/utils/parser_utils.dart b/lib/src/utils/docs_parser/utils/parser_utils.dart new file mode 100644 index 00000000..a1fac705 --- /dev/null +++ b/lib/src/utils/docs_parser/utils/parser_utils.dart @@ -0,0 +1,166 @@ +import 'dart:io'; + +import 'package:analyzer/dart/analysis/features.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as p; + +import 'package:solid_lints/src/utils/docs_parser/utils/parser_regexes.dart'; +import 'package:solid_lints/src/utils/file_utils.dart'; + +/// Utils used by parsers +class ParserUtils { + static const _maxMacroDepth = 5; + static const _lintNameVariable = 'lintName'; + + ParserUtils._(); + + /// Parse a Dart file and return its [CompilationUnit] + static CompilationUnit parseAst(String path) => parseFile( + path: path, + featureSet: FeatureSet.latestLanguageVersion(), + ).unit; + + static String _cleanCommentLine(String line) => line.substring(switch (line) { + _ when line.startsWith('/// ') => 4, + _ when line.startsWith('///') => 3, + _ => 0, + }); + + /// Format [documentationComment] + static String? formatDocumentationComment(Comment? documentationComment) => + documentationComment?.tokens + .map((token) => _cleanCommentLine(token.lexeme)) + .join('\n') + .trim(); + + /// Scan the codebase directory for all `{@template}` definitions + static Map scanForTemplates(Directory libDir) => { + for (final entity in libDir.listSync(recursive: true)) + if (entity case File() when entity.path.endsWith('.dart')) + if (entity.tryReadAsStringSync() case final content? + when content.contains('{@template')) + for (final match in ParserRegexes.templateRegex.allMatches(content)) + if (match.groups([1, 2]) case [ + final String name, + final String rawContent, + ]) + name: rawContent + .split('\n') + .map((line) => _cleanCommentLine(line.trim())) + .join('\n') + .trim(), + }; + + /// Expand `{@macro}` references in the formatted documentation comment and + /// strip template tags + static String expandMacros( + String text, + Map templates, { + String? source, + }) { + var expanded = text; + + if (expanded.contains('{@macro')) { + for (var i = 0; i < _maxMacroDepth; i++) { + final newExpanded = expanded.replaceAllMapped( + ParserRegexes.macroRegex, + (match) { + final name = match.group(1)!; + final template = templates[name]; + if (template == null) { + final sourceSuffix = source != null ? ' in $source' : ''; + throw 'Macro reference to non-existent template: ' + '"$name"$sourceSuffix'; + } + return template; + }, + ); + + if (newExpanded == expanded) break; + + expanded = newExpanded; + + if (i == _maxMacroDepth - 1 && expanded.contains('{@macro')) { + final sourceSuffix = source != null ? ' in $source' : ''; + throw 'Circular macro dependency detected or macro expansion depth ' + 'exceeded the limit of $_maxMacroDepth$sourceSuffix'; + } + } + } + + expanded = expanded.replaceAll(ParserRegexes.templateTagRegex, ''); + + return expanded.trim(); + } + + /// Convert camelCase string to snake_case + static String camelCaseToSnakeCase(String camelCaseString) => camelCaseString + .replaceAllMapped( + ParserRegexes.camelCaseRegex, + (Match m) => '_${m.group(0)}', + ) + .toLowerCase(); + + /// Get the dart file name suffix + static String fileNameSuffix(Uri uri) => uri.pathSegments.last + .replaceFirst(RegExp(r'\.dart$'), '') + .split('_') + .last; + + /// Safely get the name of a ClassDeclaration or EnumDeclaration + static String? getDeclarationName(CompilationUnitMember declaration) => + switch (declaration) { + ClassDeclaration() => declaration.namePart.typeName.lexeme, + EnumDeclaration() => declaration.namePart.typeName.lexeme, + _ => null, + }; + + /// Safely get members of a ClassDeclaration + static List getClassMembers(ClassDeclaration declaration) { + final body = declaration.body; + return body is BlockClassBody ? body.members : []; + } + + /// Safely get the value of static const lintName from a [ClassDeclaration] + static String? getLintName(ClassDeclaration classDeclaration) { + final initializer = getClassMembers(classDeclaration) + .whereType() + .where((member) => member.isStatic) + .expand((member) => member.fields.variables) + .firstWhereOrNull((v) => v.name.lexeme == _lintNameVariable) + ?.initializer; + + return initializer is StringLiteral ? initializer.stringValue : null; + } + + /// Find the `lib` directory by traversing up from the given [startDir] + static Directory findLibDir(Directory startDir) { + var current = startDir.absolute; + while (current.path != current.parent.path) { + if (p.basename(current.path) == 'lib') { + return current; + } + current = current.parent; + } + final localLib = Directory(p.join(Directory.current.path, 'lib')); + if (localLib.existsSync()) return localLib; + return startDir; + } + + /// Escape angle brackets `<` and `>` in plain text (outside of code blocks) + /// to make the text safe for Docusaurus MDX parser. + static String escapeMdx(String text) => text + .split('`') + .mapIndexed( + (i, p) => i.isOdd + ? p + : p + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('{', r'\{') + .replaceAll('}', r'\}'), + ) + .join('`'); +} diff --git a/lib/src/utils/file_utils.dart b/lib/src/utils/file_utils.dart new file mode 100644 index 00000000..e345a511 --- /dev/null +++ b/lib/src/utils/file_utils.dart @@ -0,0 +1,14 @@ +import 'dart:io'; + +/// Extension on [File] that provides safe, exception-handling methods. +extension FileUtils on File { + /// Tries to read the file content as a string. Returns `null` if any + /// exception (such as [FileSystemException] or [FormatException]) is thrown. + String? tryReadAsStringSync() { + try { + return readAsStringSync(); + } catch (_) { + return null; + } + } +} From afd181c49ffb999b02300c9e4234baed1be92ce2 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 17 Jul 2026 16:35:44 +0300 Subject: [PATCH 43/49] fix: support package-level analysis options and rule suppression (#318) * fix: support package-level analysis options and rule suppression * refactor: improve analysis options parsing robustness with type standardization and fix path resolution logic * refactor: implement file-change detection for package configs and support multiple analysis option includes * fix: improve robustness of configuration parsing and URI resolution to prevent crashes on malformed input * refactor: replace constants file with a consolidated SolidLintsConstants class * refactor: update AnalysisOptionsLoader to use a private constructor and factory pattern for cleaner dependency injection * refactor: remove explicit generic type from _withNearestAnalysisOptionsFilePathForContext call * refactor: simplify package URI parsing using pattern matching in package config resolver * refactor: simplify package config path resolution loop logic * refactor: simplify package config parsing using pattern matching and extract uri resolution logic * refactor: simplify _toStandardMap using map entry pattern matching * refactor: simplify rule merging logic in analysis options parser using spread operators * refactor: simplify loop iterations using map entry pattern matching in analysis options parser * refactor: simplify map merging logic in analysis options parser using spread operator and pattern matching * refactor: simplify diagnostic configuration extraction by iterating over potential plugin source locations * refactor: introduce MapExtensions for type-safe filtering and refactor analysis options parser to utilize new utilities --------- Co-authored-by: Illia Aihistov --- lib/main.dart | 14 +- .../analysis_options_loader.dart | 88 ++--- .../analysis_options_parser.dart | 231 +++++++++++++ .../cached_package_config.dart | 14 + .../cached_package_rules.dart | 4 + .../package_config_resolver.dart | 125 +++++++ .../common/parameter_parser/rules_data.dart | 16 + lib/src/common/solid_lints_constants.dart | 5 + .../models/avoid_using_api_parameters.dart | 15 +- lib/src/models/proxy_analysis_rule.dart | 54 +++ lib/src/models/proxy_multi_analysis_rule.dart | 54 +++ lib/src/utils/map_utils.dart | 16 + pubspec.yaml | 5 + .../analysis_options_loader_test.dart | 326 ++++++++++++++++++ .../avoid_using_api_rule_test.dart | 17 + test/utils/fake_analysis_options_loader.dart | 3 + 16 files changed, 938 insertions(+), 49 deletions(-) create mode 100644 lib/src/common/parameter_parser/analysis_options_parser.dart create mode 100644 lib/src/common/parameter_parser/cached_package_config.dart create mode 100644 lib/src/common/parameter_parser/package_config_resolver.dart create mode 100644 lib/src/common/parameter_parser/rules_data.dart create mode 100644 lib/src/common/solid_lints_constants.dart create mode 100644 lib/src/models/proxy_analysis_rule.dart create mode 100644 lib/src/models/proxy_multi_analysis_rule.dart create mode 100644 lib/src/utils/map_utils.dart diff --git a/lib/main.dart b/lib/main.dart index a6f55fad..8471e3e8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,8 @@ import 'package:analysis_server_plugin/plugin.dart'; import 'package:analysis_server_plugin/registry.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/common/solid_lints_constants.dart'; import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; import 'package:solid_lints/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; @@ -30,6 +32,8 @@ import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; import 'package:solid_lints/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart'; +import 'package:solid_lints/src/models/proxy_analysis_rule.dart'; +import 'package:solid_lints/src/models/proxy_multi_analysis_rule.dart'; import 'package:solid_lints/src/models/rule_with_fixes.dart'; /// The entry point for the Solid Lints analyser server plugin. @@ -44,7 +48,7 @@ final plugin = SolidLintsPlugin(); /// by the Dart analyzer during code analysis. class SolidLintsPlugin extends Plugin { @override - String get name => 'solid_lints'; + String get name => SolidLintsConstants.pluginName; @override void register(PluginRegistry registry) { @@ -83,7 +87,13 @@ class SolidLintsPlugin extends Plugin { ]; for (final lintRule in lintRules) { - registry.registerLintRule(lintRule); + final ruleToRegister = switch (lintRule) { + MultiAnalysisRule() => ProxyMultiAnalysisRule(lintRule, analysisLoader), + AnalysisRule() => ProxyAnalysisRule(lintRule, analysisLoader), + }; + + registry.registerWarningRule(ruleToRegister); + if (lintRule is RuleWithFixes) { for (final entry in (lintRule as RuleWithFixes).fixesForCodes) { registry.registerFixForRule(entry.key, entry.value); diff --git a/lib/src/common/parameter_parser/analysis_options_loader.dart b/lib/src/common/parameter_parser/analysis_options_loader.dart index 5b0a016e..104d8aa3 100644 --- a/lib/src/common/parameter_parser/analysis_options_loader.dart +++ b/lib/src/common/parameter_parser/analysis_options_loader.dart @@ -1,17 +1,42 @@ import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/file_system/file_system.dart'; import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_parser.dart'; import 'package:solid_lints/src/common/parameter_parser/cached_package_rules.dart'; -import 'package:yaml/yaml.dart'; +import 'package:solid_lints/src/common/parameter_parser/package_config_resolver.dart'; /// Loads and parses analysis options from a Dart project's YAML file. class AnalysisOptionsLoader { final ResourceProvider _resourceProvider; final Map _rulesCache = {}; + final AnalysisOptionsParser _parser; + + /// Creates an instance of [AnalysisOptionsLoader]. + factory AnalysisOptionsLoader({ + ResourceProvider? resourceProvider, + AnalysisOptionsParser? parser, + }) { + final resolvedResourceProvider = + resourceProvider ?? PhysicalResourceProvider.INSTANCE; + + final resolvedParser = + parser ?? + AnalysisOptionsParser( + resolvedResourceProvider, + PackageConfigResolver(resolvedResourceProvider), + ); + + return AnalysisOptionsLoader._( + resourceProvider: resolvedResourceProvider, + parser: resolvedParser, + ); + } - /// Creates an instance of [AnalysisOptionsLoader] - AnalysisOptionsLoader({ResourceProvider? resourceProvider}) - : _resourceProvider = resourceProvider ?? PhysicalResourceProvider.INSTANCE; + AnalysisOptionsLoader._({ + required ResourceProvider resourceProvider, + required AnalysisOptionsParser parser, + }) : _resourceProvider = resourceProvider, + _parser = parser; /// Gets the options for a specific rule by its name. Map? getRuleOptions(RuleContext context, String ruleName) => @@ -36,6 +61,17 @@ class AnalysisOptionsLoader { return _rulesCache[yamlPath]?.rules[ruleName]; } + /// Checks if a rule is explicitly disabled. + bool isRuleDisabled(RuleContext context, String ruleName) => + _withNearestAnalysisOptionsFilePathForContext( + context, + (path) { + _loadRulesOptionsIfNewer(path); + return _rulesCache[path]?.disabledRules.contains(ruleName) ?? false; + }, + ) ?? + false; + /// Loads lint rules from the analysis options file for all rules /// using the provided [RuleContext]. void loadRulesOptionsFromContext(RuleContext context) => @@ -66,18 +102,19 @@ class AnalysisOptionsLoader { return; } - final rules = _getRules(analysisOptionsFile); + final rulesData = _parser.parse(analysisOptionsFile); _rulesCache[yamlPath] = CachedPackageRules( modificationStamp: modificationStamp, - rules: rules, + rules: rulesData.rules, + disabledRules: rulesData.disabledRules, ); } String? _findNearestAnalysisOptionsFilePath(String startDirectoryPath) { final pathContext = _resourceProvider.pathContext; - String currentDirectoryPath = startDirectoryPath; + var currentDirectoryPath = startDirectoryPath; - while (true) { + while (currentDirectoryPath.isNotEmpty) { final candidatePath = pathContext.join( currentDirectoryPath, 'analysis_options.yaml', @@ -97,39 +134,4 @@ class AnalysisOptionsLoader { return null; } - - Map> _getRules(File? analysisOptionsFile) { - if (analysisOptionsFile == null || !analysisOptionsFile.exists) { - return {}; - } - - Object? yaml; - try { - final optionsString = analysisOptionsFile.readAsStringSync(); - yaml = loadYaml(optionsString); - } catch (_) { - return {}; - } - - Object? rawDiagnostics; - if (yaml case {'solid_lints': {'diagnostics': final diagnostics}}) { - rawDiagnostics = diagnostics; - } else if (yaml case { - 'plugins': {'solid_lints': {'diagnostics': final diagnostics}}, - }) { - rawDiagnostics = diagnostics; - } - - if (rawDiagnostics is! Map) return {}; - - return { - for (final entry in rawDiagnostics.entries) - if (entry.key is String && entry.value is Map) - entry.key as String: { - for (final optionEntry in (entry.value as Map).entries) - if (optionEntry.key is String) - optionEntry.key as String: optionEntry.value, - }, - }; - } } diff --git a/lib/src/common/parameter_parser/analysis_options_parser.dart b/lib/src/common/parameter_parser/analysis_options_parser.dart new file mode 100644 index 00000000..f3465d4d --- /dev/null +++ b/lib/src/common/parameter_parser/analysis_options_parser.dart @@ -0,0 +1,231 @@ +import 'package:analyzer/file_system/file_system.dart'; +import 'package:solid_lints/src/common/parameter_parser/package_config_resolver.dart'; +import 'package:solid_lints/src/common/parameter_parser/rules_data.dart'; +import 'package:solid_lints/src/common/solid_lints_constants.dart'; +import 'package:solid_lints/src/utils/map_utils.dart'; +import 'package:yaml/yaml.dart'; + +/// Parser for analysis_options.yaml files to extract RulesData. +class AnalysisOptionsParser { + final ResourceProvider _resourceProvider; + final PackageConfigResolver _packageConfigResolver; + + /// Creates a new instance of [AnalysisOptionsParser]. + AnalysisOptionsParser(this._resourceProvider, this._packageConfigResolver); + + /// Parses the given [analysisOptionsFile] and resolves its imports + /// to return [RulesData]. + RulesData parse(File? analysisOptionsFile) { + return _parseWithSeen(analysisOptionsFile, {}); + } + + RulesData _parseWithSeen(File? analysisOptionsFile, Set seenPaths) { + if (analysisOptionsFile == null || !analysisOptionsFile.exists) { + return const RulesData.empty(); + } + + final path = analysisOptionsFile.path; + if (seenPaths.contains(path)) { + return const RulesData.empty(); + } + + final yaml = _parseYaml(analysisOptionsFile); + if (yaml == null) { + return const RulesData.empty(); + } + + final mergedRules = >{}; + final disabledRules = {}; + + final nextSeenPaths = {...seenPaths, path}; + _resolveAndMergeIncludes( + analysisOptionsFile, + yaml, + nextSeenPaths, + mergedRules, + disabledRules, + ); + _parseRuleOptions(yaml, mergedRules, disabledRules); + _parseSuppressedErrors(yaml, mergedRules, disabledRules); + + return RulesData(rules: mergedRules, disabledRules: disabledRules); + } + + Map? _parseYaml(File file) { + try { + final optionsString = file.readAsStringSync(); + final parsed = loadYaml(optionsString); + if (parsed is! Map) return null; + return _toStandardMap(parsed); + } catch (_) { + return null; + } + } + + Map _toStandardMap(Map map) => + map.whereKeyType().map( + (key, value) => MapEntry(key, _toStandardType(value)), + ); + + Object? _toStandardType(Object? value) => switch (value) { + Map() => _toStandardMap(value), + Iterable() => value.map(_toStandardType).toList(), + _ => value, + }; + + void _resolveAndMergeIncludes( + File baseFile, + Map yaml, + Set seenPaths, + Map> mergedRules, + Set disabledRules, + ) { + final includeOption = yaml['include']; + switch (includeOption) { + case String(): + _resolveAndMergeInclude( + baseFile, + includeOption, + seenPaths, + mergedRules, + disabledRules, + ); + case List(): + for (final include in includeOption) { + if (include is String) { + _resolveAndMergeInclude( + baseFile, + include, + seenPaths, + mergedRules, + disabledRules, + ); + } + } + } + } + + void _resolveAndMergeInclude( + File baseFile, + String includePath, + Set seenPaths, + Map> mergedRules, + Set disabledRules, + ) { + final includedFile = _resolveIncludedFile(baseFile, includePath); + if (includedFile == null) return; + + final includedData = _parseWithSeen(includedFile, seenPaths); + for (final MapEntry(key: ruleName, value: includedOptions) + in includedData.rules.entries) { + mergedRules[ruleName] = { + ...?mergedRules[ruleName], + ...includedOptions, + }; + } + disabledRules.addAll(includedData.disabledRules); + } + + File? _resolveIncludedFile(File baseFile, String includePath) { + final pathContext = _resourceProvider.pathContext; + if (includePath.startsWith('package:')) { + final resolvedPath = _packageConfigResolver.resolvePackageUri( + baseFile.path, + includePath, + ); + if (resolvedPath != null) { + return _resourceProvider.getFile(resolvedPath); + } + return null; + } + + final baseDir = pathContext.dirname(baseFile.path); + final resolvedPath = pathContext.join(baseDir, includePath); + return _resourceProvider.getFile(resolvedPath); + } + + void _parseRuleOptions( + Map yaml, + Map> mergedRules, + Set disabledRules, + ) { + final rawDiagnostics = _extractDiagnostics(yaml); + if (rawDiagnostics is! Map) return; + + for (final MapEntry(key: ruleName, :value) in rawDiagnostics.entries) { + if (ruleName is! String) continue; + + switch (value) { + case Map(): + mergedRules[ruleName] = { + ...?mergedRules[ruleName], + ...value.whereKeyType(), + }; + disabledRules.remove(ruleName); + case bool(): + if (value) { + disabledRules.remove(ruleName); + } else { + mergedRules.remove(ruleName); + disabledRules.add(ruleName); + } + case null: + disabledRules.remove(ruleName); + } + } + } + + Object? _extractDiagnostics(Map yaml) { + final sources = [yaml, yaml['plugins']]; + for (final source in sources) { + if (source is! Map) continue; + final config = source[SolidLintsConstants.pluginName]; + if (config is Map) return config['diagnostics']; + } + + return null; + } + + /// Parses rule suppression configured under `analyzer: errors:`. + /// + /// By default, when a user excludes/ignores a rule using the IDE + /// quick fix or standard settings, the Dart analysis server appends + /// the following to `analysis_options.yaml`: + /// + /// ```yaml + /// analyzer: + /// errors: + /// solid_lints/rule_name: ignore + /// ``` + /// + /// To respect this standard mechanism and prevent the plugin rules + /// from running, we parse this section and add suppressed rules + /// to [disabledRules]. + void _parseSuppressedErrors( + Map yaml, + Map> mergedRules, + Set disabledRules, + ) { + final analyzer = yaml['analyzer']; + if (analyzer is! Map) return; + + final errors = analyzer['errors']; + if (errors is! Map) return; + + const pluginPrefix = '${SolidLintsConstants.pluginName}/'; + + for (final MapEntry(key: key, value: errorValue) + in errors.whereKeyType().entries) { + if (!key.startsWith(pluginPrefix)) continue; + + final ruleName = key.substring(pluginPrefix.length); + + if (errorValue == 'ignore' || errorValue == false) { + mergedRules.remove(ruleName); + disabledRules.add(ruleName); + } else { + disabledRules.remove(ruleName); + } + } + } +} diff --git a/lib/src/common/parameter_parser/cached_package_config.dart b/lib/src/common/parameter_parser/cached_package_config.dart new file mode 100644 index 00000000..48fda09a --- /dev/null +++ b/lib/src/common/parameter_parser/cached_package_config.dart @@ -0,0 +1,14 @@ +/// Cached package configuration with its modification stamp. +class CachedPackageConfig { + /// The modification stamp of the package_config.json file when cached. + final int modificationStamp; + + /// The parsed package mappings (packageName -> absolute path to lib). + final Map packages; + + /// Creates a new instance of [CachedPackageConfig]. + const CachedPackageConfig({ + required this.modificationStamp, + required this.packages, + }); +} diff --git a/lib/src/common/parameter_parser/cached_package_rules.dart b/lib/src/common/parameter_parser/cached_package_rules.dart index a65201d9..d9bee3bb 100644 --- a/lib/src/common/parameter_parser/cached_package_rules.dart +++ b/lib/src/common/parameter_parser/cached_package_rules.dart @@ -6,9 +6,13 @@ class CachedPackageRules { /// Cached rules options by rule name for the package final Map> rules; + /// Rules that are explicitly disabled + final Set disabledRules; + /// Creates an instance of [CachedPackageRules] const CachedPackageRules({ required this.modificationStamp, required this.rules, + required this.disabledRules, }); } diff --git a/lib/src/common/parameter_parser/package_config_resolver.dart b/lib/src/common/parameter_parser/package_config_resolver.dart new file mode 100644 index 00000000..1cc1207e --- /dev/null +++ b/lib/src/common/parameter_parser/package_config_resolver.dart @@ -0,0 +1,125 @@ +import 'dart:convert'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:solid_lints/src/common/parameter_parser/cached_package_config.dart'; + +/// Helper class to resolve package: URIs to physical file system paths. +class PackageConfigResolver { + final ResourceProvider _resourceProvider; + + // Caches package_config.json path -> CachedPackageConfig + final Map _packagesCache = {}; + + /// Creates a new instance of [PackageConfigResolver]. + PackageConfigResolver(this._resourceProvider); + + /// Resolves the absolute directory path for a package URI + /// (e.g., `package:solid_lints/lib/`). + String? resolvePackageUri(String baseFilePath, String packageUri) { + if (!packageUri.startsWith('package:')) return null; + + final pathSegments = Uri.tryParse(packageUri)?.pathSegments; + if (pathSegments == null || pathSegments.isEmpty) return null; + + final baseDir = _resourceProvider.pathContext.dirname(baseFilePath); + final packageConfigPath = _findPackageConfig(baseDir); + if (packageConfigPath == null) return null; + + final [packageName, ...relativePathSegments] = pathSegments; + final packageRoot = _resolvePackageRoot(packageConfigPath, packageName); + if (packageRoot == null) return null; + + return _resourceProvider.pathContext.join( + packageRoot, + relativePathSegments.join('/'), + ); + } + + /// Finds the nearest `.dart_tool/package_config.json` file by walking up + /// the directory tree starting from [startDirectoryPath]. + String? _findPackageConfig(String startDirectoryPath) { + final pathContext = _resourceProvider.pathContext; + var currentDirectoryPath = startDirectoryPath; + + while (true) { + final candidatePath = pathContext.join( + currentDirectoryPath, + '.dart_tool', + 'package_config.json', + ); + + final candidateFile = _resourceProvider.getFile(candidatePath); + if (candidateFile.exists) return candidatePath; + + final parentDir = pathContext.dirname(currentDirectoryPath); + if (parentDir == currentDirectoryPath || parentDir.isEmpty) return null; + + currentDirectoryPath = parentDir; + } + } + + String? _resolvePackageRoot(String packageConfigPath, String packageName) { + final file = _resourceProvider.getFile(packageConfigPath); + final modificationStamp = file.exists ? file.modificationStamp : -1; + + var cached = _packagesCache[packageConfigPath]; + if (cached == null || cached.modificationStamp != modificationStamp) { + final packageMap = _parsePackageConfig(file); + cached = CachedPackageConfig( + modificationStamp: modificationStamp, + packages: packageMap, + ); + _packagesCache[packageConfigPath] = cached; + } + return cached.packages[packageName]; + } + + /// Reads and parses the `.dart_tool/package_config.json` configuration file. + /// + /// Returns a map associating each package name (e.g., 'lints') with its + /// absolute physical path to the `lib/` directory on disk. + Map _parsePackageConfig(File file) { + if (!file.exists) return const {}; + + try { + if (jsonDecode(file.readAsStringSync()) case { + 'packages': final List packagesList, + }) { + return { + for (final package in packagesList) + if (package case { + 'name': final String name, + 'rootUri': final String rootUriString, + 'packageUri': final String packageUriString, + }) + if (_resolveRootUriFrom( + rootUriString: rootUriString, + filePath: file.path, + ) + case final resolvedRootUri?) + name: _resourceProvider.pathContext.join( + resolvedRootUri, + packageUriString, + ), + }; + } + } catch (_) {} + + return const {}; + } + + String? _resolveRootUriFrom({ + required String rootUriString, + required String filePath, + }) { + final rootUri = Uri.parse(rootUriString); + final resolvedRootUri = rootUri.isAbsolute + ? rootUri + : _resourceProvider.pathContext.toUri(filePath).resolveUri(rootUri); + + if (resolvedRootUri.isScheme('file')) { + return _resourceProvider.pathContext.fromUri(resolvedRootUri); + } + + return null; + } +} diff --git a/lib/src/common/parameter_parser/rules_data.dart b/lib/src/common/parameter_parser/rules_data.dart new file mode 100644 index 00000000..a589b3fe --- /dev/null +++ b/lib/src/common/parameter_parser/rules_data.dart @@ -0,0 +1,16 @@ +/// Data class representing parsed rules configuration and disabled rules. +class RulesData { + /// The active rules options mapped by rule name. + final Map> rules; + + /// The set of explicitly disabled rules. + final Set disabledRules; + + /// Creates a new instance of [RulesData]. + const RulesData({required this.rules, required this.disabledRules}); + + /// Creates a new empty instance of [RulesData]. + const RulesData.empty() + : rules = const {}, + disabledRules = const {}; +} diff --git a/lib/src/common/solid_lints_constants.dart b/lib/src/common/solid_lints_constants.dart new file mode 100644 index 00000000..cf09b8ca --- /dev/null +++ b/lib/src/common/solid_lints_constants.dart @@ -0,0 +1,5 @@ +/// Shared constants for the solid_lints package. +abstract final class SolidLintsConstants { + /// The name of the plugin. + static const pluginName = 'solid_lints'; +} diff --git a/lib/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart b/lib/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart index 6494fcaf..f132b23c 100644 --- a/lib/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart +++ b/lib/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart @@ -47,10 +47,17 @@ class AvoidUsingApiParameters { final avoidUsingApi = json['avoid_using_api']; return AvoidUsingApiParameters( entries: List.from( - (json['entries'] as Iterable?)?.map( - (e) => AvoidUsingApiEntryParameters.fromJson( - (e as YamlMap).toMap(), - ), + (json['entries'] as Iterable?) + ?.whereType>() + .map( + (e) { + if (e is YamlMap) { + return AvoidUsingApiEntryParameters.fromJson(e.toMap()); + } + return AvoidUsingApiEntryParameters.fromJson( + Map.from(e), + ); + }, ) ?? [], ), diff --git a/lib/src/models/proxy_analysis_rule.dart b/lib/src/models/proxy_analysis_rule.dart new file mode 100644 index 00000000..2bdd2d4e --- /dev/null +++ b/lib/src/models/proxy_analysis_rule.dart @@ -0,0 +1,54 @@ +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/pubspec.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; + +/// A proxy wrapper for [AnalysisRule] that checks if the rule is disabled +/// before registering its node processors. +class ProxyAnalysisRule extends AnalysisRule { + /// The delegated rule. + final AnalysisRule delegate; + + /// The loader used to check if the rule is disabled. + final AnalysisOptionsLoader loader; + + /// Creates a new instance of [ProxyAnalysisRule]. + ProxyAnalysisRule(this.delegate, this.loader) + : super( + name: delegate.name, + description: delegate.description, + state: delegate.state, + ); + + @override + DiagnosticCode get diagnosticCode => delegate.diagnosticCode; + + @override + bool get canUseParsedResult => delegate.canUseParsedResult; + + @override + List get incompatibleRules => delegate.incompatibleRules; + + @override + PubspecVisitor? get pubspecVisitor => delegate.pubspecVisitor; + + @override + set reporter(DiagnosticReporter value) { + super.reporter = value; + delegate.reporter = value; + } + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + if (loader.isRuleDisabled(context, name)) { + return; + } + delegate.registerNodeProcessors(registry, context); + } +} diff --git a/lib/src/models/proxy_multi_analysis_rule.dart b/lib/src/models/proxy_multi_analysis_rule.dart new file mode 100644 index 00000000..e497590b --- /dev/null +++ b/lib/src/models/proxy_multi_analysis_rule.dart @@ -0,0 +1,54 @@ +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/pubspec.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; + +/// A proxy wrapper for [MultiAnalysisRule] that checks if the rule is disabled +/// before registering its node processors. +class ProxyMultiAnalysisRule extends MultiAnalysisRule { + /// The delegated rule. + final MultiAnalysisRule delegate; + + /// The loader used to check if the rule is disabled. + final AnalysisOptionsLoader loader; + + /// Creates a new instance of [ProxyMultiAnalysisRule]. + ProxyMultiAnalysisRule(this.delegate, this.loader) + : super( + name: delegate.name, + description: delegate.description, + state: delegate.state, + ); + + @override + List get diagnosticCodes => delegate.diagnosticCodes; + + @override + bool get canUseParsedResult => delegate.canUseParsedResult; + + @override + List get incompatibleRules => delegate.incompatibleRules; + + @override + PubspecVisitor? get pubspecVisitor => delegate.pubspecVisitor; + + @override + set reporter(DiagnosticReporter value) { + super.reporter = value; + delegate.reporter = value; + } + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + if (loader.isRuleDisabled(context, name)) { + return; + } + delegate.registerNodeProcessors(registry, context); + } +} diff --git a/lib/src/utils/map_utils.dart b/lib/src/utils/map_utils.dart new file mode 100644 index 00000000..9b5c37d5 --- /dev/null +++ b/lib/src/utils/map_utils.dart @@ -0,0 +1,16 @@ +/// Extension on [Map] to provide filtering and casting utilities. +extension MapExtensions on Map { + /// Filters the map entries keeping only those whose keys are of type [U], + /// and casts the resulting map's keys to [U]. + Map whereKeyType() => { + for (final MapEntry(:key, :value) in entries) + if (key is U) key: value, + }; + + /// Filters the map entries keeping only those whose values are of type [W], + /// and casts the resulting map's values to [W]. + Map whereValueType() => { + for (final MapEntry(:key, :value) in entries) + if (value is W) key: value, + }; +} diff --git a/pubspec.yaml b/pubspec.yaml index 56c55842..31a9af9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,3 +27,8 @@ dev_dependencies: args: ^2.6.0 analyzer_testing: ^0.1.9 test_reflective_loader: ^0.3.0 + +plugin: + platforms: + dart: + pluginClass: SolidLintsPlugin diff --git a/test/src/common/parameter_parser/analysis_options_loader_test.dart b/test/src/common/parameter_parser/analysis_options_loader_test.dart index 67fffb6a..a47a26e0 100644 --- a/test/src/common/parameter_parser/analysis_options_loader_test.dart +++ b/test/src/common/parameter_parser/analysis_options_loader_test.dart @@ -277,6 +277,332 @@ plugins: expect(options, {'some_parameter': 'nested_value'}); } + void test_resolve_include_relative_path() { + final includedOptionsPath = '$testPackageRootPath/included_options.yaml'; + newFile( + includedOptionsPath, + ''' +solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: + some_parameter: included_value +''', + ); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +include: included_options.yaml +''', + ); + + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final options = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + expect(options, isNotNull); + expect(options, {'some_parameter': 'included_value'}); + } + + void test_resolve_include_package_path() { + final sharedPackageRoot = '/home/shared'; + newFolder(sharedPackageRoot); + newFile( + '$sharedPackageRoot/lib/analysis_options.yaml', + ''' +solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: + some_parameter: package_value +''', + ); + + newPackageConfigJsonFile( + testPackageRootPath, + ''' +{ + "configVersion": 2, + "packages": [ + { + "name": "shared", + "rootUri": "file://$sharedPackageRoot", + "packageUri": "lib/" + } + ] +} +''', + ); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +include: package:shared/analysis_options.yaml +''', + ); + + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final options = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + expect(options, isNotNull); + expect(options, {'some_parameter': 'package_value'}); + } + + void test_resolve_include_malformed_package_uri() { + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +include: package:foo:bar/baz.yaml +''', + ); + + // Should not throw FormatException + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final options = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + expect(options, isNull); + } + + void test_resolve_include_cyclic() { + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +include: analysis_options.yaml +solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: + some_parameter: cyclic_value +''', + ); + + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final options = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + expect(options, isNotNull); + expect(options, {'some_parameter': 'cyclic_value'}); + } + + void test_isRuleDisabled_when_set_to_false() { + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: false +''', + ); + + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + expect( + analysisOptionsLoader.isRuleDisabled(mockRuleContext, _mockRuleThatNeedsConfigName), + isTrue, + ); + } + + void test_isRuleDisabled_when_suppressed_in_analyzer_errors() { + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +analyzer: + errors: + solid_lints/$_mockRuleThatNeedsConfigName: ignore + solid_lints/$_mockRule2Name: ignore +''', + ); + + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + expect( + analysisOptionsLoader.isRuleDisabled(mockRuleContext, _mockRuleThatNeedsConfigName), + isTrue, + ); + expect( + analysisOptionsLoader.isRuleDisabled(mockRuleContext, _mockRule2Name), + isTrue, + ); + } + + void test_isRuleDisabled_when_suppressed_in_analyzer_errors_as_false() { + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +analyzer: + errors: + solid_lints/$_mockRuleThatNeedsConfigName: false +''', + ); + + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + expect( + analysisOptionsLoader.isRuleDisabled(mockRuleContext, _mockRuleThatNeedsConfigName), + isTrue, + ); + } + + + void test_isRuleDisabled_when_suppressed_in_included_analyzer_errors() { + final includedOptionsPath = '$testPackageRootPath/included_options.yaml'; + newFile( + includedOptionsPath, + ''' +analyzer: + errors: + solid_lints/$_mockRuleThatNeedsConfigName: ignore +''', + ); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +include: included_options.yaml +''', + ); + + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + expect( + analysisOptionsLoader.isRuleDisabled(mockRuleContext, _mockRuleThatNeedsConfigName), + isTrue, + ); + } + + void test_isRuleDisabled_when_disabled_in_include_but_re_enabled_with_null() { + final includedOptionsPath = '$testPackageRootPath/included_options.yaml'; + newFile( + includedOptionsPath, + ''' +solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: false +''', + ); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +include: included_options.yaml +solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: +''', + ); + + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + expect( + analysisOptionsLoader.isRuleDisabled(mockRuleContext, _mockRuleThatNeedsConfigName), + isFalse, + ); + } + + void test_options_merging_with_include() { + final includedOptionsPath = '$testPackageRootPath/included_options.yaml'; + newFile( + includedOptionsPath, + ''' +solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: + param_a: val_a + param_b: val_b +''', + ); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +include: included_options.yaml +solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: + param_b: local_val_b + param_c: val_c +''', + ); + + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final options = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + expect(options, { + 'param_a': 'val_a', + 'param_b': 'local_val_b', + 'param_c': 'val_c', + }); + } + + void test_options_merging_with_multiple_includes() { + final includedOptionsPath1 = '$testPackageRootPath/included_options_1.yaml'; + final includedOptionsPath2 = '$testPackageRootPath/included_options_2.yaml'; + newFile( + includedOptionsPath1, + ''' +solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: + param_a: val_a + param_b: val_b +''', + ); + newFile( + includedOptionsPath2, + ''' +solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: + param_b: val_b_2 + param_c: val_c +''', + ); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + ''' +include: + - included_options_1.yaml + - included_options_2.yaml +solid_lints: + diagnostics: + $_mockRuleThatNeedsConfigName: + param_c: local_val_c + param_d: val_d +''', + ); + + analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); + + final options = analysisOptionsLoader.getRuleOptions( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ); + + expect(options, { + 'param_a': 'val_a', + 'param_b': 'val_b_2', + 'param_c': 'local_val_c', + 'param_d': 'val_d', + }); + } + RuleContext _createMockContextForPackage( String packageRootPath, { RuleContextUnit? definingUnit, diff --git a/test/src/lints/avoid_using_api/avoid_using_api_rule_test.dart b/test/src/lints/avoid_using_api/avoid_using_api_rule_test.dart index 09b070b3..780ddc8d 100644 --- a/test/src/lints/avoid_using_api/avoid_using_api_rule_test.dart +++ b/test/src/lints/avoid_using_api/avoid_using_api_rule_test.dart @@ -396,4 +396,21 @@ void fn() { expect(diagnostics[2].severity.name, 'info'); expect(diagnostics[3].severity.name, 'warning'); } + + Future test_does_not_crash_on_invalid_entry_types() async { + _configureRule(''' + - "not_a_map" + - 123 + - class_name: BadClass + source: package:my_dep/my_dep.dart + '''); + + return assertAutoDiagnostics(''' +import 'package:my_dep/my_dep.dart'; + +void fn() { + final ${expectLint('a')} = ${expectLint('BadClass')}(); +} +'''); + } } diff --git a/test/utils/fake_analysis_options_loader.dart b/test/utils/fake_analysis_options_loader.dart index 777abb87..3672abca 100644 --- a/test/utils/fake_analysis_options_loader.dart +++ b/test/utils/fake_analysis_options_loader.dart @@ -19,4 +19,7 @@ class FakeAnalysisOptionsLoader implements AnalysisOptionsLoader { @override void loadRulesOptionsFromContext(RuleContext context) {} + + @override + bool isRuleDisabled(RuleContext context, String ruleName) => false; } From 8c2781286bf2bfb7a72840bc70fe2713a13c1e37 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Fri, 17 Jul 2026 16:46:00 +0300 Subject: [PATCH 44/49] Add new rule: use_descriptive_names_for_type_parameters (#319) * feat: add new rule use_descriptive_names_for_type_parameters * refactor code * use cascade notattion * Add test case with nested functions * Add parameter for minimum number of type parameters * docs: add missing period to fromJson documentation comment * refactor: migrate use_descriptive_names_for_type_parameters rule * feat: extend use_descriptive_names_for_type_parameters to check class type aliases and include configuration for min_type_parameters * feat: add use_descriptive_names_for_type_parameters rule and bump version to 1.0.0 * chore: remove use_descriptive_names_for_type_parameters test files * refactor: simplify visitor implementation using pattern matching and cascade notation for registry registration --------- Co-authored-by: xJac0b Co-authored-by: Illia Aihistov --- CHANGELOG.md | 45 ++-- lib/analysis_options.yaml | 1 + lib/main.dart | 4 + ..._names_for_type_parameters_parameters.dart | 28 +++ ...iptive_names_for_type_parameters_rule.dart | 90 ++++++++ ...ive_names_for_type_parameters_visitor.dart | 73 +++++++ pubspec.yaml | 2 +- ...e_names_for_type_parameters_rule_test.dart | 205 ++++++++++++++++++ 8 files changed, 430 insertions(+), 18 deletions(-) create mode 100644 lib/src/lints/use_descriptive_names_for_type_parameters/models/use_descriptive_names_for_type_parameters_parameters.dart create mode 100644 lib/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule.dart create mode 100644 lib/src/lints/use_descriptive_names_for_type_parameters/visitors/use_descriptive_names_for_type_parameters_visitor.dart create mode 100644 test/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c8c46d4..f4ccc394 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ ## 1.0.0 - feat!: migrate to analyzer_server_plugin -- refactor: replace `avoid_unnecessary_type_casts` rule with dart analyzer's `unnecessary_cast` +- refactor: replace `avoid_unnecessary_type_casts` rule with dart analyzer's + `unnecessary_cast` - Added `use_nearest_context` rule. +- Added `use_descriptive_names_for_type_parameters` rule. ## 0.3.3 @@ -17,9 +19,9 @@ - Added `allow_with_comments` parameter for `no_empty_block` lint. - Added extension support for `avoid_using_api` -- Added `exclude_entity` parameter for `prefer_match_file_name` lint. - It is now possible to configure this lint to ignore `enum`, - `extension` and `mixin` declarations via `analysis_options.yaml`. +- Added `exclude_entity` parameter for `prefer_match_file_name` lint. It is now + possible to configure this lint to ignore `enum`, `extension` and `mixin` + declarations via `analysis_options.yaml`. ## 0.3.0 @@ -30,7 +32,8 @@ - `function_lines_of_code` - `no_empty_bloc` - `number_of_parameters` -- BREAKING CHANGE: Renamed `excludeNames` parameter to `exclude` for `function_lines_of_code` lint. +- BREAKING CHANGE: Renamed `excludeNames` parameter to `exclude` for + `function_lines_of_code` lint. - Fixed an issue with `prefer_early_retrun` for throw expression - `number_of_parameters` lint: added `copyWith` to the default exclude list. - Update dependencies: @@ -53,28 +56,36 @@ ## 0.2.0 - Added `avoid_final_with_getter` rule -- Improve `avoid_late_keyword` - `ignored_types` to support ignoring subtype of the node type () -- Abstract methods should be omitted by `proper_super_calls` () -- Add a rule prefer_guard_clause for reversing nested if statements () -- add exclude params support to avoid_returning_widgets rule () -- add quick fix to avoid_final_with_getter () +- Improve `avoid_late_keyword` - `ignored_types` to support ignoring subtype of + the node type () +- Abstract methods should be omitted by `proper_super_calls` + () +- Add a rule prefer_guard_clause for reversing nested if statements + () +- add exclude params support to avoid_returning_widgets rule + () +- add quick fix to avoid_final_with_getter + () - Renamed `avoid_debug_print` to `avoid_debug_print_in_release` -- The `avoid_debug_print_in_release` no longer reports a warning if the `debugPrint` call is wrapped in a `!kReleaseMode` check. +- The `avoid_debug_print_in_release` no longer reports a warning if the + `debugPrint` call is wrapped in a `!kReleaseMode` check. - Update custom_lints to work with newer Flutter ## 0.1.5 - Added `avoid_debug_print` rule - Fixed an issue with no_magic_number lint -- Fixed `avoid_unused_parameters` to report positional parameters from typedef if their name are not underscores. +- Fixed `avoid_unused_parameters` to report positional parameters from typedef + if their name are not underscores. - Improvement for `avoid_returning_widget` lint: - ignores methods that override ones that return widget (build() for example) - no longer allows returning widgets from methods/functions named build - Fixed unexpected avoid_unnecessary_type_assertions - Added `excludeNames` param for `function_lines_of_code` lint - Improved `avoid_unrelated_type_assertions` to support true and false results -- Set default `cyclomatic_complexity` to 10 () - Credits: Arthur Miranda () +- Set default `cyclomatic_complexity` to 10 + () Credits: Arthur + Miranda () ## 0.1.4 @@ -108,8 +119,7 @@ - avoid_unnecessary_type_casts - avoid_unrelated_type_assertions - avoid_unused_parameters - - avoid_using_api - Credits: getBoolean () + - avoid_using_api Credits: getBoolean () - cyclomatic_complexity - double_literal_format - function_lines_of_code @@ -176,7 +186,8 @@ ## 0.0.13 -- enable use_colored_box & use_decorated_box + add more comments about specific lints +- enable use_colored_box & use_decorated_box + add more comments about specific + lints ## 0.0.12 diff --git a/lib/analysis_options.yaml b/lib/analysis_options.yaml index f74ed9ed..7dbbcdd4 100644 --- a/lib/analysis_options.yaml +++ b/lib/analysis_options.yaml @@ -101,6 +101,7 @@ solid_lints: proper_super_calls: true named_parameters_ordering: true use_nearest_context: true + use_descriptive_names_for_type_parameters: true linter: rules: diff --git a/lib/main.dart b/lib/main.dart index 8471e3e8..ed4503b5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -31,6 +31,7 @@ import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; import 'package:solid_lints/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; +import 'package:solid_lints/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule.dart'; import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart'; import 'package:solid_lints/src/models/proxy_analysis_rule.dart'; import 'package:solid_lints/src/models/proxy_multi_analysis_rule.dart'; @@ -83,6 +84,9 @@ class SolidLintsPlugin extends Plugin { PreferLastRule(), PreferMatchFileNameRule(analysisOptionsLoader: analysisLoader), ProperSuperCallsRule(), + UseDescriptiveNamesForTypeParametersRule( + analysisOptionsLoader: analysisLoader, + ), UseNearestContextRule(), ]; diff --git a/lib/src/lints/use_descriptive_names_for_type_parameters/models/use_descriptive_names_for_type_parameters_parameters.dart b/lib/src/lints/use_descriptive_names_for_type_parameters/models/use_descriptive_names_for_type_parameters_parameters.dart new file mode 100644 index 00000000..fbb86b87 --- /dev/null +++ b/lib/src/lints/use_descriptive_names_for_type_parameters/models/use_descriptive_names_for_type_parameters_parameters.dart @@ -0,0 +1,28 @@ +/// A data model class that represents the +/// "use descriptive names for type parameters" input parameters. +class UseDescriptiveNamesForTypeParametersParameters { + static const _defaultMinTypeParameters = 3; + + /// Minimum number of type parameters required before the rule is enforced. + final int minTypeParameters; + + /// Constructor for [UseDescriptiveNamesForTypeParametersParameters] model + const UseDescriptiveNamesForTypeParametersParameters({ + required this.minTypeParameters, + }); + + /// Empty [UseDescriptiveNamesForTypeParametersParameters] model. + factory UseDescriptiveNamesForTypeParametersParameters.empty() { + return const UseDescriptiveNamesForTypeParametersParameters( + minTypeParameters: _defaultMinTypeParameters, + ); + } + + /// Method for creating from json data. + factory UseDescriptiveNamesForTypeParametersParameters.fromJson( + Map json, + ) => UseDescriptiveNamesForTypeParametersParameters( + minTypeParameters: + json['min_type_parameters'] as int? ?? _defaultMinTypeParameters, + ); +} diff --git a/lib/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule.dart b/lib/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule.dart new file mode 100644 index 00000000..a994b2b8 --- /dev/null +++ b/lib/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule.dart @@ -0,0 +1,90 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/use_descriptive_names_for_type_parameters/models/use_descriptive_names_for_type_parameters_parameters.dart'; +import 'package:solid_lints/src/lints/use_descriptive_names_for_type_parameters/visitors/use_descriptive_names_for_type_parameters_visitor.dart'; +import 'package:solid_lints/src/models/solid_lint_rule.dart'; + +/// A `use_descriptive_names_for_type_parameters` rule which +/// warns about single-letter type parameter names when there are +/// three or more type parameters. +/// +/// ### Example: +/// +/// Assuming config: +/// +/// ```yaml +/// plugins: +/// solid_lints: +/// diagnostics: +/// use_descriptive_names_for_type_parameters: +/// min_type_parameters: 3 +/// ``` +/// +/// #### BAD: +/// ```dart +/// class MyClass {} // LINT +/// ``` +/// +/// #### GOOD: +/// ```dart +/// class MyClass {} // OK +/// class MyClass2 {} // OK +/// ``` +class UseDescriptiveNamesForTypeParametersRule + extends SolidLintRule { + /// The lint rule name. + static const lintName = 'use_descriptive_names_for_type_parameters'; + + static const _code = LintCode( + lintName, + 'Type parameters should have descriptive names instead ' + 'of single letters when there are {0} or ' + 'more type parameters.', + ); + + @override + DiagnosticCode get diagnosticCode => _code; + + /// Creates a new instance of [UseDescriptiveNamesForTypeParametersRule]. + UseDescriptiveNamesForTypeParametersRule({ + required super.analysisOptionsLoader, + }) : super.withParameters( + name: lintName, + description: + 'Warns about single-letter type parameter names when there are ' + 'three or more type parameters.', + parametersParser: + UseDescriptiveNamesForTypeParametersParameters.fromJson, + ); + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + super.registerNodeProcessors(registry, context); + + final parameters = + getParametersForContext(context) ?? + UseDescriptiveNamesForTypeParametersParameters.empty(); + + final visitor = UseDescriptiveNamesForTypeParametersVisitor( + this, + parameters, + ); + + registry + ..addClassDeclaration(this, visitor) + ..addClassTypeAlias(this, visitor) + ..addEnumDeclaration(this, visitor) + ..addFunctionExpression(this, visitor) + ..addMethodDeclaration(this, visitor) + ..addGenericTypeAlias(this, visitor) + ..addFunctionTypeAlias(this, visitor) + ..addGenericFunctionType(this, visitor) + ..addExtensionDeclaration(this, visitor) + ..addMixinDeclaration(this, visitor) + ..addExtensionTypeDeclaration(this, visitor); + } +} diff --git a/lib/src/lints/use_descriptive_names_for_type_parameters/visitors/use_descriptive_names_for_type_parameters_visitor.dart b/lib/src/lints/use_descriptive_names_for_type_parameters/visitors/use_descriptive_names_for_type_parameters_visitor.dart new file mode 100644 index 00000000..428c6f1b --- /dev/null +++ b/lib/src/lints/use_descriptive_names_for_type_parameters/visitors/use_descriptive_names_for_type_parameters_visitor.dart @@ -0,0 +1,73 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/use_descriptive_names_for_type_parameters/models/use_descriptive_names_for_type_parameters_parameters.dart'; +import 'package:solid_lints/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule.dart'; + +/// A visitor that checks descriptive names for type parameters on declarations. +class UseDescriptiveNamesForTypeParametersVisitor + extends SimpleAstVisitor { + final UseDescriptiveNamesForTypeParametersRule _rule; + final UseDescriptiveNamesForTypeParametersParameters _parameters; + + int get _minParameters => _parameters.minTypeParameters; + + /// Creates a new instance of [UseDescriptiveNamesForTypeParametersVisitor]. + UseDescriptiveNamesForTypeParametersVisitor(this._rule, this._parameters); + + void _visit(TypeParameterList? types) { + if (types case TypeParameterList( + typeParameters: final ps, + ) when ps.length >= _minParameters) { + ps.where(_hasInvalidShortName).forEach(_report); + } + } + + bool _hasInvalidShortName(TypeParameter p) => + p.name.length == 1 && p.name.lexeme != '_'; + + void _report(TypeParameter p) => + _rule.reportAtNode(p, arguments: ['$_minParameters']); + + @override + void visitClassDeclaration(ClassDeclaration node) => + _visit(node.namePart.typeParameters); + + @override + void visitClassTypeAlias(ClassTypeAlias node) => _visit(node.typeParameters); + + @override + void visitEnumDeclaration(EnumDeclaration node) => + _visit(node.namePart.typeParameters); + + @override + void visitFunctionExpression(FunctionExpression node) => + _visit(node.typeParameters); + + @override + void visitMethodDeclaration(MethodDeclaration node) => + _visit(node.typeParameters); + + @override + void visitGenericTypeAlias(GenericTypeAlias node) => + _visit(node.typeParameters); + + @override + void visitFunctionTypeAlias(FunctionTypeAlias node) => + _visit(node.typeParameters); + + @override + void visitGenericFunctionType(GenericFunctionType node) => + _visit(node.typeParameters); + + @override + void visitExtensionDeclaration(ExtensionDeclaration node) => + _visit(node.typeParameters); + + @override + void visitMixinDeclaration(MixinDeclaration node) => + _visit(node.typeParameters); + + @override + void visitExtensionTypeDeclaration(ExtensionTypeDeclaration node) => + _visit(node.primaryConstructor.typeParameters); +} diff --git a/pubspec.yaml b/pubspec.yaml index 31a9af9e..a5276bf7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: solid_lints description: Lints for Dart and Flutter based on software industry standards and best practices. -version: 0.3.4 +version: 1.0.0 homepage: https://github.com/solid-software/solid_lints/ documentation: https://solid-software.github.io/solid_lints/docs/intro topics: [lints, linter, lint, analysis, analyzer] diff --git a/test/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule_test.dart b/test/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule_test.dart new file mode 100644 index 00000000..2b27ba46 --- /dev/null +++ b/test/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule_test.dart @@ -0,0 +1,205 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(UseDescriptiveNamesForTypeParametersRuleTest); + }); +} + +@reflectiveTest +class UseDescriptiveNamesForTypeParametersRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + static const _mockAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + use_descriptive_names_for_type_parameters: + min_type_parameters: 3 + '''; + + @override + void setUp() { + rule = UseDescriptiveNamesForTypeParametersRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); + } + + Future test_reports_on_class() async { + await assertAutoDiagnostics(''' +class MyClass< + ${expectLint('T')}, + ${expectLint('U')}, + ${expectLint('V')} +> {} +'''); + } + + Future test_reports_on_class_type_alias() async { + await assertAutoDiagnostics(''' +class Base {} +mixin MyMixin {} +class MyAlias< + ${expectLint('T')}, + ${expectLint('U')}, + ${expectLint('V')} +> = Base with MyMixin; +'''); + } + + Future test_reports_on_enum() async { + await assertAutoDiagnostics(''' +enum MyEnum< + ${expectLint('T')}, + ${expectLint('U')}, + ${expectLint('V')} +> { + a, b +} +'''); + } + + Future test_reports_on_method() async { + await assertAutoDiagnostics(''' +class MyClass { + void myMethod< + ${expectLint('T')}, + ${expectLint('U')}, + ${expectLint('V')} + >() {} +} +'''); + } + + Future test_reports_on_function() async { + await assertAutoDiagnostics(''' +void myFunction< + ${expectLint('T')}, + ${expectLint('U')}, + ${expectLint('V')} +>() {} +'''); + } + + Future test_reports_on_mixin() async { + await assertAutoDiagnostics(''' +mixin MyMixin< + ${expectLint('T')}, + ${expectLint('U')}, + ${expectLint('V')} +> {} +'''); + } + + Future test_reports_on_extension() async { + await assertAutoDiagnostics(''' +extension MyExtension< + ${expectLint('T')}, + ${expectLint('U')}, + ${expectLint('V')} +> on List {} +'''); + } + + Future test_reports_on_generic_type_alias() async { + await assertAutoDiagnostics(''' +typedef MyAlias< + ${expectLint('T')}, + ${expectLint('U')}, + ${expectLint('V')} +> = Map; +'''); + } + + Future test_reports_on_function_type_alias() async { + await assertAutoDiagnostics(''' +typedef void MyFuncAlias< + ${expectLint('T')}, + ${expectLint('U')}, + ${expectLint('V')} +>(T a, U b, V c); +'''); + } + + Future test_reports_on_generic_function_type() async { + await assertAutoDiagnostics(''' +void test() { + final void Function< + ${expectLint('T')}, + ${expectLint('U')}, + ${expectLint('V')} + >(T, U, V) func; +} +'''); + } + + Future test_reports_on_anonymous_function() async { + await assertAutoDiagnostics(''' +void test() { + final func = < + ${expectLint('T')}, + ${expectLint('U')}, + ${expectLint('V')} + >(T a, U b, V c) => a; +} +'''); + } + + Future test_reports_on_extension_type() async { + await assertAutoDiagnostics(''' +extension type MyExtensionType< + ${expectLint('T')}, + ${expectLint('U')}, + ${expectLint('V')} +>(List list) {} +'''); + } + + Future test_does_not_report_on_descriptive_names() async { + await assertNoDiagnostics(''' +class MyClass {} +'''); + } + + Future test_does_not_report_on_wildcard() async { + await assertNoDiagnostics(''' +class MyClass {} +'''); + } + + Future test_does_not_report_below_threshold() async { + await assertNoDiagnostics(''' +class MyClass {} +'''); + } + + Future test_reports_when_min_type_parameters_is_1() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + use_descriptive_names_for_type_parameters: + min_type_parameters: 1''', + ); + + await assertAutoDiagnostics(''' +class MyClass<${expectLint('T')}> {} +'''); + } +} From d9f89645026b34b33e039bdb652f00b791c0eab8 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 22 Jul 2026 11:43:49 +0300 Subject: [PATCH 45/49] Fix GitHub Actions workflow (#322) * style: format code with updated dart formatter rules * chore: add ignore_for_file directive for todo comments in no_empty_block_rule.dart * chore: remove custom lint analysis step from CI workflow * chore: migrate flutter analyze and test commands to dart CLI in CI workflow * chore: upgrade actions/checkout to v4 in flutter workflow * chore: update actions/checkout to v7 in GitHub workflow * refactor: reorganize test directory structure and remove deprecated lint testing package * refactor: throw FormatException instead of Exception when type parsing fails * refactor: update todo ignore comments in no_empty_block lint documentation examples --------- Co-authored-by: Illia Aihistov --- .github/workflows/flutter.yaml | 9 +- .../analysis_options_parser.dart | 4 +- .../common/parameter_parser/rules_data.dart | 4 +- .../avoid_debug_print_in_release_rule.dart | 2 +- .../fixes/avoid_final_with_getter_fix.dart | 19 +- .../utils/getter_reference_id.dart | 5 +- .../avoid_final_with_getter_visitor.dart | 40 ++-- .../visitors/getter_variable_visitor.dart | 21 +- .../avoid_global_state_rule.dart | 8 +- .../avoid_late_keyword_rule.dart | 10 +- .../avoid_returning_widgets_visitor.dart | 8 +- .../avoid_unnecessary_set_state_rule.dart | 11 +- .../models/avoid_using_api_parameters.dart | 4 +- .../models/member_ordering_parameters.dart | 10 +- .../member_ordering/models/modifier.dart | 6 +- .../named_parameters_ordering_parameters.dart | 11 +- .../models/parameter_type.dart | 4 +- .../no_empty_block/no_empty_block_rule.dart | 10 +- .../models/no_magic_number_parameters.dart | 9 +- .../no_magic_number/no_magic_number_rule.dart | 8 +- .../prefer_early_return_rule.dart | 8 +- .../proper_super_calls_rule.dart | 10 +- .../visitors/proper_super_calls_visitor.dart | 3 +- .../utils/use_nearest_context_utils.dart | 5 +- lib/src/models/proxy_analysis_rule.dart | 10 +- lib/src/models/proxy_multi_analysis_rule.dart | 10 +- lib/src/models/solid_lint_rule.dart | 14 +- lib/src/models/solid_multi_lint_rule.dart | 13 +- lib/src/utils/correction_utils.dart | 7 +- lib/src/utils/docs_parser/main.dart | 5 +- .../docusaurus_formatter.dart | 11 +- .../output_formatters/markdown_formatter.dart | 1 - lib/src/utils/map_utils.dart | 12 +- lib/src/utils/named_type_utils.dart | 9 +- lib/src/utils/path_utils.dart | 11 +- lint_test/.gitignore | 45 ---- lint_test/.metadata | 30 --- lint_test/README.md | 3 - .../alphabetize_by_type_test.dart | 26 --- .../analysis_options.yaml | 8 - lint_test/analysis_options.yaml | 91 -------- ...id_debug_print_in_release_prefix_test.dart | 53 ----- lint_test/named_parameters_ordering_test.dart | 133 ----------- lint_test/pubspec.yaml | 15 -- test/lints/auto_lint_data.dart | 19 -- .../analysis_options_loader_test.dart | 210 +++++++----------- .../parameters/parameters_parsing_test.dart | 5 +- ...void_debug_print_in_release_rule_test.dart | 2 +- .../avoid_final_with_getter_rule_test.dart | 2 +- .../avoid_global_state_rule_test.dart | 2 +- .../avoid_late_keyword_rule_test.dart | 2 +- .../avoid_non_null_assertion_rule_test.dart | 2 +- .../avoid_returning_widgets_rule_test.dart | 2 +- ...unnecessary_return_variable_rule_test.dart | 2 +- ...avoid_unnecessary_set_state_rule_test.dart | 2 +- ...unnecessary_type_assertions_rule_test.dart | 2 +- ...d_unrelated_type_assertions_rule_test.dart | 2 +- .../avoid_unused_parameters_rule_test.dart | 2 +- .../avoid_using_api_rule_test.dart | 2 +- .../cyclomatic_complexity_rule_test.dart | 2 +- .../double_literal_format_rule_test.dart | 2 +- .../member_ordering_rule_test.dart | 2 +- .../named_parameters_ordering_rule_test.dart | 2 +- .../newline_before_return_rule_test.dart | 2 +- .../no_empty_block_rule_test.dart | 2 +- .../no_equal_then_else_rule_test.dart | 2 +- .../no_magic_number_rule_test.dart | 2 +- .../number_of_parameters_rule_test.dart | 2 +- ...fer_conditional_expressions_rule_test.dart | 2 +- .../prefer_early_return_rule_test.dart | 2 +- .../prefer_first/prefer_first_rule_test.dart | 2 +- .../prefer_last/prefer_last_rule_test.dart | 2 +- .../prefer_match_file_name_rule_test.dart | 5 +- .../proper_super_calls_rule_test.dart | 2 +- ...e_names_for_type_parameters_rule_test.dart | 2 +- .../use_nearest_context_rule_test.dart | 2 +- test/src/utils/auto_lint_data.dart | 2 + .../utils}/auto_test_lint_offsets.dart | 2 +- test/src/utils/code_generators.dart | 3 +- .../utils/fake_analysis_options_loader.dart | 3 +- .../utils/table_driven_rule_test_base.dart | 2 +- tool/generate_web_docs_content.dart | 6 +- 82 files changed, 290 insertions(+), 759 deletions(-) delete mode 100644 lint_test/.gitignore delete mode 100644 lint_test/.metadata delete mode 100644 lint_test/README.md delete mode 100644 lint_test/alphabetize_by_type_test/alphabetize_by_type_test.dart delete mode 100644 lint_test/alphabetize_by_type_test/analysis_options.yaml delete mode 100644 lint_test/analysis_options.yaml delete mode 100644 lint_test/avoid_debug_print_in_release_test/avoid_debug_print_in_release_prefix_test.dart delete mode 100644 lint_test/named_parameters_ordering_test.dart delete mode 100644 lint_test/pubspec.yaml delete mode 100644 test/lints/auto_lint_data.dart rename test/{ => src}/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart (98%) rename test/{ => src}/lints/avoid_global_state/avoid_global_state_rule_test.dart (97%) rename test/{ => src/lints/avoid_late_keyword_rule_test}/avoid_late_keyword_rule_test.dart (99%) rename test/{ => src}/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart (98%) rename test/{ => src}/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart (99%) rename test/{ => src}/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart (99%) rename test/{ => src}/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart (98%) rename test/{ => src}/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart (97%) rename test/{ => src}/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart (99%) rename test/{ => src}/lints/double_literal_format/double_literal_format_rule_test.dart (98%) rename test/{ => src}/lints/newline_before_return/newline_before_return_rule_test.dart (98%) rename test/{ => src}/lints/no_equal_then_else/no_equal_then_else_rule_test.dart (97%) rename test/{ => src}/lints/prefer_early_return/prefer_early_return_rule_test.dart (99%) rename test/{ => src}/lints/prefer_first/prefer_first_rule_test.dart (97%) rename test/{ => src/lints}/prefer_last/prefer_last_rule_test.dart (98%) rename test/{ => src}/lints/proper_super_calls/proper_super_calls_rule_test.dart (98%) rename test/{lints => src/utils}/auto_test_lint_offsets.dart (98%) rename test/{ => src}/utils/fake_analysis_options_loader.dart (96%) diff --git a/.github/workflows/flutter.yaml b/.github/workflows/flutter.yaml index b0e90b60..90d95fe1 100644 --- a/.github/workflows/flutter.yaml +++ b/.github/workflows/flutter.yaml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v7 - name: Setup Flutter uses: subosito/flutter-action@v2 @@ -24,17 +24,14 @@ jobs: run: dart format . --set-exit-if-changed - name: Run default analyzer - run: flutter analyze - - - name: Run custom analyzer - run: dart run custom_lint + run: dart analyze - name: Run tests run: | # run tests if `test` folder exists if [ -d test ] then - flutter test -r expanded + dart test -r expanded else echo "Tests not found." fi diff --git a/lib/src/common/parameter_parser/analysis_options_parser.dart b/lib/src/common/parameter_parser/analysis_options_parser.dart index f3465d4d..0e002747 100644 --- a/lib/src/common/parameter_parser/analysis_options_parser.dart +++ b/lib/src/common/parameter_parser/analysis_options_parser.dart @@ -64,8 +64,8 @@ class AnalysisOptionsParser { Map _toStandardMap(Map map) => map.whereKeyType().map( - (key, value) => MapEntry(key, _toStandardType(value)), - ); + (key, value) => MapEntry(key, _toStandardType(value)), + ); Object? _toStandardType(Object? value) => switch (value) { Map() => _toStandardMap(value), diff --git a/lib/src/common/parameter_parser/rules_data.dart b/lib/src/common/parameter_parser/rules_data.dart index a589b3fe..d7e3c54b 100644 --- a/lib/src/common/parameter_parser/rules_data.dart +++ b/lib/src/common/parameter_parser/rules_data.dart @@ -10,7 +10,5 @@ class RulesData { const RulesData({required this.rules, required this.disabledRules}); /// Creates a new empty instance of [RulesData]. - const RulesData.empty() - : rules = const {}, - disabledRules = const {}; + const RulesData.empty() : rules = const {}, disabledRules = const {}; } diff --git a/lib/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart b/lib/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart index a5d3d44b..32557d93 100644 --- a/lib/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart +++ b/lib/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart @@ -43,7 +43,7 @@ class AvoidDebugPrintInReleaseRule extends AnalysisRule { /// Creates an instance of [AvoidDebugPrintInReleaseRule]. AvoidDebugPrintInReleaseRule() - : super(name: lintName, description: 'Avoid debugPrint in release mode.'); + : super(name: lintName, description: 'Avoid debugPrint in release mode.'); @override LintCode get diagnosticCode => _code; diff --git a/lib/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.dart b/lib/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.dart index dd810642..49d25888 100644 --- a/lib/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.dart +++ b/lib/src/lints/avoid_final_with_getter/fixes/avoid_final_with_getter_fix.dart @@ -29,16 +29,15 @@ class AvoidFinalWithGetterFix extends ResolvedCorrectionProducer { @override Future compute(ChangeBuilder builder) async { final getterNode = node; - if (getterNode - case MethodDeclaration( - isGetter: true, - declaredFragment: ExecutableFragment( - element: GetterElement( - isAbstract: false, - isPublic: true, - ), - ), - )) { + if (getterNode case MethodDeclaration( + isGetter: true, + declaredFragment: ExecutableFragment( + element: GetterElement( + isAbstract: false, + isPublic: true, + ), + ), + )) { final compilationUnit = node.thisOrAncestorOfType(); if (compilationUnit == null) return; diff --git a/lib/src/lints/avoid_final_with_getter/utils/getter_reference_id.dart b/lib/src/lints/avoid_final_with_getter/utils/getter_reference_id.dart index 0aa47984..4f25f0d6 100644 --- a/lib/src/lints/avoid_final_with_getter/utils/getter_reference_id.dart +++ b/lib/src/lints/avoid_final_with_getter/utils/getter_reference_id.dart @@ -12,9 +12,8 @@ extension GetterReferenceId on MethodDeclaration { statements: [ ReturnStatement(expression: final expr?), ], - ) - ) => - expr, + ), + ) => expr, _ => null, }; diff --git a/lib/src/lints/avoid_final_with_getter/visitors/avoid_final_with_getter_visitor.dart b/lib/src/lints/avoid_final_with_getter/visitors/avoid_final_with_getter_visitor.dart index ee1aa7de..03a201ef 100644 --- a/lib/src/lints/avoid_final_with_getter/visitors/avoid_final_with_getter_visitor.dart +++ b/lib/src/lints/avoid_final_with_getter/visitors/avoid_final_with_getter_visitor.dart @@ -19,17 +19,16 @@ class AvoidFinalWithGetterVisitor extends RecursiveAstVisitor { void visitMethodDeclaration(MethodDeclaration node) { super.visitMethodDeclaration(node); - if (node - case MethodDeclaration( - isGetter: true, - declaredFragment: ExecutableFragment( - element: ExecutableElement( - isAbstract: false, - isPublic: true, - ) - ), - getterReferenceId: final getterId?, - )) { + if (node case MethodDeclaration( + isGetter: true, + declaredFragment: ExecutableFragment( + element: ExecutableElement( + isAbstract: false, + isPublic: true, + ), + ), + getterReferenceId: final getterId?, + )) { _gettersPairLookup[getterId] = node; if (_fieldsPairLookup.containsKey(getterId)) { @@ -42,16 +41,15 @@ class AvoidFinalWithGetterVisitor extends RecursiveAstVisitor { void visitVariableDeclaration(VariableDeclaration node) { super.visitVariableDeclaration(node); - if (node - case VariableDeclaration( - declaredFragment: VariableFragment( - element: VariableElement( - isPrivate: true, - isFinal: true, - id: final variableId, - ) - ) - )) { + if (node case VariableDeclaration( + declaredFragment: VariableFragment( + element: VariableElement( + isPrivate: true, + isFinal: true, + id: final variableId, + ), + ), + )) { _fieldsPairLookup[variableId] = node; if (_gettersPairLookup[variableId] case final getter?) { diff --git a/lib/src/lints/avoid_final_with_getter/visitors/getter_variable_visitor.dart b/lib/src/lints/avoid_final_with_getter/visitors/getter_variable_visitor.dart index 0e9b4195..c9afd760 100644 --- a/lib/src/lints/avoid_final_with_getter/visitors/getter_variable_visitor.dart +++ b/lib/src/lints/avoid_final_with_getter/visitors/getter_variable_visitor.dart @@ -10,23 +10,22 @@ class GetterVariableVisitor extends RecursiveAstVisitor { /// Creates a new instance of [GetterVariableVisitor] GetterVariableVisitor(MethodDeclaration getter) - : _getterId = getter.getterReferenceId; + : _getterId = getter.getterReferenceId; /// Is there a variable associated with the getter VariableDeclaration? get variable => _variable; @override void visitVariableDeclaration(VariableDeclaration node) { - if (node - case VariableDeclaration( - declaredFragment: VariableFragment( - element: VariableElement( - isPrivate: true, - isFinal: true, - :final id, - ) - ) - ) when id == _getterId) { + if (node case VariableDeclaration( + declaredFragment: VariableFragment( + element: VariableElement( + isPrivate: true, + isFinal: true, + :final id, + ), + ), + ) when id == _getterId) { _variable = node; } diff --git a/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart b/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart index 6ee78241..d63f76bb 100644 --- a/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart +++ b/lib/src/lints/avoid_global_state/avoid_global_state_rule.dart @@ -48,10 +48,10 @@ class AvoidGlobalStateRule extends AnalysisRule { /// Creates an instance of [AvoidGlobalStateRule]. AvoidGlobalStateRule() - : super( - name: lintName, - description: 'Avoid top-level or static mutable variables ', - ); + : super( + name: lintName, + description: 'Avoid top-level or static mutable variables ', + ); @override LintCode get diagnosticCode => _code; diff --git a/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart b/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart index e1378562..e9552709 100644 --- a/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart +++ b/lib/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart @@ -58,11 +58,11 @@ class AvoidLateKeywordRule extends SolidLintRule { /// Creates an instance of [AvoidLateKeywordRule]. AvoidLateKeywordRule({required super.analysisOptionsLoader}) - : super.withParameters( - name: lintName, - description: 'Warns against using the late keyword.', - parametersParser: AvoidLateKeywordParameters.fromJson, - ); + : super.withParameters( + name: lintName, + description: 'Warns against using the late keyword.', + parametersParser: AvoidLateKeywordParameters.fromJson, + ); @override LintCode get diagnosticCode => _code; diff --git a/lib/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart b/lib/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart index b81f4c39..f637acfe 100644 --- a/lib/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart +++ b/lib/src/lints/avoid_returning_widgets/visitors/avoid_returning_widgets_visitor.dart @@ -43,8 +43,8 @@ class AvoidReturningWidgetsVisitor extends RecursiveAstVisitor { final returnType = switch (node) { Declaration( declaredFragment: ExecutableFragment( - element: ExecutableElement(type: FunctionType(:final returnType)) - ) + element: ExecutableElement(type: FunctionType(:final returnType)), + ), ) => returnType, MethodDeclaration(returnType: TypeAnnotation(:final type)) => type, @@ -70,8 +70,8 @@ class AvoidReturningWidgetsVisitor extends RecursiveAstVisitor { declaredFragment: Fragment( element: Element( name: final String name, - enclosingElement: final InterfaceElement enclosingElement - ) + enclosingElement: final InterfaceElement enclosingElement, + ), ), ) => enclosingElement.getInheritedMember( diff --git a/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart b/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart index 7ea8ac1d..eacba39f 100644 --- a/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart +++ b/lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart @@ -60,7 +60,8 @@ class AvoidUnnecessarySetStateRule extends AnalysisRule { static const lintName = 'avoid_unnecessary_setstate'; /// The message shown when the lint rule is triggered. - static const _lintMessage = 'Avoid calling unnecessary setState. ' + static const _lintMessage = + 'Avoid calling unnecessary setState. ' 'Consider changing the state directly.'; /// The lint code for this rule. @@ -71,10 +72,10 @@ class AvoidUnnecessarySetStateRule extends AnalysisRule { /// Creates a new instance of [AvoidUnnecessarySetStateRule]. AvoidUnnecessarySetStateRule() - : super( - name: lintName, - description: _lintMessage, - ); + : super( + name: lintName, + description: _lintMessage, + ); @override LintCode get diagnosticCode => _code; diff --git a/lib/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart b/lib/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart index f132b23c..6e9d074c 100644 --- a/lib/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart +++ b/lib/src/lints/avoid_using_api/models/avoid_using_api_parameters.dart @@ -47,9 +47,7 @@ class AvoidUsingApiParameters { final avoidUsingApi = json['avoid_using_api']; return AvoidUsingApiParameters( entries: List.from( - (json['entries'] as Iterable?) - ?.whereType>() - .map( + (json['entries'] as Iterable?)?.whereType>().map( (e) { if (e is YamlMap) { return AvoidUsingApiEntryParameters.fromJson(e.toMap()); diff --git a/lib/src/lints/member_ordering/models/member_ordering_parameters.dart b/lib/src/lints/member_ordering/models/member_ordering_parameters.dart index 647abec5..2613d3dd 100644 --- a/lib/src/lints/member_ordering/models/member_ordering_parameters.dart +++ b/lib/src/lints/member_ordering/models/member_ordering_parameters.dart @@ -56,11 +56,11 @@ class MemberOrderingParameters { /// Factory for creating empty/default parameters. factory MemberOrderingParameters.empty() => MemberOrderingParameters( - groupsOrder: MemberOrderingConfigParser.parseOrder(null), - widgetsGroupsOrder: MemberOrderingConfigParser.parseWidgetsOrder(null), - alphabetize: false, - alphabetizeByType: false, - ); + groupsOrder: MemberOrderingConfigParser.parseOrder(null), + widgetsGroupsOrder: MemberOrderingConfigParser.parseWidgetsOrder(null), + alphabetize: false, + alphabetizeByType: false, + ); /// Method for creating from json data factory MemberOrderingParameters.fromJson(Map json) => diff --git a/lib/src/lints/member_ordering/models/modifier.dart b/lib/src/lints/member_ordering/models/modifier.dart index f840f261..d7fe6be2 100644 --- a/lib/src/lints/member_ordering/models/modifier.dart +++ b/lib/src/lints/member_ordering/models/modifier.dart @@ -41,8 +41,10 @@ enum Modifier { const Modifier(this.type); /// Parses a String access modifier and returns instance of [Modifier] - static Modifier parse(String? name) => values - .firstWhere((type) => type.type == name, orElse: () => Modifier.unset); + static Modifier parse(String? name) => values.firstWhere( + (type) => type.type == name, + orElse: () => Modifier.unset, + ); } /// Logical implication operation for access modifier diff --git a/lib/src/lints/named_parameters_ordering/models/named_parameters_ordering_parameters.dart b/lib/src/lints/named_parameters_ordering/models/named_parameters_ordering_parameters.dart index 7ff0009f..cb16d227 100644 --- a/lib/src/lints/named_parameters_ordering/models/named_parameters_ordering_parameters.dart +++ b/lib/src/lints/named_parameters_ordering/models/named_parameters_ordering_parameters.dart @@ -39,10 +39,9 @@ class NamedParametersOrderingParameters { /// Method for creating from json data factory NamedParametersOrderingParameters.fromJson( Map json, - ) => - NamedParametersOrderingParameters( - order: NamedParametersConfigParser.parseOrder( - json[_orderConfig], - ), - ); + ) => NamedParametersOrderingParameters( + order: NamedParametersConfigParser.parseOrder( + json[_orderConfig], + ), + ); } diff --git a/lib/src/lints/named_parameters_ordering/models/parameter_type.dart b/lib/src/lints/named_parameters_ordering/models/parameter_type.dart index a72ea2ac..8f560db3 100644 --- a/lib/src/lints/named_parameters_ordering/models/parameter_type.dart +++ b/lib/src/lints/named_parameters_ordering/models/parameter_type.dart @@ -61,8 +61,8 @@ enum ParameterType { return ParameterType.defaultValue; case FieldFormalParameter(:final isRequired) || - FunctionTypedFormalParameter(:final isRequired) || - SimpleFormalParameter(:final isRequired): + FunctionTypedFormalParameter(:final isRequired) || + SimpleFormalParameter(:final isRequired): return isRequired ? ParameterType.required : ParameterType.nullable; } } diff --git a/lib/src/lints/no_empty_block/no_empty_block_rule.dart b/lib/src/lints/no_empty_block/no_empty_block_rule.dart index db3e670f..0f15e066 100644 --- a/lib/src/lints/no_empty_block/no_empty_block_rule.dart +++ b/lib/src/lints/no_empty_block/no_empty_block_rule.dart @@ -45,11 +45,13 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// #### GOOD: /// ```dart /// int fn() { +// ignore: todo /// // TODO: complete this /// } /// /// Function getCallback() { /// return () { +// ignore: todo /// // TODO: actually do something /// }; /// } @@ -81,10 +83,10 @@ class NoEmptyBlockRule extends SolidLintRule { NoEmptyBlockRule({ required super.analysisOptionsLoader, }) : super.withParameters( - name: lintName, - description: _code.problemMessage, - parametersParser: NoEmptyBlockParameters.fromJson, - ); + name: lintName, + description: _code.problemMessage, + parametersParser: NoEmptyBlockParameters.fromJson, + ); @override void registerNodeProcessors( diff --git a/lib/src/lints/no_magic_number/models/no_magic_number_parameters.dart b/lib/src/lints/no_magic_number/models/no_magic_number_parameters.dart index 50bdc93f..8d6938fb 100644 --- a/lib/src/lints/no_magic_number/models/no_magic_number_parameters.dart +++ b/lib/src/lints/no_magic_number/models/no_magic_number_parameters.dart @@ -49,9 +49,9 @@ class NoMagicNumberParameters { /// Creates an empty/default instance of [NoMagicNumberParameters] factory NoMagicNumberParameters.empty() => const NoMagicNumberParameters( - allowedNumbers: _defaultMagicNumbers, - allowedInWidgetParams: false, - ); + allowedNumbers: _defaultMagicNumbers, + allowedInWidgetParams: false, + ); /// Method for creating from json data factory NoMagicNumberParameters.fromJson(Map json) { @@ -62,8 +62,7 @@ class NoMagicNumberParameters { return NoMagicNumberParameters( allowedNumbers: allowedList, - allowedInWidgetParams: - json[_allowedInWidgetParamsConfigName] == true, + allowedInWidgetParams: json[_allowedInWidgetParamsConfigName] == true, ); } } diff --git a/lib/src/lints/no_magic_number/no_magic_number_rule.dart b/lib/src/lints/no_magic_number/no_magic_number_rule.dart index 57e5e9f9..9380fc03 100644 --- a/lib/src/lints/no_magic_number/no_magic_number_rule.dart +++ b/lib/src/lints/no_magic_number/no_magic_number_rule.dart @@ -125,10 +125,10 @@ class NoMagicNumberRule extends SolidLintRule { NoMagicNumberRule({ required super.analysisOptionsLoader, }) : super.withParameters( - name: lintName, - description: 'Forbids having numbers without variable.', - parametersParser: NoMagicNumberParameters.fromJson, - ); + name: lintName, + description: 'Forbids having numbers without variable.', + parametersParser: NoMagicNumberParameters.fromJson, + ); @override void registerNodeProcessors( diff --git a/lib/src/lints/prefer_early_return/prefer_early_return_rule.dart b/lib/src/lints/prefer_early_return/prefer_early_return_rule.dart index 582378df..91f26aec 100644 --- a/lib/src/lints/prefer_early_return/prefer_early_return_rule.dart +++ b/lib/src/lints/prefer_early_return/prefer_early_return_rule.dart @@ -43,10 +43,10 @@ class PreferEarlyReturnRule extends AnalysisRule { /// Creates an instance of [PreferEarlyReturnRule] PreferEarlyReturnRule() - : super( - name: lintName, - description: 'Use reverse if to reduce nesting', - ); + : super( + name: lintName, + description: 'Use reverse if to reduce nesting', + ); @override LintCode get diagnosticCode => _code; diff --git a/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart b/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart index fb82e8e8..90b04a9c 100644 --- a/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart +++ b/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart @@ -61,11 +61,11 @@ class ProperSuperCallsRule extends AnalysisRule { /// Creates an instance of [ProperSuperCallsRule]. ProperSuperCallsRule() - : super( - name: lintName, - description: - 'Ensures proper ordering of Flutter lifecycle super calls.', - ); + : super( + name: lintName, + description: + 'Ensures proper ordering of Flutter lifecycle super calls.', + ); @override LintCode get diagnosticCode => superInitStateCode; diff --git a/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart b/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart index 871938ad..3fe9b217 100644 --- a/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart +++ b/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart @@ -83,7 +83,8 @@ class ProperSuperCallsVisitor extends SimpleAstVisitor { } bool _isStateSubclass(InterfaceType supertype) { - final isStateSubclass = supertype.element.name == _flutterStateClass || + final isStateSubclass = + supertype.element.name == _flutterStateClass || supertype.allSupertypes.any( (t) => t.element.name == _flutterStateClass, ); diff --git a/lib/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart b/lib/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart index 8e79d9d8..d5b4652f 100644 --- a/lib/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart +++ b/lib/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart @@ -9,8 +9,9 @@ SimpleFormalParameter? findClosestBuildContext(AstNode node) { if (current is FunctionExpression) { final functionParams = current.parameters?.parameters ?? []; for (final param in functionParams) { - final actualParam = - param is DefaultFormalParameter ? param.parameter : param; + final actualParam = param is DefaultFormalParameter + ? param.parameter + : param; if (actualParam is SimpleFormalParameter && isBuildContext(actualParam.declaredFragment?.element.type)) { return actualParam; diff --git a/lib/src/models/proxy_analysis_rule.dart b/lib/src/models/proxy_analysis_rule.dart index 2bdd2d4e..47376dd1 100644 --- a/lib/src/models/proxy_analysis_rule.dart +++ b/lib/src/models/proxy_analysis_rule.dart @@ -17,11 +17,11 @@ class ProxyAnalysisRule extends AnalysisRule { /// Creates a new instance of [ProxyAnalysisRule]. ProxyAnalysisRule(this.delegate, this.loader) - : super( - name: delegate.name, - description: delegate.description, - state: delegate.state, - ); + : super( + name: delegate.name, + description: delegate.description, + state: delegate.state, + ); @override DiagnosticCode get diagnosticCode => delegate.diagnosticCode; diff --git a/lib/src/models/proxy_multi_analysis_rule.dart b/lib/src/models/proxy_multi_analysis_rule.dart index e497590b..5f25dddb 100644 --- a/lib/src/models/proxy_multi_analysis_rule.dart +++ b/lib/src/models/proxy_multi_analysis_rule.dart @@ -17,11 +17,11 @@ class ProxyMultiAnalysisRule extends MultiAnalysisRule { /// Creates a new instance of [ProxyMultiAnalysisRule]. ProxyMultiAnalysisRule(this.delegate, this.loader) - : super( - name: delegate.name, - description: delegate.description, - state: delegate.state, - ); + : super( + name: delegate.name, + description: delegate.description, + state: delegate.state, + ); @override List get diagnosticCodes => delegate.diagnosticCodes; diff --git a/lib/src/models/solid_lint_rule.dart b/lib/src/models/solid_lint_rule.dart index b4bf288e..2b705805 100644 --- a/lib/src/models/solid_lint_rule.dart +++ b/lib/src/models/solid_lint_rule.dart @@ -15,8 +15,8 @@ abstract class SolidLintRule extends AnalysisRule { required super.name, required super.description, super.state, - }) : _analysisOptionsLoader = null, - _parametersParser = null; + }) : _analysisOptionsLoader = null, + _parametersParser = null; /// Constructor for [SolidLintRule] model with parameters. SolidLintRule.withParameters({ @@ -25,15 +25,17 @@ abstract class SolidLintRule extends AnalysisRule { required super.name, required super.description, super.state, - }) : _analysisOptionsLoader = analysisOptionsLoader, - _parametersParser = parametersParser; + }) : _analysisOptionsLoader = analysisOptionsLoader, + _parametersParser = parametersParser; /// Reads the rule parameters from analysis options and parses them to [T] T? getParametersForContext(RuleContext context) { _analysisOptionsLoader?.loadRulesOptionsFromContext(context); - final unparsedParameters = - _analysisOptionsLoader?.getRuleOptions(context, name); + final unparsedParameters = _analysisOptionsLoader?.getRuleOptions( + context, + name, + ); if (unparsedParameters == null) return null; return _parametersParser?.call(unparsedParameters); diff --git a/lib/src/models/solid_multi_lint_rule.dart b/lib/src/models/solid_multi_lint_rule.dart index 244c9377..947a4915 100644 --- a/lib/src/models/solid_multi_lint_rule.dart +++ b/lib/src/models/solid_multi_lint_rule.dart @@ -8,8 +8,7 @@ import 'package:solid_lints/src/models/rule_parameters_parser.dart'; /// /// Mirrors SolidLintRule but extends MultiAnalysisRule instead of /// AnalysisRule, allowing rules to define multiple diagnostic codes. -abstract class SolidMultiLintRule - extends MultiAnalysisRule { +abstract class SolidMultiLintRule extends MultiAnalysisRule { final AnalysisOptionsLoader _analysisOptionsLoader; final RuleParametersParser _parametersParser; @@ -21,15 +20,17 @@ abstract class SolidMultiLintRule required super.name, required super.description, super.state, - }) : _analysisOptionsLoader = analysisOptionsLoader, - _parametersParser = parametersParser; + }) : _analysisOptionsLoader = analysisOptionsLoader, + _parametersParser = parametersParser; /// Reads the rule parameters from analysis options and parses them to [T]. T? getParametersForContext(RuleContext context) { _analysisOptionsLoader.loadRulesOptionsFromContext(context); - final unparsedParameters = - _analysisOptionsLoader.getRuleOptions(context, name); + final unparsedParameters = _analysisOptionsLoader.getRuleOptions( + context, + name, + ); if (unparsedParameters == null) return null; return _parametersParser(unparsedParameters); diff --git a/lib/src/utils/correction_utils.dart b/lib/src/utils/correction_utils.dart index 80bfe87c..5ba2c945 100644 --- a/lib/src/utils/correction_utils.dart +++ b/lib/src/utils/correction_utils.dart @@ -21,9 +21,7 @@ extension CorrectionUtilsExtension on CorrectionUtils { }) { if (node.end >= nextOffset) return null; - final sameLine = getTextRange(node.end, nextOffset) - .split('\n') - .first; + final sameLine = getTextRange(node.end, nextOffset).split('\n').first; final commentIdx = sameLine.indexOf('//'); @@ -55,7 +53,8 @@ extension CorrectionUtilsExtension on CorrectionUtils { required Token? leadingComment, required int minOffset, }) { - final hasValidComment = leadingComment != null && + final hasValidComment = + leadingComment != null && leadingComment.offset >= minOffset && leadingComment.offset < node.offset; diff --git a/lib/src/utils/docs_parser/main.dart b/lib/src/utils/docs_parser/main.dart index abf91055..1bdd4a3d 100644 --- a/lib/src/utils/docs_parser/main.dart +++ b/lib/src/utils/docs_parser/main.dart @@ -5,8 +5,9 @@ import 'package:solid_lints/src/utils/docs_parser/output_formatters/markdown_for import 'package:solid_lints/src/utils/docs_parser/parsers/docs_parser.dart'; void main() async { - final dir = - Directory(normalize(join(Directory.current.path, 'lib', 'src', 'lints'))); + final dir = Directory( + normalize(join(Directory.current.path, 'lib', 'src', 'lints')), + ); final parser = DocsParser( formatter: MarkdownFormatter(), diff --git a/lib/src/utils/docs_parser/output_formatters/docusaurus_formatter.dart b/lib/src/utils/docs_parser/output_formatters/docusaurus_formatter.dart index 33abfab3..b57bf984 100644 --- a/lib/src/utils/docs_parser/output_formatters/docusaurus_formatter.dart +++ b/lib/src/utils/docs_parser/output_formatters/docusaurus_formatter.dart @@ -24,8 +24,8 @@ sidebar_position: 0 DocusaurusFormatter({ required String docusaurusDocsDirPath, required String readmePath, - }) : _outputDirectory = Directory(docusaurusDocsDirPath), - _readmeFile = File(readmePath); + }) : _outputDirectory = Directory(docusaurusDocsDirPath), + _readmeFile = File(readmePath); @override void format(List rules) { @@ -40,9 +40,10 @@ sidebar_position: 0 rules.forEach(_createMarkdownFileForRule); } - void _createMarkdownFileForRule(RuleDoc rule) => File( - join(_outputDirectory.path, '${rule.name}.md'), - ) + void _createMarkdownFileForRule(RuleDoc rule) => + File( + join(_outputDirectory.path, '${rule.name}.md'), + ) ..createSync() ..writeAsString( _markdownFormatter.formatRuleToMarkdown( diff --git a/lib/src/utils/docs_parser/output_formatters/markdown_formatter.dart b/lib/src/utils/docs_parser/output_formatters/markdown_formatter.dart index 64442503..e82d5dd5 100644 --- a/lib/src/utils/docs_parser/output_formatters/markdown_formatter.dart +++ b/lib/src/utils/docs_parser/output_formatters/markdown_formatter.dart @@ -55,5 +55,4 @@ class MarkdownFormatter implements RulesDocumentationFormatter { return formattedString.toString(); } - } diff --git a/lib/src/utils/map_utils.dart b/lib/src/utils/map_utils.dart index 9b5c37d5..0e6928d6 100644 --- a/lib/src/utils/map_utils.dart +++ b/lib/src/utils/map_utils.dart @@ -3,14 +3,14 @@ extension MapExtensions on Map { /// Filters the map entries keeping only those whose keys are of type [U], /// and casts the resulting map's keys to [U]. Map whereKeyType() => { - for (final MapEntry(:key, :value) in entries) - if (key is U) key: value, - }; + for (final MapEntry(:key, :value) in entries) + if (key is U) key: value, + }; /// Filters the map entries keeping only those whose values are of type [W], /// and casts the resulting map's values to [W]. Map whereValueType() => { - for (final MapEntry(:key, :value) in entries) - if (value is W) key: value, - }; + for (final MapEntry(:key, :value) in entries) + if (value is W) key: value, + }; } diff --git a/lib/src/utils/named_type_utils.dart b/lib/src/utils/named_type_utils.dart index 574a40a2..05680a5c 100644 --- a/lib/src/utils/named_type_utils.dart +++ b/lib/src/utils/named_type_utils.dart @@ -12,9 +12,12 @@ NamedType parseNamedTypeFromString(String typeString) { return namedTypeFinder.foundNamedType!; } catch (_) { - throw Exception("No NamedType could be parsed from the input " - "typeString: '$typeString'. Ensure it's a valid Dart " - "type declaration."); + throw FormatException( + "No NamedType could be parsed from the input " + "typeString: '$typeString'. Ensure it's a valid Dart " + "type declaration.", + typeString, + ); } } diff --git a/lib/src/utils/path_utils.dart b/lib/src/utils/path_utils.dart index e6ba3a67..066e5199 100644 --- a/lib/src/utils/path_utils.dart +++ b/lib/src/utils/path_utils.dart @@ -23,11 +23,13 @@ bool shouldSkipFile({ final relative = relativePath(path, rootPath); - final matchesInclude = includes.isEmpty || + final matchesInclude = + includes.isEmpty || _matchesAny(includes, relative) || _matchesAny(includes, path); - final matchesExclude = excludes.isNotEmpty && + final matchesExclude = + excludes.isNotEmpty && (_matchesAny(excludes, relative) || _matchesAny(excludes, path)); return !matchesInclude || matchesExclude; @@ -40,8 +42,9 @@ bool _matchesAny(List globs, String path) => /// replaces backslashes with forward slashes String relativePath(String path, [String? root]) { final uriNormalizedPath = p.toUri(path).normalizePath().path; - final uriNormalizedRoot = - root != null ? p.toUri(root).normalizePath().path : null; + final uriNormalizedRoot = root != null + ? p.toUri(root).normalizePath().path + : null; return p.posix.relative(uriNormalizedPath, from: uriNormalizedRoot); } diff --git a/lint_test/.gitignore b/lint_test/.gitignore deleted file mode 100644 index 3820a95c..00000000 --- a/lint_test/.gitignore +++ /dev/null @@ -1,45 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ -/coverage/ - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/lint_test/.metadata b/lint_test/.metadata deleted file mode 100644 index 0bb2266c..00000000 --- a/lint_test/.metadata +++ /dev/null @@ -1,30 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "05db9689081f091050f01aed79f04dce0c750154" - channel: "stable" - -project_type: app - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 05db9689081f091050f01aed79f04dce0c750154 - base_revision: 05db9689081f091050f01aed79f04dce0c750154 - - platform: web - create_revision: 05db9689081f091050f01aed79f04dce0c750154 - base_revision: 05db9689081f091050f01aed79f04dce0c750154 - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/lint_test/README.md b/lint_test/README.md deleted file mode 100644 index 9002c39f..00000000 --- a/lint_test/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# solid_lints_test - -A new Flutter project. diff --git a/lint_test/alphabetize_by_type_test/alphabetize_by_type_test.dart b/lint_test/alphabetize_by_type_test/alphabetize_by_type_test.dart deleted file mode 100644 index e26d062e..00000000 --- a/lint_test/alphabetize_by_type_test/alphabetize_by_type_test.dart +++ /dev/null @@ -1,26 +0,0 @@ -// ignore_for_file: unused_field -// ignore_for_file: unused_element -// ignore_for_file: prefer_match_file_name - -/// Check the `member_ordering` rule -/// alphabetical-by-type option enabled - -class CorrectAlphabeticalByTypeClass { - final double e = 1; - final int a = 1; -} - -class WrongAlphabeticalByTypeClass { - final int e = 1; - - // expect_lint: member_ordering - final double a = 1; -} - -class PartiallyWrongAlphabeticalByTypeClass { - final int a = 1; - final String str = 's'; - - // expect_lint: member_ordering - final double e = 1; -} diff --git a/lint_test/alphabetize_by_type_test/analysis_options.yaml b/lint_test/alphabetize_by_type_test/analysis_options.yaml deleted file mode 100644 index 000cfecb..00000000 --- a/lint_test/alphabetize_by_type_test/analysis_options.yaml +++ /dev/null @@ -1,8 +0,0 @@ -analyzer: - plugins: - - ../custom_lint - -custom_lint: - rules: - - member_ordering: - alphabetize_by_type: true diff --git a/lint_test/analysis_options.yaml b/lint_test/analysis_options.yaml deleted file mode 100644 index 788633dc..00000000 --- a/lint_test/analysis_options.yaml +++ /dev/null @@ -1,91 +0,0 @@ -analyzer: - plugins: - - custom_lint - -custom_lint: - rules: - - cyclomatic_complexity: - max_complexity: 4 - exclude: - - class_name: Exclude - method_name: excludeMethod - - method_name: excludeMethod - - number_of_parameters: - max_parameters: 2 - exclude: - - class_name: Exclude - method_name: avoidNumberOfParameters - - method_name: avoidNumberOfParameters - - function_lines_of_code: - max_lines: 50 - - avoid_non_null_assertion - - avoid_late_keyword: - allow_initialized: true - - avoid_global_state - - avoid_returning_widgets: - exclude: - - class_name: ExcludeWidget - method_name: excludeWidgetMethod - - method_name: excludeMethod - - avoid_unnecessary_setstate - - double_literal_format - - avoid_unnecessary_type_assertions - - avoid_unrelated_type_assertions - - avoid_unused_parameters: - exclude: - - class_name: Exclude - method_name: excludeMethod - - method_name: excludeMethod - - simpleMethodName - - SimpleClassName - - exclude - - newline_before_return - - no_empty_block: - allow_with_comments: true - exclude: - - class_name: Exclude - method_name: excludeMethod - - method_name: excludeMethod - - no_equal_then_else - - avoid_debug_print_in_release - - prefer_early_return - - member_ordering: - alphabetize: true - order: - - public_fields - - private_fields - - constructors - - getters - - setters - - public_methods - - private_methods - - close_method - widgets_order: - - const_fields - - static_fields - - static_methods - - public_fields - - private_fields - - public_methods - - private_methods - - constructors - - build_method - - init_state_method - - did_change_dependencies_method - - did_update_widget_method - - dispose_method - - no_magic_number - - prefer_conditional_expressions - - prefer_first - - prefer_last - - prefer_match_file_name - - proper_super_calls - - avoid_final_with_getter - - named_parameters_ordering: - order: - - required_super - - super - - required - - nullable - - default - - avoid_unnecessary_return_variable diff --git a/lint_test/avoid_debug_print_in_release_test/avoid_debug_print_in_release_prefix_test.dart b/lint_test/avoid_debug_print_in_release_test/avoid_debug_print_in_release_prefix_test.dart deleted file mode 100644 index 01b162aa..00000000 --- a/lint_test/avoid_debug_print_in_release_test/avoid_debug_print_in_release_prefix_test.dart +++ /dev/null @@ -1,53 +0,0 @@ -// ignore_for_file: unused_local_variable - -import 'package:flutter/foundation.dart' as f; - -/// Test the avoid_debug_print_in_release -void avoidDebugPrintTest() { - // expect_lint: avoid_debug_print_in_release - f.debugPrint(''); - - // expect_lint: avoid_debug_print_in_release - final test = f.debugPrint; - - test('test'); - - // expect_lint: avoid_debug_print_in_release - final test2 = f.debugPrint(''); - - debugPrint(); - - debugPrint; - - if (!f.kReleaseMode) { - f.debugPrint(''); - - final test = f.debugPrint; - - var test2; - - test2 = debugPrint; - - test.call('test'); - - final test3 = f.debugPrint(''); - - if (true) { - f.debugPrint(''); - - final test = f.debugPrint; - - var test2; - - test2 = debugPrint; - - test.call('test'); - - final test3 = f.debugPrint(''); - } - } -} - -void debugPrint() { - return; -} diff --git a/lint_test/named_parameters_ordering_test.dart b/lint_test/named_parameters_ordering_test.dart deleted file mode 100644 index 3ea72264..00000000 --- a/lint_test/named_parameters_ordering_test.dart +++ /dev/null @@ -1,133 +0,0 @@ -// ignore_for_file: unused_field, prefer_match_file_name, proper_super_calls, number_of_parameters, avoid_unused_parameters, member_ordering -// ignore_for_file: unused_element -// ignore_for_file: no_empty_block - -/// Check the `named_parameters_ordering` rule - -class User { - final String accountType; - final String? userId; - - User({ - this.userId, - // expect_lint: named_parameters_ordering - required this.accountType, - }); -} - -class UserProfile extends User { - final String? age; - final String? country; - final String email; - final bool isActive; - final String name; - final String? profileId; - - // no lint - UserProfile.orderedConstructor( - this.profileId, { - required super.accountType, - super.userId, - required this.name, - required this.email, - this.age, - this.country, - this.isActive = true, - }); - - UserProfile.partiallyOrderedConstructor( - this.profileId, { - required super.accountType, - required this.email, - this.age, - // expect_lint: named_parameters_ordering - required this.name, - this.country, - // expect_lint: named_parameters_ordering - super.userId, - this.isActive = true, - }); - - UserProfile.unorderedConstructor( - String profileId, { - this.age, - // expect_lint: named_parameters_ordering - super.userId, - // expect_lint: named_parameters_ordering - required super.accountType, - this.country, - // expect_lint: named_parameters_ordering - required this.name, - this.isActive = true, - // expect_lint: named_parameters_ordering - required this.email, - }) : profileId = profileId; - - // no lint - void orderedMethod({ - required String name, - required String email, - int? age, - bool isActive = true, - }) { - return; - } - - void partiallyOrderedMethod({ - required String name, - int? age, - // expect_lint: named_parameters_ordering - required String email, - bool isActive = true, - }) { - return; - } - - void unorderedMethod({ - int? age, - // expect_lint: named_parameters_ordering - required String email, - bool isActive = true, - // expect_lint: named_parameters_ordering - required String name, - }) { - return; - } - - void mixedParameters( - String accountType, - String? userId, { - int? age, - // expect_lint: named_parameters_ordering - required String email, - bool isActive = true, - // expect_lint: named_parameters_ordering - required String name, - }) { - return; - } -} - -void functionExample({ - required String name, - bool isActive = true, - // expect_lint: named_parameters_ordering - int? age, - // expect_lint: named_parameters_ordering - required String email, -}) { - return; -} - -void mixedParameters( - String accountType, - String? userId, { - int? age, - // expect_lint: named_parameters_ordering - required String email, - bool isActive = true, - // expect_lint: named_parameters_ordering - required String name, -}) { - return; -} diff --git a/lint_test/pubspec.yaml b/lint_test/pubspec.yaml deleted file mode 100644 index 10e64475..00000000 --- a/lint_test/pubspec.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: solid_lints_test -description: Test project for solid_lints rules -publish_to: none - -environment: - sdk: ">=3.5.0 <4.0.0" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - solid_lints: - path: ../ - test: ^1.20.1 diff --git a/test/lints/auto_lint_data.dart b/test/lints/auto_lint_data.dart deleted file mode 100644 index 61b900dc..00000000 --- a/test/lints/auto_lint_data.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart'; - -/// Holds data about a lint placeholder, including the original code -/// and optional diagnostic assertions. -class AutoLintData { - final String code; - final Pattern? correctionContains; - final List messageContainsAll; - final String? name; - final List? contextMessages; - - const AutoLintData({ - required this.code, - this.correctionContains, - this.messageContainsAll = const [], - this.name, - this.contextMessages, - }); -} diff --git a/test/src/common/parameter_parser/analysis_options_loader_test.dart b/test/src/common/parameter_parser/analysis_options_loader_test.dart index a47a26e0..6261eb31 100644 --- a/test/src/common/parameter_parser/analysis_options_loader_test.dart +++ b/test/src/common/parameter_parser/analysis_options_loader_test.dart @@ -20,7 +20,8 @@ class AnalysisOptionsLoaderTest extends PubPackageResolutionTest { static const _mockRule2Name = 'mock_rule_2'; static const _cyclomaticComplexityName = 'cyclomatic_complexity'; - static const _mockAnalysisOptionsContent = ''' + static const _mockAnalysisOptionsContent = + ''' plugins: solid_lints: diagnostics: @@ -38,7 +39,8 @@ plugins: method_name: mockMethod - method_name: mockMethod2 '''; - static const _mockDifferentAnalysisOptionsContent = ''' + static const _mockDifferentAnalysisOptionsContent = + ''' plugins: solid_lints: diagnostics: @@ -64,8 +66,9 @@ plugins: void setUp() { super.setUp(); - analysisOptionsLoader = - AnalysisOptionsLoader(resourceProvider: resourceProvider); + analysisOptionsLoader = AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ); mockRuleContext = _createMockContextForPackage(testPackageRootPath); _writeMockAnalysisOptionsYamlFile(); @@ -93,7 +96,7 @@ plugins: for (final ruleName in [ _mockRuleThatNeedsConfigName, _mockRule2Name, - _cyclomaticComplexityName + _cyclomaticComplexityName, ]) { final currentPackageOptions = analysisOptionsLoader.getRuleOptions( mockRuleContext, @@ -104,10 +107,7 @@ plugins: ruleName, ); - expect( - currentPackageOptions, - isNot(equals(otherPackageOptions)), - ); + expect(currentPackageOptions, isNot(equals(otherPackageOptions))); } } @@ -135,7 +135,7 @@ plugins: 'foo': 'bar', 'exclude': [ {'class_name': 'MockClass', 'method_name': 'mockMethod'}, - ] + ], }); expect(cyclomaticComplexityOptions, isNotNull); @@ -144,7 +144,7 @@ plugins: 'exclude': [ {'class_name': 'MockClass', 'method_name': 'mockMethod'}, {'method_name': 'mockMethod2'}, - ] + ], }); } @@ -182,13 +182,10 @@ plugins: } void test_does_not_crash_when_plugins_is_list() { - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' plugins: - solid_lints -''', - ); +'''); analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); final options = analysisOptionsLoader.getRuleOptions( mockRuleContext, @@ -198,12 +195,9 @@ plugins: } void test_does_not_crash_when_solid_lints_is_boolean() { - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' solid_lints: true -''', - ); +'''); analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); final options = analysisOptionsLoader.getRuleOptions( mockRuleContext, @@ -213,17 +207,14 @@ solid_lints: true } void test_does_not_crash_when_rule_option_has_non_string_key() { - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' plugins: solid_lints: diagnostics: $_mockRuleThatNeedsConfigName: 123: true some_parameter: root_value -''', - ); +'''); analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); final options = analysisOptionsLoader.getRuleOptions( mockRuleContext, @@ -279,22 +270,16 @@ plugins: void test_resolve_include_relative_path() { final includedOptionsPath = '$testPackageRootPath/included_options.yaml'; - newFile( - includedOptionsPath, - ''' + newFile(includedOptionsPath, ''' solid_lints: diagnostics: $_mockRuleThatNeedsConfigName: some_parameter: included_value -''', - ); +'''); - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' include: included_options.yaml -''', - ); +'''); analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); @@ -310,19 +295,14 @@ include: included_options.yaml void test_resolve_include_package_path() { final sharedPackageRoot = '/home/shared'; newFolder(sharedPackageRoot); - newFile( - '$sharedPackageRoot/lib/analysis_options.yaml', - ''' + newFile('$sharedPackageRoot/lib/analysis_options.yaml', ''' solid_lints: diagnostics: $_mockRuleThatNeedsConfigName: some_parameter: package_value -''', - ); +'''); - newPackageConfigJsonFile( - testPackageRootPath, - ''' + newPackageConfigJsonFile(testPackageRootPath, ''' { "configVersion": 2, "packages": [ @@ -333,15 +313,11 @@ solid_lints: } ] } -''', - ); +'''); - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' include: package:shared/analysis_options.yaml -''', - ); +'''); analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); @@ -355,12 +331,9 @@ include: package:shared/analysis_options.yaml } void test_resolve_include_malformed_package_uri() { - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' include: package:foo:bar/baz.yaml -''', - ); +'''); // Should not throw FormatException analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); @@ -374,16 +347,13 @@ include: package:foo:bar/baz.yaml } void test_resolve_include_cyclic() { - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' include: analysis_options.yaml solid_lints: diagnostics: $_mockRuleThatNeedsConfigName: some_parameter: cyclic_value -''', - ); +'''); analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); @@ -397,38 +367,38 @@ solid_lints: } void test_isRuleDisabled_when_set_to_false() { - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' solid_lints: diagnostics: $_mockRuleThatNeedsConfigName: false -''', - ); +'''); analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); expect( - analysisOptionsLoader.isRuleDisabled(mockRuleContext, _mockRuleThatNeedsConfigName), + analysisOptionsLoader.isRuleDisabled( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ), isTrue, ); } void test_isRuleDisabled_when_suppressed_in_analyzer_errors() { - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' analyzer: errors: solid_lints/$_mockRuleThatNeedsConfigName: ignore solid_lints/$_mockRule2Name: ignore -''', - ); +'''); analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); expect( - analysisOptionsLoader.isRuleDisabled(mockRuleContext, _mockRuleThatNeedsConfigName), + analysisOptionsLoader.isRuleDisabled( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ), isTrue, ); expect( @@ -438,103 +408,90 @@ analyzer: } void test_isRuleDisabled_when_suppressed_in_analyzer_errors_as_false() { - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' analyzer: errors: solid_lints/$_mockRuleThatNeedsConfigName: false -''', - ); +'''); analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); expect( - analysisOptionsLoader.isRuleDisabled(mockRuleContext, _mockRuleThatNeedsConfigName), + analysisOptionsLoader.isRuleDisabled( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ), isTrue, ); } - void test_isRuleDisabled_when_suppressed_in_included_analyzer_errors() { final includedOptionsPath = '$testPackageRootPath/included_options.yaml'; - newFile( - includedOptionsPath, - ''' + newFile(includedOptionsPath, ''' analyzer: errors: solid_lints/$_mockRuleThatNeedsConfigName: ignore -''', - ); +'''); - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' include: included_options.yaml -''', - ); +'''); analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); expect( - analysisOptionsLoader.isRuleDisabled(mockRuleContext, _mockRuleThatNeedsConfigName), + analysisOptionsLoader.isRuleDisabled( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ), isTrue, ); } void test_isRuleDisabled_when_disabled_in_include_but_re_enabled_with_null() { final includedOptionsPath = '$testPackageRootPath/included_options.yaml'; - newFile( - includedOptionsPath, - ''' + newFile(includedOptionsPath, ''' solid_lints: diagnostics: $_mockRuleThatNeedsConfigName: false -''', - ); +'''); - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' include: included_options.yaml solid_lints: diagnostics: $_mockRuleThatNeedsConfigName: -''', - ); +'''); analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); expect( - analysisOptionsLoader.isRuleDisabled(mockRuleContext, _mockRuleThatNeedsConfigName), + analysisOptionsLoader.isRuleDisabled( + mockRuleContext, + _mockRuleThatNeedsConfigName, + ), isFalse, ); } void test_options_merging_with_include() { final includedOptionsPath = '$testPackageRootPath/included_options.yaml'; - newFile( - includedOptionsPath, - ''' + newFile(includedOptionsPath, ''' solid_lints: diagnostics: $_mockRuleThatNeedsConfigName: param_a: val_a param_b: val_b -''', - ); +'''); - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' include: included_options.yaml solid_lints: diagnostics: $_mockRuleThatNeedsConfigName: param_b: local_val_b param_c: val_c -''', - ); +'''); analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); @@ -553,30 +510,22 @@ solid_lints: void test_options_merging_with_multiple_includes() { final includedOptionsPath1 = '$testPackageRootPath/included_options_1.yaml'; final includedOptionsPath2 = '$testPackageRootPath/included_options_2.yaml'; - newFile( - includedOptionsPath1, - ''' + newFile(includedOptionsPath1, ''' solid_lints: diagnostics: $_mockRuleThatNeedsConfigName: param_a: val_a param_b: val_b -''', - ); - newFile( - includedOptionsPath2, - ''' +'''); + newFile(includedOptionsPath2, ''' solid_lints: diagnostics: $_mockRuleThatNeedsConfigName: param_b: val_b_2 param_c: val_c -''', - ); +'''); - newAnalysisOptionsYamlFile( - testPackageRootPath, - ''' + newAnalysisOptionsYamlFile(testPackageRootPath, ''' include: - included_options_1.yaml - included_options_2.yaml @@ -585,8 +534,7 @@ solid_lints: $_mockRuleThatNeedsConfigName: param_c: local_val_c param_d: val_d -''', - ); +'''); analysisOptionsLoader.loadRulesOptionsFromContext(mockRuleContext); @@ -610,7 +558,8 @@ solid_lints: final rootFolder = getFolder(packageRootPath); return _TestRuleContext( _TestWorkspacePackage(rootFolder), - definingUnit: definingUnit ?? + definingUnit: + definingUnit ?? _TestRuleContextUnit( rootFolder.getChildAssumingFile('lib/dummy.dart'), ), @@ -625,10 +574,7 @@ class _TestRuleContext implements RuleContext { @override final RuleContextUnit definingUnit; - _TestRuleContext( - this.package, { - required this.definingUnit, - }); + _TestRuleContext(this.package, {required this.definingUnit}); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); diff --git a/test/src/common/parameters/parameters_parsing_test.dart b/test/src/common/parameters/parameters_parsing_test.dart index f64d303a..75a027c4 100644 --- a/test/src/common/parameters/parameters_parsing_test.dart +++ b/test/src/common/parameters/parameters_parsing_test.dart @@ -9,7 +9,10 @@ void main() { final param = ExcludedAnnotationsListParameter.fromJson({ 'exclude_annotation': ['MyAnnotation1', 'MyAnnotation2'], }); - expect(param.excludedAnnotations, containsAll(['MyAnnotation1', 'MyAnnotation2'])); + expect( + param.excludedAnnotations, + containsAll(['MyAnnotation1', 'MyAnnotation2']), + ); }); test('parses single string', () { diff --git a/test/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart b/test/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart similarity index 98% rename from test/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart rename to test/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart index 1848496e..5da8b6d9 100644 --- a/test/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart +++ b/test/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule_test.dart b/test/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule_test.dart index af66db59..a57aa2fe 100644 --- a/test/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule_test.dart +++ b/test/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule_test.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../../../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/lints/avoid_global_state/avoid_global_state_rule_test.dart b/test/src/lints/avoid_global_state/avoid_global_state_rule_test.dart similarity index 97% rename from test/lints/avoid_global_state/avoid_global_state_rule_test.dart rename to test/src/lints/avoid_global_state/avoid_global_state_rule_test.dart index 2ba14682..008a4de8 100644 --- a/test/lints/avoid_global_state/avoid_global_state_rule_test.dart +++ b/test/src/lints/avoid_global_state/avoid_global_state_rule_test.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/avoid_late_keyword_rule_test.dart b/test/src/lints/avoid_late_keyword_rule_test/avoid_late_keyword_rule_test.dart similarity index 99% rename from test/avoid_late_keyword_rule_test.dart rename to test/src/lints/avoid_late_keyword_rule_test/avoid_late_keyword_rule_test.dart index 1211ac6a..701b6b9d 100644 --- a/test/avoid_late_keyword_rule_test.dart +++ b/test/src/lints/avoid_late_keyword_rule_test/avoid_late_keyword_rule_test.dart @@ -4,7 +4,7 @@ import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader. import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import 'lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart b/test/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart similarity index 98% rename from test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart rename to test/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart index 001cf982..68997f8c 100644 --- a/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart +++ b/test/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart @@ -4,7 +4,7 @@ import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader. import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart b/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart index dba8fd06..cf822377 100644 --- a/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart +++ b/test/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule_test.dart @@ -4,7 +4,7 @@ import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader. import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../../../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart b/test/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart similarity index 99% rename from test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart rename to test/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart index 051c61c0..7073167a 100644 --- a/test/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart +++ b/test/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule_test.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart b/test/src/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart similarity index 99% rename from test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart rename to test/src/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart index a7d308f0..7800eefa 100644 --- a/test/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart +++ b/test/src/lints/avoid_unnecessary_set_state/avoid_unnecessary_set_state_rule_test.dart @@ -25,7 +25,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart b/test/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart similarity index 98% rename from test/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart rename to test/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart index a383bf07..b5e4b371 100644 --- a/test/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart +++ b/test/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule_test.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart b/test/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart similarity index 97% rename from test/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart rename to test/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart index c41bb3ad..13767dfd 100644 --- a/test/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart +++ b/test/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule_test.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/avoid_unrelated_type_assertions/avoid_unrelated_type_assertions_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart b/test/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart similarity index 99% rename from test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart rename to test/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart index 82d5830c..5f29bee4 100644 --- a/test/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart +++ b/test/src/lints/avoid_unused_parameters/avoid_unused_parameters_rule_test.dart @@ -3,7 +3,7 @@ import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_param import 'package:test_reflective_loader/test_reflective_loader.dart'; import '../../utils/fake_analysis_options_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/lints/avoid_using_api/avoid_using_api_rule_test.dart b/test/src/lints/avoid_using_api/avoid_using_api_rule_test.dart index 780ddc8d..308dd9dd 100644 --- a/test/src/lints/avoid_using_api/avoid_using_api_rule_test.dart +++ b/test/src/lints/avoid_using_api/avoid_using_api_rule_test.dart @@ -5,7 +5,7 @@ import 'package:solid_lints/src/lints/avoid_using_api/avoid_using_api_rule.dart' import 'package:test/test.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../../../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart index 4b5c09c1..94758467 100644 --- a/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart +++ b/test/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule_test.dart @@ -4,7 +4,7 @@ import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader. import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../../../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/lints/double_literal_format/double_literal_format_rule_test.dart b/test/src/lints/double_literal_format/double_literal_format_rule_test.dart similarity index 98% rename from test/lints/double_literal_format/double_literal_format_rule_test.dart rename to test/src/lints/double_literal_format/double_literal_format_rule_test.dart index 43e01e78..42abc04d 100644 --- a/test/lints/double_literal_format/double_literal_format_rule_test.dart +++ b/test/src/lints/double_literal_format/double_literal_format_rule_test.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/lints/member_ordering/member_ordering_rule_test.dart b/test/src/lints/member_ordering/member_ordering_rule_test.dart index 30ff152a..e77177ff 100644 --- a/test/src/lints/member_ordering/member_ordering_rule_test.dart +++ b/test/src/lints/member_ordering/member_ordering_rule_test.dart @@ -4,7 +4,7 @@ import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader. import 'package:solid_lints/src/lints/member_ordering/member_ordering_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../../../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/lints/named_parameters_ordering/named_parameters_ordering_rule_test.dart b/test/src/lints/named_parameters_ordering/named_parameters_ordering_rule_test.dart index 9ed5e32b..ecc896c4 100644 --- a/test/src/lints/named_parameters_ordering/named_parameters_ordering_rule_test.dart +++ b/test/src/lints/named_parameters_ordering/named_parameters_ordering_rule_test.dart @@ -4,7 +4,7 @@ import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader. import 'package:solid_lints/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../../../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineRefSuite(); diff --git a/test/lints/newline_before_return/newline_before_return_rule_test.dart b/test/src/lints/newline_before_return/newline_before_return_rule_test.dart similarity index 98% rename from test/lints/newline_before_return/newline_before_return_rule_test.dart rename to test/src/lints/newline_before_return/newline_before_return_rule_test.dart index a8ea4f19..d19dadc9 100644 --- a/test/lints/newline_before_return/newline_before_return_rule_test.dart +++ b/test/src/lints/newline_before_return/newline_before_return_rule_test.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/newline_before_return/newline_before_return_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/lints/no_empty_block/no_empty_block_rule_test.dart b/test/src/lints/no_empty_block/no_empty_block_rule_test.dart index 0199ea81..59f51fd8 100644 --- a/test/src/lints/no_empty_block/no_empty_block_rule_test.dart +++ b/test/src/lints/no_empty_block/no_empty_block_rule_test.dart @@ -4,7 +4,7 @@ import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader. import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../../../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/lints/no_equal_then_else/no_equal_then_else_rule_test.dart b/test/src/lints/no_equal_then_else/no_equal_then_else_rule_test.dart similarity index 97% rename from test/lints/no_equal_then_else/no_equal_then_else_rule_test.dart rename to test/src/lints/no_equal_then_else/no_equal_then_else_rule_test.dart index 99059d62..59e8679f 100644 --- a/test/lints/no_equal_then_else/no_equal_then_else_rule_test.dart +++ b/test/src/lints/no_equal_then_else/no_equal_then_else_rule_test.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/no_equal_then_else/no_equal_then_else_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/lints/no_magic_number/no_magic_number_rule_test.dart b/test/src/lints/no_magic_number/no_magic_number_rule_test.dart index 46d7b1a8..bd4ff8c7 100644 --- a/test/src/lints/no_magic_number/no_magic_number_rule_test.dart +++ b/test/src/lints/no_magic_number/no_magic_number_rule_test.dart @@ -4,7 +4,7 @@ import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader. import 'package:solid_lints/src/lints/no_magic_number/no_magic_number_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../../../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/lints/number_of_parameters/number_of_parameters_rule_test.dart b/test/src/lints/number_of_parameters/number_of_parameters_rule_test.dart index 2b7dd7d9..52a9d171 100644 --- a/test/src/lints/number_of_parameters/number_of_parameters_rule_test.dart +++ b/test/src/lints/number_of_parameters/number_of_parameters_rule_test.dart @@ -4,7 +4,7 @@ import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader. import 'package:solid_lints/src/lints/number_of_parameters/number_of_parameters_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../../../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule_test.dart b/test/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule_test.dart index 724e0747..c8d5d922 100644 --- a/test/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule_test.dart +++ b/test/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule_test.dart @@ -4,7 +4,7 @@ import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader. import 'package:solid_lints/src/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../../../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/lints/prefer_early_return/prefer_early_return_rule_test.dart b/test/src/lints/prefer_early_return/prefer_early_return_rule_test.dart similarity index 99% rename from test/lints/prefer_early_return/prefer_early_return_rule_test.dart rename to test/src/lints/prefer_early_return/prefer_early_return_rule_test.dart index 4b3cfa2e..d6281b5e 100644 --- a/test/lints/prefer_early_return/prefer_early_return_rule_test.dart +++ b/test/src/lints/prefer_early_return/prefer_early_return_rule_test.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/prefer_early_return/prefer_early_return_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/lints/prefer_first/prefer_first_rule_test.dart b/test/src/lints/prefer_first/prefer_first_rule_test.dart similarity index 97% rename from test/lints/prefer_first/prefer_first_rule_test.dart rename to test/src/lints/prefer_first/prefer_first_rule_test.dart index 50bdf212..ba66b169 100644 --- a/test/lints/prefer_first/prefer_first_rule_test.dart +++ b/test/src/lints/prefer_first/prefer_first_rule_test.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/prefer_last/prefer_last_rule_test.dart b/test/src/lints/prefer_last/prefer_last_rule_test.dart similarity index 98% rename from test/prefer_last/prefer_last_rule_test.dart rename to test/src/lints/prefer_last/prefer_last_rule_test.dart index a25747c6..16ef8cbc 100644 --- a/test/prefer_last/prefer_last_rule_test.dart +++ b/test/src/lints/prefer_last/prefer_last_rule_test.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/lints/prefer_match_file_name/prefer_match_file_name_rule_test.dart b/test/src/lints/prefer_match_file_name/prefer_match_file_name_rule_test.dart index ff27dfc7..581b0b9f 100644 --- a/test/src/lints/prefer_match_file_name/prefer_match_file_name_rule_test.dart +++ b/test/src/lints/prefer_match_file_name/prefer_match_file_name_rule_test.dart @@ -4,7 +4,7 @@ import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader. import 'package:solid_lints/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../../../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { @@ -202,7 +202,8 @@ final someVariable = 42; '''); } - void test_does_not_report_on_multiple_public_declarations_if_first_matches() async { + void + test_does_not_report_on_multiple_public_declarations_if_first_matches() async { await assertNoDiagnostics(r''' class Test {} class AnotherPublicClass {} diff --git a/test/lints/proper_super_calls/proper_super_calls_rule_test.dart b/test/src/lints/proper_super_calls/proper_super_calls_rule_test.dart similarity index 98% rename from test/lints/proper_super_calls/proper_super_calls_rule_test.dart rename to test/src/lints/proper_super_calls/proper_super_calls_rule_test.dart index 6d73d246..a61fcfea 100644 --- a/test/lints/proper_super_calls/proper_super_calls_rule_test.dart +++ b/test/src/lints/proper_super_calls/proper_super_calls_rule_test.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule_test.dart b/test/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule_test.dart index 2b27ba46..439b8dc3 100644 --- a/test/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule_test.dart +++ b/test/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule_test.dart @@ -4,7 +4,7 @@ import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader. import 'package:solid_lints/src/lints/use_descriptive_names_for_type_parameters/use_descriptive_names_for_type_parameters_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../../../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/lints/use_nearest_context/use_nearest_context_rule_test.dart b/test/src/lints/use_nearest_context/use_nearest_context_rule_test.dart index 070b71e2..5e68a889 100644 --- a/test/src/lints/use_nearest_context/use_nearest_context_rule_test.dart +++ b/test/src/lints/use_nearest_context/use_nearest_context_rule_test.dart @@ -3,7 +3,7 @@ import 'package:analyzer_testing/utilities/utilities.dart'; import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart'; import 'package:test_reflective_loader/test_reflective_loader.dart'; -import '../../../lints/auto_test_lint_offsets.dart'; +import '../../utils/auto_test_lint_offsets.dart'; void main() { defineReflectiveSuite(() { diff --git a/test/src/utils/auto_lint_data.dart b/test/src/utils/auto_lint_data.dart index f0aa562b..61b900dc 100644 --- a/test/src/utils/auto_lint_data.dart +++ b/test/src/utils/auto_lint_data.dart @@ -1,5 +1,7 @@ import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart'; +/// Holds data about a lint placeholder, including the original code +/// and optional diagnostic assertions. class AutoLintData { final String code; final Pattern? correctionContains; diff --git a/test/lints/auto_test_lint_offsets.dart b/test/src/utils/auto_test_lint_offsets.dart similarity index 98% rename from test/lints/auto_test_lint_offsets.dart rename to test/src/utils/auto_test_lint_offsets.dart index b4b41fbb..e026f382 100644 --- a/test/lints/auto_test_lint_offsets.dart +++ b/test/src/utils/auto_test_lint_offsets.dart @@ -2,7 +2,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart'; import 'package:collection/collection.dart'; -import '../src/utils/auto_lint_data.dart'; +import 'auto_lint_data.dart'; mixin AutoTestLintOffsets on AnalysisRuleTest { int _nextPlaceholderId = 0; diff --git a/test/src/utils/code_generators.dart b/test/src/utils/code_generators.dart index 5947a168..7c36cb85 100644 --- a/test/src/utils/code_generators.dart +++ b/test/src/utils/code_generators.dart @@ -3,6 +3,5 @@ extension RepeatLinesExtension on String { /// Repeats this string [times] times, joining with newlines. /// /// Useful for generating test Dart code with a specific number of lines. - String repeatLines(int times) => - List.generate(times, (_) => this).join('\n'); + String repeatLines(int times) => List.generate(times, (_) => this).join('\n'); } diff --git a/test/utils/fake_analysis_options_loader.dart b/test/src/utils/fake_analysis_options_loader.dart similarity index 96% rename from test/utils/fake_analysis_options_loader.dart rename to test/src/utils/fake_analysis_options_loader.dart index 3672abca..c3e16e2a 100644 --- a/test/utils/fake_analysis_options_loader.dart +++ b/test/src/utils/fake_analysis_options_loader.dart @@ -14,8 +14,7 @@ class FakeAnalysisOptionsLoader implements AnalysisOptionsLoader { Map? getRuleOptionsForFile( String filePath, String ruleName, - ) => - ruleOptions; + ) => ruleOptions; @override void loadRulesOptionsFromContext(RuleContext context) {} diff --git a/test/src/utils/table_driven_rule_test_base.dart b/test/src/utils/table_driven_rule_test_base.dart index f27fdaad..97d8a164 100644 --- a/test/src/utils/table_driven_rule_test_base.dart +++ b/test/src/utils/table_driven_rule_test_base.dart @@ -1,7 +1,7 @@ import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; import 'package:test/test.dart' hide setUp; -import '../../lints/auto_test_lint_offsets.dart'; +import 'auto_test_lint_offsets.dart'; /// Result expected from a table-driven test case. enum ExpectedResult { diff --git a/tool/generate_web_docs_content.dart b/tool/generate_web_docs_content.dart index 233ebfb0..7ec6bb55 100644 --- a/tool/generate_web_docs_content.dart +++ b/tool/generate_web_docs_content.dart @@ -25,14 +25,16 @@ void main(List rawArgs) async { ..addOption( 'docs-dir', abbr: 'o', - help: 'Parser output path. i.e "docusaurus/docs/Solid Lints" directory.' + help: + 'Parser output path. i.e "docusaurus/docs/Solid Lints" directory.' 'Please note that parent directory would be used to place the intro.md file', defaultsTo: docusaurusDefaultPath, ) ..addOption( 'readme', abbr: 'r', - help: 'Path to the README.md file that should be' + help: + 'Path to the README.md file that should be' ' copied as docusaurus intro.md', defaultsTo: readmeDefaultPath, ) From 24d6168fb05bb3de84d1d95c15cded33446a3aef Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 22 Jul 2026 16:21:16 +0300 Subject: [PATCH 46/49] Add new rule avoid_similar_names (#321) * feat: add avoid_similar_names lint rule * refactor: optimize variable collection and improve tokenization logic in avoid_similar_names lint * fix: improve acronym tokenization in avoid_similar_names lint to support camelCase suffixes * refactor: support linting of declared identifiers and extract variable collection logic * feat: update avoid_similar_names lint to ignore nullability differences when comparing variable types * refactor: switch to SimpleAstVisitor and remove redundant super calls in AvoidSimilarNamesVisitor * feat: add support for detecting similar names in pattern variable declarations * fix: clear reported nodes in avoid similar names visitor to prevent stale lint reports * feat: improve name similarity detection by refining NameTokenizer and adding related tests * refactor: simplify avoid_similar_names visitor logic * refactor: decouple lint reporting logic from token comparison methods in avoid_similar_names visitor * refactor: simplify avoid_similar_names visitor methods * refactor: simplify avoid_similar_names test suite using helper methods --------- Co-authored-by: Illia Aihistov --- lib/main.dart | 2 + .../avoid_similar_names_rule.dart | 67 ++++++++ .../models/scope_variable.dart | 49 ++++++ .../utils/name_tokenizer.dart | 70 ++++++++ .../visitors/avoid_similar_names_visitor.dart | 109 ++++++++++++ .../visitors/local_variables_visitor.dart | 51 ++++++ lib/src/utils/iterable_utils.dart | 17 ++ lib/src/utils/types_utils.dart | 4 + .../avoid_similar_names_rule_test.dart | 158 ++++++++++++++++++ .../utils/name_tokenizer_test.dart | 31 ++++ 10 files changed, 558 insertions(+) create mode 100644 lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart create mode 100644 lib/src/lints/avoid_similar_names/models/scope_variable.dart create mode 100644 lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart create mode 100644 lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart create mode 100644 lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart create mode 100644 test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart create mode 100644 test/src/lints/avoid_similar_names/utils/name_tokenizer_test.dart diff --git a/lib/main.dart b/lib/main.dart index ed4503b5..f68bdf9d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/avoid_similar_names_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_return_variable/avoid_unnecessary_return_variable_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_unnecessary_type_assertions/avoid_unnecessary_type_assertions_rule.dart'; @@ -62,6 +63,7 @@ class SolidLintsPlugin extends Plugin { AvoidLateKeywordRule(analysisOptionsLoader: analysisLoader), AvoidNonNullAssertionRule(analysisOptionsLoader: analysisLoader), AvoidReturningWidgetsRule(analysisOptionsLoader: analysisLoader), + AvoidSimilarNamesRule(), AvoidUnnecessaryReturnVariableRule(), AvoidUnnecessarySetStateRule(), AvoidUnnecessaryTypeAssertionsRule(), diff --git a/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart b/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart new file mode 100644 index 00000000..58140dab --- /dev/null +++ b/lib/src/lints/avoid_similar_names/avoid_similar_names_rule.dart @@ -0,0 +1,67 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart'; +import 'package:solid_lints/src/models/solid_lint_rule.dart'; + +/// Avoid similar names +/// +/// Warns about variables or parameters that have confusingly similar names +/// within the same function scope (e.g., using numeric suffixes or +/// single-letter modifiers like `someClass1` and `someClass2`). +/// +/// This encourages using descriptive, distinct names to improve code +/// readability and prevent logical errors caused by mixing up variables. +/// +/// ### Example +/// +/// #### BAD: +/// +/// ```dart +/// void test(SomeClass someClass1, SomeClass someClass2) { // LINT +/// final tempA = 'a'; // LINT +/// final tempB = 'b'; // LINT +/// } +/// ``` +/// +/// #### GOOD: +/// +/// ```dart +/// void test(SomeClass first, SomeClass second) { +/// final that = 'a'; +/// final other = 'b'; +/// } +/// ``` +class AvoidSimilarNamesRule extends SolidLintRule { + /// The name of this lint rule. + static const lintName = 'avoid_similar_names'; + + static const _code = LintCode( + lintName, + 'Avoid using similar names.', + correctionMessage: 'Use more descriptive names.', + ); + + /// Creates an instance of [AvoidSimilarNamesRule]. + AvoidSimilarNamesRule() + : super( + name: lintName, + description: 'Warns about variables or parameters with similar names.', + ); + + @override + LintCode get diagnosticCode => _code; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + final visitor = AvoidSimilarNamesVisitor(this); + + registry + ..addMethodDeclaration(this, visitor) + ..addConstructorDeclaration(this, visitor) + ..addFunctionDeclaration(this, visitor); + } +} diff --git a/lib/src/lints/avoid_similar_names/models/scope_variable.dart b/lib/src/lints/avoid_similar_names/models/scope_variable.dart new file mode 100644 index 00000000..11991556 --- /dev/null +++ b/lib/src/lints/avoid_similar_names/models/scope_variable.dart @@ -0,0 +1,49 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/utils/name_tokenizer.dart'; + +/// Represents a variable or parameter collected from a scope. +class ScopeVariable { + /// The resolved type of the variable, if available. + final DartType? type; + + /// The AST node representing this variable declaration. + final AstNode node; + + /// The token representing the name. + final Token nameToken; + + /// The individual word/digit tokens of the name. + final List tokens; + + /// The minimum length for a variable name to be considered descriptive enough + /// to be analyzed for similarity. + static const minDescriptiveNameLength = 3; + + /// Creates a new [ScopeVariable] if the [nameToken] is descriptive enough + /// to be analyzed for similarity. Returns `null` if the cleaned name + /// is too short. + static ScopeVariable? createOrNull({ + required Token nameToken, + required DartType? type, + required AstNode node, + }) { + final cleaned = NameTokenizer.cleanName(nameToken.lexeme); + if (cleaned.length < minDescriptiveNameLength) return null; + + return ScopeVariable._( + nameToken: nameToken, + type: type, + node: node, + tokens: NameTokenizer.tokenize(cleaned), + ); + } + + ScopeVariable._({ + required this.nameToken, + required this.type, + required this.node, + required this.tokens, + }); +} diff --git a/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart b/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart new file mode 100644 index 00000000..58925281 --- /dev/null +++ b/lib/src/lints/avoid_similar_names/utils/name_tokenizer.dart @@ -0,0 +1,70 @@ +/// Utility class for tokenizing identifiers and +/// comparing name similarity. +abstract final class NameTokenizer { + /// Regex Fragment | Meaning + /// ================================================================= + /// [A-Z]?[a-z]+ | Match words (e.g., Class, user): + /// [A-Z]? | ... optional leading uppercase, + /// [a-z]+ | ... followed by lowercase letters. + /// | | OR + /// [A-Z]+(?=[A-Z][a-z]|\d|\b)| Match acronyms (uppercase letters). + /// | | OR + /// \d+ | Match digits (e.g., 1, 10). + static final _tokenPattern = RegExp( + r'[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\d|\b)|\d+', + ); + + /// Regex Fragment | Meaning + /// ================================================================= + /// ^ | Match start of string. + /// _+ | Match one or more leading underscores. + static final _leadingUnderscoresPattern = RegExp('^_+'); + + static const _allowedTokens = {'x', 'y', 'z', 'w', 'i', 'j', 'k'}; + + /// Splits a camelCase or snake_case identifier + /// into lowercase tokens. + /// + /// E.g., `someClass1` returns `['some', 'class', '1']`. + static List tokenize(String name) => [ + for (final match in _tokenPattern.allMatches(name)) + if (match.group(0) case final group?) group.toLowerCase(), + ]; + + /// Strips leading underscores from a name. + /// + /// E.g., `_someName` returns `someName`. + static String cleanName(String name) => + name.replaceFirst(_leadingUnderscoresPattern, ''); + + /// Returns `true` if the string consists only + /// of digit characters. + static bool isDigit(String s) => int.tryParse(s) != null; + + /// Returns `true` if the token is a common + /// loop variable or coordinate name. + static bool isAllowedToken(String s) => _allowedTokens.contains(s); + + /// Returns `true` if the token is considered non-descriptive + /// (either a digit or a disallowed single letter). + static bool isNonDescriptiveToken(String s) => + isDigit(s) || (s.length == 1 && !isAllowedToken(s)); + + /// Returns `true` if [longer] is a superset of + /// [shorter] with exactly one extra non-descriptive token. + static bool isSubsetWithNonDescriptiveToken( + List longer, + List shorter, + ) { + if (longer.length != shorter.length + 1) return false; + var i = 0; + while (i < shorter.length && longer[i] == shorter[i]) { + i++; + } + if (!isNonDescriptiveToken(longer[i])) return false; + while (i < shorter.length && longer[i + 1] == shorter[i]) { + i++; + } + return i == shorter.length; + } +} diff --git a/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart new file mode 100644 index 00000000..e753d87f --- /dev/null +++ b/lib/src/lints/avoid_similar_names/visitors/avoid_similar_names_visitor.dart @@ -0,0 +1,109 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:collection/collection.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/avoid_similar_names_rule.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/models/scope_variable.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/utils/name_tokenizer.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart'; +import 'package:solid_lints/src/utils/iterable_utils.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; + +/// A visitor that checks for variables with +/// confusingly similar names. +class AvoidSimilarNamesVisitor extends SimpleAstVisitor { + final AvoidSimilarNamesRule _rule; + final _reportedNodes = {}; + final _collector = LocalVariablesVisitor(); + + /// Creates a new instance of + /// [AvoidSimilarNamesVisitor]. + AvoidSimilarNamesVisitor(this._rule); + + @override + void visitMethodDeclaration(MethodDeclaration node) => + _checkScope(node.parameters, node.body); + + @override + void visitConstructorDeclaration(ConstructorDeclaration node) => + _checkScope(node.parameters, node.body); + + @override + void visitFunctionDeclaration(FunctionDeclaration node) => _checkScope( + node.functionExpression.parameters, + node.functionExpression.body, + ); + + void _checkScope( + FormalParameterList? parameters, + FunctionBody body, + ) { + _reportedNodes.clear(); + _collector.variables.clear(); + body.accept(_collector); + + _compareVariables([ + ..._extractParameters(parameters), + ..._collector.variables, + ]); + _reportedNodes.clear(); + } + + Iterable _extractParameters( + FormalParameterList? parameters, + ) => [ + for (final parameter in parameters?.parameters ?? const []) + if (parameter.name case final nameToken?) + if (ScopeVariable.createOrNull( + nameToken: nameToken, + type: parameter.declaredFragment?.element.type, + node: parameter, + ) + case final variable?) + variable, + ]; + + void _compareVariables(List variables) { + for (var i = 0; i < variables.length; i++) { + for (var j = i + 1; j < variables.length; j++) { + _comparePair(variables[i], variables[j]); + } + } + } + + void _comparePair(ScopeVariable a, ScopeVariable b) { + final lengthDiff = (a.tokens.length - b.tokens.length).abs(); + final shouldReport = + a.type?.isDifferentIgnoringNullability(b.type) != true && + ((lengthDiff == 0 && _checkSameLengthTokens(a, b)) || + (lengthDiff == 1 && _checkSingleTokenLengthDifference(a, b))); + + if (shouldReport) { + _report(a, b); + } + } + + bool _checkSameLengthTokens( + ScopeVariable a, + ScopeVariable b, + ) { + final diff = a.tokens + .zipWithIndexed(b.tokens) + .where((e) => e.$2 != e.$3) + .singleOrNull + ?.$1; + + return diff != null && + NameTokenizer.isNonDescriptiveToken(a.tokens[diff]) && + NameTokenizer.isNonDescriptiveToken(b.tokens[diff]); + } + + bool _checkSingleTokenLengthDifference(ScopeVariable a, ScopeVariable b) { + final [shorter, longer] = [a.tokens, b.tokens].sortedBy((e) => e.length); + + return NameTokenizer.isSubsetWithNonDescriptiveToken(longer, shorter); + } + + void _report(ScopeVariable a, ScopeVariable b) => [a, b] + .where((e) => _reportedNodes.add(e.node)) + .forEach((e) => _rule.reportAtToken(e.nameToken)); +} diff --git a/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart b/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart new file mode 100644 index 00000000..9fe50538 --- /dev/null +++ b/lib/src/lints/avoid_similar_names/visitors/local_variables_visitor.dart @@ -0,0 +1,51 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/models/scope_variable.dart'; + +/// Collects local variable declarations within a function body, +/// stopping at nested function boundaries. +class LocalVariablesVisitor extends RecursiveAstVisitor { + /// The collected variables. + final List variables = []; + + @override + void visitVariableDeclaration(VariableDeclaration node) { + _collect(node.name, node.declaredFragment?.element.type, node); + super.visitVariableDeclaration(node); + } + + @override + void visitDeclaredIdentifier(DeclaredIdentifier node) { + _collect(node.name, node.declaredFragment?.element.type, node); + super.visitDeclaredIdentifier(node); + } + + @override + void visitDeclaredVariablePattern(DeclaredVariablePattern node) { + _collect(node.name, node.declaredFragment?.element.type, node); + super.visitDeclaredVariablePattern(node); + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + // Stop traversing nested function scopes. + } + + @override + void visitFunctionExpression(FunctionExpression node) { + // Stop traversing nested closures. + } + + void _collect(Token nameToken, DartType? type, AstNode node) { + final variable = ScopeVariable.createOrNull( + nameToken: nameToken, + type: type, + node: node, + ); + if (variable != null) { + variables.add(variable); + } + } +} diff --git a/lib/src/utils/iterable_utils.dart b/lib/src/utils/iterable_utils.dart index f7037447..3dcf538b 100644 --- a/lib/src/utils/iterable_utils.dart +++ b/lib/src/utils/iterable_utils.dart @@ -25,3 +25,20 @@ extension IterablePairwise on Iterable { } } } + +/// Extension on [Iterable] to zip elements with another iterable. +extension IterableZip on Iterable { + /// Zips this iterable with [other]. + Iterable<(T, U)> zipWith(Iterable other) sync* { + for (var i = 0; i < length && i < other.length; i++) { + yield (elementAt(i), other.elementAt(i)); + } + } + + /// Zips this iterable with [other] and includes the index. + Iterable<(int, T, U)> zipWithIndexed(Iterable other) sync* { + for (var i = 0; i < length && i < other.length; i++) { + yield (i, elementAt(i), other.elementAt(i)); + } + } +} diff --git a/lib/src/utils/types_utils.dart b/lib/src/utils/types_utils.dart index 64558270..d3ebac5e 100644 --- a/lib/src/utils/types_utils.dart +++ b/lib/src/utils/types_utils.dart @@ -75,6 +75,10 @@ extension Subtypes on DartType { return false; } + + /// Compares this type with [other] ignoring nullability where applicable. + bool isDifferentIgnoringNullability(DartType? other) => + other != null && (element ?? this) != (other.element ?? other); } extension TypeString on String { diff --git a/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart new file mode 100644 index 00000000..2082a301 --- /dev/null +++ b/test/src/lints/avoid_similar_names/avoid_similar_names_rule_test.dart @@ -0,0 +1,158 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/lints/avoid_similar_names/avoid_similar_names_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../utils/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidSimilarNamesRuleTest); + }); +} + +@reflectiveTest +class AvoidSimilarNamesRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + @override + void setUp() { + rule = AvoidSimilarNamesRule(); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + analysisOptionsContent(rules: [rule.name]), + ); + } + + Future test_reports_on_similar_names_in_function() => _assert(''' + int ${expectLint('someClass1')} = 1; + int ${expectLint('someClass2')} = 2; +'''); + + Future test_does_not_report_on_different_types() => _assertNo(''' + String user1 = 'Alice'; + int user2 = 123; +'''); + + Future test_reports_on_similar_names_with_different_nullability() => + _assert(''' + int ${expectLint('user1')} = 1; + int? ${expectLint('user2')} = 2; +'''); + + Future test_reports_on_borderline_short_names() => _assert(''' + int ${expectLint('id1')} = 1; + int ${expectLint('id2')} = 2; +'''); + + Future test_does_not_report_on_different_descriptive_tokens() => + _assertNo(''' + int minHeight = 10; + int maxHeight = 20; +'''); + + Future test_does_not_report_on_short_names() => _assertNo(''' + int x1 = 1; + int x2 = 2; + int dx = 5; + int dy = 10; +'''); + + Future test_does_not_report_on_allowed_coordinates() => _assertNo(''' + double pointX = 1.0; + double pointY = 2.0; +'''); + + Future test_reports_on_digits_in_parameters() => + assertAutoDiagnostics(''' +bool isEqual( + int ${expectLint('someClass1')}, + int ${expectLint('someClass2')}, +) { + return someClass1 == someClass2; +} +'''); + + Future test_reports_on_similar_names_in_method() => + assertAutoDiagnostics(''' +class A { + void test() { + String ${expectLint('tempA')} = "a"; + String ${expectLint('tempB')} = "b"; + } +} +'''); + + Future test_reports_on_subset_with_extra_digit() => _assert(''' + String ${expectLint('data')} = "a"; + String ${expectLint('data1')} = "b"; +'''); + + Future test_reports_on_subset_with_multi_digit_number() => _assert(''' + String ${expectLint('data')} = "a"; + String ${expectLint('data10')} = "b"; +'''); + + Future test_reports_on_mixed_non_descriptive_suffixes() => _assert(''' + String ${expectLint('user1')} = "a"; + String ${expectLint('userA')} = "b"; +'''); + + Future test_reports_on_subset_with_extra_letter() => _assert(''' + String ${expectLint('user')} = "a"; + String ${expectLint('userA')} = "b"; +'''); + + Future test_does_not_report_on_subset_with_descriptive_token() => + _assertNo(''' + String user = "a"; + String userProfile = "b"; + String data = "c"; + String dataFetch = "d"; +'''); + + Future test_reports_on_three_similar_names() => _assert(''' + int ${expectLint('id1')} = 1; + int ${expectLint('id2')} = 2; + int ${expectLint('id3')} = 3; +'''); + + Future test_does_not_report_on_anonymous_lambda() => _assertNo(''' + void process(int Function(int, int) callback) {} + process((day1, day2) => day1 + day2); +'''); + + Future test_reports_on_acronym_and_camel_case_suffix() => _assert(''' + int ${expectLint('APIRequest')} = 1; + int ${expectLint('apiRequest1')} = 2; +'''); + + Future test_reports_on_similar_names_in_for_in_loops() => _assert(''' + final users = [1, 2]; + for (final ${expectLint('user1')} in users) { + int ${expectLint('user2')} = user1; + } +'''); + + Future + test_reports_on_similar_names_in_pattern_variable_declarations() => + _assert(''' + final (${expectLint('user1')}, ${expectLint('user2')}) = (1, 2); +'''); + + Future test_reports_on_similar_names_in_pattern_matching_if_case() => + assertAutoDiagnostics(''' +void test(Object obj) { + if (obj case [int ${expectLint('user1')}, int ${expectLint('user2')}]) { + // ... + } +} +'''); + + Future _assert(String body) => + assertAutoDiagnostics('''void test() {$body}'''); + + Future _assertNo(String body) => + assertNoDiagnostics('''void test() {$body}'''); +} diff --git a/test/src/lints/avoid_similar_names/utils/name_tokenizer_test.dart b/test/src/lints/avoid_similar_names/utils/name_tokenizer_test.dart new file mode 100644 index 00000000..55244946 --- /dev/null +++ b/test/src/lints/avoid_similar_names/utils/name_tokenizer_test.dart @@ -0,0 +1,31 @@ +import 'package:solid_lints/src/lints/avoid_similar_names/utils/name_tokenizer.dart'; +import 'package:test/test.dart'; + +void main() { + group('NameTokenizer', () { + test('isSubsetWithNonDescriptiveToken guard clause', () { + expect( + NameTokenizer.isSubsetWithNonDescriptiveToken( + ['a', 'descriptive'], + ['a', 'descriptive'], + ), + isFalse, + ); + expect( + NameTokenizer.isSubsetWithNonDescriptiveToken( + ['a', 'descriptive'], + ['a'], + ), + isFalse, + ); + expect( + NameTokenizer.isSubsetWithNonDescriptiveToken(['a', '1'], ['a']), + isTrue, + ); + expect( + NameTokenizer.isSubsetWithNonDescriptiveToken(['a'], ['a', 'b']), + isFalse, + ); + }); + }); +} From d8ccca1e91cd6756974904f459cf8507f24695a5 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 22 Jul 2026 16:38:26 +0300 Subject: [PATCH 47/49] Add new rule avoid_duplicate_code (#320) * feat: add avoid_duplicate_code lint rule * feat: increase default min_statements for avoid_duplicate_code from 3 to 10 * feat: enable cross-file duplicate code detection by implementing a global hash registry * chore: remove avoid_using_api from analysis exclude list * refactor: introduce SolidDiagnosticMessage to encapsulate diagnostic reporting and improve code formatting * feat: register SolidLintsPlugin in pubspec and remove redundant analyzer plugin configuration * refactor: optimize duplicate code detection with Jenkins hashing, structural config validation, and improved caching settings. * refactor: simplify avoid_duplicate_code parameters by removing default method exclusions and cleaning up JSON parsing logic * refactor: simplify duplicate code detection logic by extracting visitor helper methods and improving cache handling * refactor: modularize duplicate detection logic by introducing specialized visitors and utility extensions * chore: remove exclusion for avoid_using_api lint rules * fix: use stable strings instead of runtime hash codes in AST structural hashing to prevent cache invalidation * fix: improve accuracy and stability of duplicate code detection by implementing robust cache invalidation, parameter equality checks, and collision guarding. * refactor: remove unnecessary empty line in global_hash_registry.dart * refactor: fix indentation in AvoidDuplicateCodeParameters hashCode method * docs: update changelog with avoid_duplicate_code rule and formatting improvements * chore: update default minimum tokens to 30 for avoid_duplicate_code lint * feat: add equality operators and hashCode overrides to parameter models for reliable comparison * fix: handle corrupted cache files in HashCacheStorage and add regression test * test: update avoid_duplicate_code_rule_test to include excluded identifiers and context root * fix: use set for deletedFiles and skip them when building the inverted index * fix: prevent infinite loop in token counting by returning count when a cycle is detected * fix: improve path precision in duplicate code detection to avoid sibling directory collisions and encapsulate utility methods. * refactor: enable independent debounce logic for different package roots in GlobalHashRegistry * fix: catch all exceptions during file operations in hash cache storage to prevent analysis crashes * fix: target specific DuplicateLocation for removal in global hash registry instead of filtering by path * fix: resolve package-specific parameter retrieval during debounced cache saves in multi-package workspaces * refactor: inject ResourceProvider into avoid_duplicate_code services to support testability * refactor: simplify toJson method in ExcludedIdentifierParameter * refactor: improve FileCacheEntry JSON parsing safety with tryMap * refactor: simplify isFileExcluded method * refactor: optimize JenkinsHasher by extracting mix logic and simplifying method implementations * refactor: migrate TokenUtils to AstNode extension for cleaner token count retrieval * refactor: extract redundant visitation logic into private helper method in DescendantVisitor * chore: add trailing newline to pubspec.yaml * refactor: convert AvoidDuplicateCodeParameters.fromJson factory to arrow syntax * refactor: consolidate code duplication candidate visitation logic into a generic method * refactor: introduce SetDictionary for inverted index and modernize code with pattern matching * refactor: improve cache storage robustness with new utility helpers and cleaner file handling logic * refactor: modernize AstStructuralHashVisitor using pattern matching and streamlined visitor logic * refactor: simplify duplicate code detection logic using collection utilities and extract message generation * refactor: convert FileCacheEntry.fromJson factory to a generative constructor * refactor: convert HashEntry.fromJson factory constructor to a generative constructor * refactor: convert HashCacheStorage from static utility to instance-based service * refactor: implement Equatable for ExcludedIdentifierParameter * refactor: add batch update methods to SetDictionary and implement via new HashEntryListExtension * refactor: simplify DuplicateLocation instantiation in hash entry list extension * refactor: reorder HashCacheStorage constructor and optimize GlobalHashRegistry setup/teardown using cascades --------- Co-authored-by: Illia Aihistov --- CHANGELOG.md | 1 + lib/analysis_options.yaml | 6 + lib/analysis_options_test.yaml | 3 + lib/main.dart | 2 + .../excluded_identifier_parameter.dart | 17 +- .../excluded_identifiers_list_parameter.dart | 13 + .../avoid_duplicate_code_rule.dart | 81 +++ .../avoid_duplicate_code_parameters.dart | 87 +++ .../models/body_candidate.dart | 7 + .../models/cross_file_match.dart | 27 + .../models/duplicate_location.dart | 27 + .../models/file_cache_entry.dart | 37 ++ .../models/hash_entry.dart | 47 ++ .../services/global_hash_registry.dart | 300 ++++++++++ .../services/hash_cache_storage.dart | 93 +++ .../utils/context_root_extensions.dart | 12 + .../avoid_duplicate_code/utils/debouncer.dart | 29 + .../utils/hash_entry_list_extension.dart | 15 + .../utils/jenkins_hasher.dart | 33 ++ .../utils/path_utils.dart | 25 + .../utils/range_extension.dart | 20 + .../utils/token_utils.dart | 17 + .../visitors/ast_structural_hash_visitor.dart | 206 +++++++ .../avoid_duplicate_code_visitor.dart | 303 ++++++++++ .../visitors/candidate_visitor.dart | 48 ++ .../visitors/descendant_visitor.dart | 26 + lib/src/models/solid_diagnostic_message.dart | 32 + lib/src/utils/function_utils.dart | 12 + lib/src/utils/iterable_utils.dart | 22 + lib/src/utils/resource_provider_utils.dart | 9 + lib/src/utils/set_dictionary.dart | 46 ++ pubspec.yaml | 1 + .../avoid_duplicate_code_rule_test.dart | 485 ++++++++++++++++ .../global_hash_registry_test.dart | 548 ++++++++++++++++++ .../utils/jenkins_hasher_test.dart | 72 +++ 35 files changed, 2707 insertions(+), 2 deletions(-) create mode 100644 lib/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart create mode 100644 lib/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart create mode 100644 lib/src/lints/avoid_duplicate_code/models/body_candidate.dart create mode 100644 lib/src/lints/avoid_duplicate_code/models/cross_file_match.dart create mode 100644 lib/src/lints/avoid_duplicate_code/models/duplicate_location.dart create mode 100644 lib/src/lints/avoid_duplicate_code/models/file_cache_entry.dart create mode 100644 lib/src/lints/avoid_duplicate_code/models/hash_entry.dart create mode 100644 lib/src/lints/avoid_duplicate_code/services/global_hash_registry.dart create mode 100644 lib/src/lints/avoid_duplicate_code/services/hash_cache_storage.dart create mode 100644 lib/src/lints/avoid_duplicate_code/utils/context_root_extensions.dart create mode 100644 lib/src/lints/avoid_duplicate_code/utils/debouncer.dart create mode 100644 lib/src/lints/avoid_duplicate_code/utils/hash_entry_list_extension.dart create mode 100644 lib/src/lints/avoid_duplicate_code/utils/jenkins_hasher.dart create mode 100644 lib/src/lints/avoid_duplicate_code/utils/path_utils.dart create mode 100644 lib/src/lints/avoid_duplicate_code/utils/range_extension.dart create mode 100644 lib/src/lints/avoid_duplicate_code/utils/token_utils.dart create mode 100644 lib/src/lints/avoid_duplicate_code/visitors/ast_structural_hash_visitor.dart create mode 100644 lib/src/lints/avoid_duplicate_code/visitors/avoid_duplicate_code_visitor.dart create mode 100644 lib/src/lints/avoid_duplicate_code/visitors/candidate_visitor.dart create mode 100644 lib/src/lints/avoid_duplicate_code/visitors/descendant_visitor.dart create mode 100644 lib/src/models/solid_diagnostic_message.dart create mode 100644 lib/src/utils/function_utils.dart create mode 100644 lib/src/utils/resource_provider_utils.dart create mode 100644 lib/src/utils/set_dictionary.dart create mode 100644 test/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule_test.dart create mode 100644 test/src/lints/avoid_duplicate_code/global_hash_registry_test.dart create mode 100644 test/src/lints/avoid_duplicate_code/utils/jenkins_hasher_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index f4ccc394..17527b5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - refactor: replace `avoid_unnecessary_type_casts` rule with dart analyzer's `unnecessary_cast` - Added `use_nearest_context` rule. +- Added `avoid_duplicate_code` rule. - Added `use_descriptive_names_for_type_parameters` rule. ## 0.3.3 diff --git a/lib/analysis_options.yaml b/lib/analysis_options.yaml index 7dbbcdd4..77071fad 100644 --- a/lib/analysis_options.yaml +++ b/lib/analysis_options.yaml @@ -36,6 +36,12 @@ analyzer: solid_lints: diagnostics: avoid_global_state: true + avoid_duplicate_code: + min_tokens: 30 + check_blocks: true + exclude: + - method_name: initState + - method_name: dispose avoid_late_keyword: allow_initialized: true diff --git a/lib/analysis_options_test.yaml b/lib/analysis_options_test.yaml index 19ddb7ec..10d57108 100644 --- a/lib/analysis_options_test.yaml +++ b/lib/analysis_options_test.yaml @@ -51,3 +51,6 @@ solid_lints: avoid_late_keyword: false # It's acceptable to include stubs or other helper classes into the test file. prefer_match_file_name: false +# Test files inherently contain a lot of duplicated setups, teardowns, and mock structures. +# Running duplicate code detection on them usually produces a lot of noise. + avoid_duplicate_code: false diff --git a/lib/main.dart b/lib/main.dart index f68bdf9d..ce9a5bfa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:analyzer/analysis_rule/analysis_rule.dart'; import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; import 'package:solid_lints/src/common/solid_lints_constants.dart'; import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart'; import 'package:solid_lints/src/lints/avoid_final_with_getter/avoid_final_with_getter_rule.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart'; @@ -58,6 +59,7 @@ class SolidLintsPlugin extends Plugin { final lintRules = [ AvoidDebugPrintInReleaseRule(), + AvoidDuplicateCodeRule(analysisOptionsLoader: analysisLoader), AvoidFinalWithGetterRule(), AvoidGlobalStateRule(), AvoidLateKeywordRule(analysisOptionsLoader: analysisLoader), diff --git a/lib/src/common/parameters/excluded_identifier_parameter.dart b/lib/src/common/parameters/excluded_identifier_parameter.dart index f55f9704..b77e2d3e 100644 --- a/lib/src/common/parameters/excluded_identifier_parameter.dart +++ b/lib/src/common/parameters/excluded_identifier_parameter.dart @@ -1,5 +1,7 @@ +import 'package:equatable/equatable.dart'; + /// Model class for ExcludeRule parameters -class ExcludedIdentifierParameter { +class ExcludedIdentifierParameter extends Equatable { /// The name of the method that should be excluded from the lint. final String? methodName; @@ -21,8 +23,19 @@ class ExcludedIdentifierParameter { Map json, ) { return ExcludedIdentifierParameter( - methodName: json['method_name'] as String, + methodName: json['method_name'] as String?, className: json['class_name'] as String?, + declarationName: json['declaration_name'] as String?, ); } + + /// Method to convert parameter to JSON Map. + Map toJson() => { + 'method_name': ?methodName, + 'class_name': ?className, + 'declaration_name': ?declarationName, + }; + + @override + List get props => [methodName, className, declarationName]; } diff --git a/lib/src/common/parameters/excluded_identifiers_list_parameter.dart b/lib/src/common/parameters/excluded_identifiers_list_parameter.dart index d7ca8244..691a5f9d 100644 --- a/lib/src/common/parameters/excluded_identifiers_list_parameter.dart +++ b/lib/src/common/parameters/excluded_identifiers_list_parameter.dart @@ -90,4 +90,17 @@ class ExcludedIdentifiersListParameter { classDeclaration.namePart.typeName.lexeme == className; } } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ExcludedIdentifiersListParameter && + const ListEquality().equals( + other.exclude, + exclude, + ); + + @override + int get hashCode => + const ListEquality().hash(exclude); } diff --git a/lib/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart b/lib/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart new file mode 100644 index 00000000..688a31ad --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart @@ -0,0 +1,81 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/visitors/avoid_duplicate_code_visitor.dart'; +import 'package:solid_lints/src/models/solid_lint_rule.dart'; + +/// A lint rule that detects duplicated code blocks (clones) within a single +/// file and across multiple files in the project. +/// +/// When two or more function/method/constructor bodies have structurally +/// identical AST subtrees (Type 2 clones — same structure, variable names +/// may differ), the rule reports all copies and provides context messages +/// linking to the other occurrences. +/// +/// Cross-file detection works on a "best-effort" basis: duplicates are +/// detected against files that have already been analyzed in the current +/// session. The detection improves as more files are analyzed. +/// +/// ### Example config: +/// +/// ```yaml +/// plugins: +/// solid_lints: +/// diagnostics: +/// avoid_duplicate_code: +/// min_tokens: 50 +/// ignore_literals: false +/// ignore_identifiers: true +/// check_blocks: false +/// exclude: +/// - method_name: build +/// ``` +class AvoidDuplicateCodeRule + extends SolidLintRule { + /// Name of the lint. + static const lintName = 'avoid_duplicate_code'; + + static const _code = LintCode( + lintName, + 'Perhaps this code is a duplicate.\n' + 'Consider extracting the shared logic into a common function.', + ); + + @override + DiagnosticCode get diagnosticCode => _code; + + /// Creates a new instance of [AvoidDuplicateCodeRule]. + AvoidDuplicateCodeRule({ + required super.analysisOptionsLoader, + }) : super.withParameters( + name: lintName, + description: + 'Detects structurally identical function/method bodies ' + 'within a single file and across files (code clones).', + parametersParser: AvoidDuplicateCodeParameters.fromJson, + ); + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + super.registerNodeProcessors(registry, context); + + final parameters = + getParametersForContext(context) ?? + AvoidDuplicateCodeParameters.empty(); + + final visitor = AvoidDuplicateCodeVisitor( + this, + parameters, + filePath: context.definingUnit.file.path, + modificationStamp: context.definingUnit.file.modificationStamp, + contextRoot: context.libraryElement?.session.analysisContext.contextRoot, + resourceProvider: context.definingUnit.file.provider, + ); + + registry.addCompilationUnit(this, visitor); + } +} diff --git a/lib/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart b/lib/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart new file mode 100644 index 00000000..e8b3676c --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart @@ -0,0 +1,87 @@ +import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart'; + +/// Configuration parameters for the avoid_duplicate_code rule. +class AvoidDuplicateCodeParameters { + /// Minimum number of tokens in a function body or block to be considered + /// for clone detection. Bodies/blocks shorter than this are ignored. + final int minTokens; + + /// When `true`, literal values (strings, numbers, booleans) are excluded + /// from the structural hash, making the check ignore literal differences. + final bool ignoreLiterals; + + /// When `true`, local variable and parameter names are excluded + /// from the structural hash, allowing detection of renamed variables + /// (Type 2). Note that method, class, and field names are NOT ignored + /// to prevent excessive false positives. + final bool ignoreIdentifiers; + + /// When `true`, statement blocks (like if-blocks or loops) inside functions + /// are also checked for duplication. + final bool checkBlocks; + + /// A list of methods/functions that should be excluded from the lint. + final ExcludedIdentifiersListParameter exclude; + + static const _defaultMinTokens = 30; + + static final _defaultExclude = ExcludedIdentifiersListParameter( + exclude: const [], + ); + + /// Constructor for [AvoidDuplicateCodeParameters] model. + const AvoidDuplicateCodeParameters({ + required this.minTokens, + required this.ignoreLiterals, + required this.ignoreIdentifiers, + required this.checkBlocks, + required this.exclude, + }); + + /// Empty [AvoidDuplicateCodeParameters] model with default values. + factory AvoidDuplicateCodeParameters.empty() => AvoidDuplicateCodeParameters( + minTokens: _defaultMinTokens, + ignoreLiterals: false, + ignoreIdentifiers: true, + checkBlocks: true, + exclude: _defaultExclude, + ); + + /// Creates parameters from JSON configuration. + factory AvoidDuplicateCodeParameters.fromJson(Map json) => + AvoidDuplicateCodeParameters( + minTokens: json['min_tokens'] as int? ?? _defaultMinTokens, + ignoreLiterals: json['ignore_literals'] as bool? ?? false, + ignoreIdentifiers: json['ignore_identifiers'] as bool? ?? true, + checkBlocks: json['check_blocks'] as bool? ?? true, + exclude: ExcludedIdentifiersListParameter.defaultFromJson(json), + ); + + /// Converts the parameters to a JSON-compatible Map. + Map toJson() => { + 'min_tokens': minTokens, + 'ignore_literals': ignoreLiterals, + 'ignore_identifiers': ignoreIdentifiers, + 'check_blocks': checkBlocks, + 'exclude': exclude.exclude.map((e) => e.toJson()).toList(), + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AvoidDuplicateCodeParameters && + other.minTokens == minTokens && + other.ignoreLiterals == ignoreLiterals && + other.ignoreIdentifiers == ignoreIdentifiers && + other.checkBlocks == checkBlocks && + other.exclude == exclude; + + @override + int get hashCode => Object.hash( + minTokens, + ignoreLiterals, + ignoreIdentifiers, + checkBlocks, + exclude, + ); +} diff --git a/lib/src/lints/avoid_duplicate_code/models/body_candidate.dart b/lib/src/lints/avoid_duplicate_code/models/body_candidate.dart new file mode 100644 index 00000000..8e0b2b83 --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/models/body_candidate.dart @@ -0,0 +1,7 @@ +import 'package:analyzer/dart/ast/ast.dart'; + +/// A record representing a block or expression candidate for clone detection. +typedef BodyCandidate = ({ + AstNode node, + Declaration? enclosingDeclaration, +}); diff --git a/lib/src/lints/avoid_duplicate_code/models/cross_file_match.dart b/lib/src/lints/avoid_duplicate_code/models/cross_file_match.dart new file mode 100644 index 00000000..a8889b3c --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/models/cross_file_match.dart @@ -0,0 +1,27 @@ +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/duplicate_location.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/hash_entry.dart'; + +/// Represents a structural duplicate found in other files. +class CrossFileMatch { + /// The hash entry in the current file. + final HashEntry currentEntry; + + /// The list of locations in other files where a duplicate of this entry + /// exists. + final List duplicates; + + /// Creates a new [CrossFileMatch]. + const CrossFileMatch({ + required this.currentEntry, + required this.duplicates, + }); +} + +/// Extension on an iterable of [CrossFileMatch] to group duplicates by hash. +extension CrossFileMatchIterableExtension on Iterable { + /// Converts this iterable of cross-file matches to a map of duplicates + /// grouped by hash. + Map> toDuplicatesByHash() => { + for (final match in this) match.currentEntry.hash: match.duplicates, + }; +} diff --git a/lib/src/lints/avoid_duplicate_code/models/duplicate_location.dart b/lib/src/lints/avoid_duplicate_code/models/duplicate_location.dart new file mode 100644 index 00000000..d4c90cd2 --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/models/duplicate_location.dart @@ -0,0 +1,27 @@ +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/hash_entry.dart'; + +/// Represents a location in the codebase where a duplicate was found. +class DuplicateLocation { + /// The matching hash entry in the other file. + final HashEntry entry; + + /// The absolute path of the other file. + final String filePath; + + /// Creates a new [DuplicateLocation]. + const DuplicateLocation({ + required this.entry, + required this.filePath, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DuplicateLocation && + other.filePath == filePath && + other.entry.hash == entry.hash && + other.entry.offset == entry.offset; + + @override + int get hashCode => Object.hash(filePath, entry.hash, entry.offset); +} diff --git a/lib/src/lints/avoid_duplicate_code/models/file_cache_entry.dart b/lib/src/lints/avoid_duplicate_code/models/file_cache_entry.dart new file mode 100644 index 00000000..79e34d7d --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/models/file_cache_entry.dart @@ -0,0 +1,37 @@ +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/hash_entry.dart'; +import 'package:solid_lints/src/utils/iterable_utils.dart'; + +/// A cache entry containing the file's modification stamp and its structural +/// hashes. +class FileCacheEntry { + /// The file modification stamp. + final int modificationStamp; + + /// The list of structural hash entries for this file. + final List entries; + + /// Creates a new [FileCacheEntry]. + const FileCacheEntry({ + required this.modificationStamp, + required this.entries, + }); + + /// Converts this [FileCacheEntry] to a JSON map. + Map toJson() => { + 'm': modificationStamp, + 'e': entries.map((e) => e.toJson()).toList(), + }; + + /// Parses a [FileCacheEntry] from a JSON map. + FileCacheEntry.fromJson(Map json) + : modificationStamp = (json['m'] as int?) ?? 0, + entries = switch (json['e']) { + final List eValue => + eValue + .whereType>() + .tryMap(HashEntry.fromJson) + .nonNulls + .toList(), + _ => [], + }; +} diff --git a/lib/src/lints/avoid_duplicate_code/models/hash_entry.dart b/lib/src/lints/avoid_duplicate_code/models/hash_entry.dart new file mode 100644 index 00000000..f4fbdb2f --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/models/hash_entry.dart @@ -0,0 +1,47 @@ +/// A single structural hash entry representing a function body or block +/// candidate for cross-file clone detection. +class HashEntry { + /// The structural hash of the AST subtree. + final int hash; + + /// The line number where this candidate starts. + final int lineNumber; + + /// The character offset from the start of the file. + final int offset; + + /// The length of the code block. + final int length; + + /// The number of tokens in the candidate body. + final int tokenCount; + + /// Returns the offset and length as a range tuple. + (int, int) get range => (offset, length); + + /// Creates a new [HashEntry]. + const HashEntry({ + required this.hash, + required this.lineNumber, + required this.tokenCount, + this.offset = 0, + this.length = 0, + }); + + /// Converts this [HashEntry] to a JSON-compatible map using shortened keys. + Map toJson() => { + 'h': hash, + 'n': lineNumber, + 'o': offset, + 'l': length, + 't': tokenCount, + }; + + /// Creates a [HashEntry] from a JSON map. + HashEntry.fromJson(Map json) + : hash = json['h']! as int, + lineNumber = json['n']! as int, + offset = (json['o'] ?? 0) as int, + length = (json['l'] ?? 0) as int, + tokenCount = (json['t'] ?? json['s'] ?? 0) as int; +} diff --git a/lib/src/lints/avoid_duplicate_code/services/global_hash_registry.dart b/lib/src/lints/avoid_duplicate_code/services/global_hash_registry.dart new file mode 100644 index 00000000..b77ac50d --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/services/global_hash_registry.dart @@ -0,0 +1,300 @@ +import 'dart:io' as io; + +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; + +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/cross_file_match.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/duplicate_location.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/file_cache_entry.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/hash_entry.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/services/hash_cache_storage.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/utils/debouncer.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/utils/hash_entry_list_extension.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/utils/path_utils.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/visitors/avoid_duplicate_code_visitor.dart'; +import 'package:solid_lints/src/utils/iterable_utils.dart'; +import 'package:solid_lints/src/utils/set_dictionary.dart'; + +/// A singleton registry that stores structural hashes for all analyzed files. +/// +/// This registry lives as long as the plugin isolate, enabling cross-file +/// clone detection on a "best-effort" basis — duplicates are detected +/// against files that have already been analyzed in the current session. +/// +/// When a file is analyzed, its hash entries are stored in the registry. +/// Subsequent file analyses check their hashes against the registry to find +/// cross-file duplicates. +class GlobalHashRegistry { + static const _saveDebounceDuration = Duration(milliseconds: 500); + + /// Singleton instance — lives as long as the plugin isolate. + static final instance = GlobalHashRegistry._(); + + /// Whether to automatically clean up files that do not exist physically on + /// disk. + /// + /// Set to `false` in tests using a virtual resource provider. + bool enablePhysicalFileCleanup = true; + + /// The resource provider used for file system operations. + ResourceProvider resourceProvider = PhysicalResourceProvider.INSTANCE; + + /// Internal index: filePath → FileCacheEntry. + final _index = {}; + + /// Auxiliary inverted index: hash → Set. + final _hashToLocations = SetDictionary(); + + final _loadedRoots = {}; + final _saveDebouncers = {}; + + GlobalHashRegistry._(); + + /// The number of files currently indexed. + int get fileCount => _index.length; + + String _getRoot(String? packageRoot) => + packageRoot ?? io.Directory.current.path; + + void _addToInvertedIndex( + String absoluteFilePath, + List entries, + ) => _hashToLocations.addAll(entries.asIndexEntries(absoluteFilePath)); + + void _removeFromInvertedIndex( + String absoluteFilePath, + List entries, + ) => _hashToLocations.removeAll(entries.asIndexEntries(absoluteFilePath)); + + void _ensureLoaded( + String packageRoot, [ + AvoidDuplicateCodeParameters? currentParams, + ]) { + final params = currentParams ?? AvoidDuplicateCodeParameters.empty(); + if (_loadedRoots[packageRoot] case final previousParams?) { + if (previousParams == params) return; + + _clearEntriesForRoot(packageRoot); + } + + _loadedRoots[packageRoot] = params; + + final cached = HashCacheStorage( + packageRoot: packageRoot, + resourceProvider: resourceProvider, + currentParams: params, + ).load(); + if (cached == null) return; + + final deletedFiles = !enablePhysicalFileCleanup + ? {} + : cached.keys.where((p) => !resourceProvider.getFile(p).exists).toSet(); + + final hashes = cached.keys.toSet().difference(deletedFiles); + + for (final k in hashes) { + _index[k] = cached[k]!; + _addToInvertedIndex(k, cached[k]!.entries); + } + + if (deletedFiles.isNotEmpty) _scheduleSave(packageRoot, params); + } + + void _clearEntriesForRoot(String packageRoot) { + _index.removeWhere((key, oldEntry) { + if (PathUtils.isWithinOrEqual(packageRoot, key)) { + _removeFromInvertedIndex(key, oldEntry.entries); + return true; + } + return false; + }); + } + + String _resolveAndLoad( + String filePath, + String? packageRoot, + AvoidDuplicateCodeParameters? parameters, + ) { + final root = _getRoot(packageRoot); + _ensureLoaded(root, parameters); + return PathUtils.normalizePath(filePath, root); + } + + /// Returns the cached modification stamp for [filePath], or `null` if no + /// indexed. + int? getModificationStamp( + String filePath, { + AvoidDuplicateCodeParameters? parameters, + String? packageRoot, + }) => _getCache(filePath, packageRoot, parameters)?.modificationStamp; + + /// Returns the cached entries for [filePath], or `null` if not indexed. + List? getFileEntries( + String filePath, { + AvoidDuplicateCodeParameters? parameters, + String? packageRoot, + }) => _getCache(filePath, packageRoot, parameters)?.entries; + + FileCacheEntry? _getCache( + String filePath, + String? packageRoot, + AvoidDuplicateCodeParameters? parameters, + ) => _index[_resolveAndLoad(filePath, packageRoot, parameters)]; + + /// Updates the hash entries for [filePath], replacing any previous entries. + void updateFile( + String filePath, + List entries, { + required int modificationStamp, + AvoidDuplicateCodeParameters? parameters, + String? packageRoot, + }) { + final root = _getRoot(packageRoot); + final absoluteFilePath = _resolveAndLoad(filePath, packageRoot, parameters); + + if (_index[absoluteFilePath] case final oldEntry?) { + _removeFromInvertedIndex(absoluteFilePath, oldEntry.entries); + } + + _index[absoluteFilePath] = FileCacheEntry( + modificationStamp: modificationStamp, + entries: entries, + ); + _addToInvertedIndex(absoluteFilePath, entries); + _scheduleSave(root, parameters); + } + + /// Finds cross-file duplicates for [currentEntries] against all other + /// indexed files (excluding [currentFilePath]). + /// + /// Returns a list of [CrossFileMatch] objects, one for each duplicate found. + List findCrossFileMatches( + String currentFilePath, + List currentEntries, { + AvoidDuplicateCodeParameters? parameters, + bool Function(String filePath)? isFileExcluded, + String? packageRoot, + }) { + final root = _getRoot(packageRoot); + final absoluteCurrentFilePath = _resolveAndLoad( + currentFilePath, + packageRoot, + parameters, + ); + + final matches = []; + final filesToRemove = {}; + final fileStatusCache = {}; + + for (final entry in currentEntries) { + final duplicates = []; + final locations = _hashToLocations[entry.hash]; + + if (locations == null) continue; + + for (final loc in locations) { + final key = loc.filePath; + if (key == absoluteCurrentFilePath) continue; + + final isInvalid = fileStatusCache.putIfAbsent( + key, + () => + (enablePhysicalFileCleanup && + !resourceProvider.getFile(key).exists) || + (isFileExcluded?.call(key) ?? false), + ); + + if (isInvalid) { + filesToRemove.add(key); + } else if (PathUtils.isWithinOrEqual(root, key) && + // guard against hash collisions + loc.entry.tokenCount == entry.tokenCount) { + duplicates.add(loc); + } + } + + if (duplicates.isNotEmpty) { + matches.add( + CrossFileMatch( + currentEntry: entry, + duplicates: duplicates, + ), + ); + } + } + + if (filesToRemove.isEmpty) return matches; + + for (final file in filesToRemove) { + if (_index.remove(file) case final oldEntry?) { + _removeFromInvertedIndex(file, oldEntry.entries); + } + } + + _scheduleSave(root, parameters); + + return matches; + } + + /// Removes [filePath] from the index. + void removeFile( + String filePath, { + AvoidDuplicateCodeParameters? parameters, + String? packageRoot, + }) { + final root = _getRoot(packageRoot); + final absoluteFilePath = _resolveAndLoad(filePath, packageRoot, parameters); + if (_index.remove(absoluteFilePath) case final oldEntry?) { + _removeFromInvertedIndex(absoluteFilePath, oldEntry.entries); + } + _scheduleSave(root, parameters); + } + + void _scheduleSave( + String packageRoot, [ + AvoidDuplicateCodeParameters? parameters, + ]) { + _saveDebouncers + .putIfAbsent(packageRoot, () => Debouncer(_saveDebounceDuration)) + .run(() { + _performSave(packageRoot, parameters); + }); + } + + void _performSave( + String packageRoot, [ + AvoidDuplicateCodeParameters? parameters, + ]) => + HashCacheStorage( + packageRoot: packageRoot, + resourceProvider: resourceProvider, + currentParams: + parameters ?? + _loadedRoots[packageRoot] ?? + AvoidDuplicateCodeParameters.empty(), + ).save( + // Filter _index for files belonging to this packageRoot + _index.entries.whereKey( + (k) => PathUtils.isWithinOrEqual(packageRoot, k), + ), + ); + + /// Clears the entire index and deletes the persistent cache file. + /// + /// Primarily used in tests to ensure test isolation. + void clear() { + for (final debouncer in _saveDebouncers.values) { + debouncer.cancel(); + } + _saveDebouncers.clear(); + _loadedRoots.clear(); + _index.clear(); + _hashToLocations.clear(); + HashCacheStorage( + packageRoot: io.Directory.current.path, + resourceProvider: resourceProvider, + ).delete(); + AvoidDuplicateCodeVisitor.clearPackageRootCache(); + } +} diff --git a/lib/src/lints/avoid_duplicate_code/services/hash_cache_storage.dart b/lib/src/lints/avoid_duplicate_code/services/hash_cache_storage.dart new file mode 100644 index 00000000..2bc466c7 --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/services/hash_cache_storage.dart @@ -0,0 +1,93 @@ +import 'dart:convert'; + +import 'package:analyzer/file_system/file_system.dart'; +import 'package:collection/collection.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/file_cache_entry.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/utils/path_utils.dart'; +import 'package:solid_lints/src/utils/function_utils.dart'; +import 'package:solid_lints/src/utils/resource_provider_utils.dart'; + +/// Service responsible for persisting and loading the duplicate code hash +/// cache. +class HashCacheStorage { + static const _cacheDirName = '.dart_tool/solid_lints'; + static const _cacheFileName = 'duplicate_index.json'; + + /// The root directory of the package being analyzed. + final String packageRoot; + + /// The resource provider used for file system operations. + final ResourceProvider resourceProvider; + + /// The current configuration parameters. + final AvoidDuplicateCodeParameters currentParams; + + File get _file => resourceProvider.getFile( + resourceProvider.pathContext.join( + packageRoot, + _cacheDirName, + _cacheFileName, + ), + ); + + /// Constructor for [HashCacheStorage]. + HashCacheStorage({ + required this.packageRoot, + required this.resourceProvider, + AvoidDuplicateCodeParameters? currentParams, + }) : currentParams = currentParams ?? AvoidDuplicateCodeParameters.empty(); + + /// Loads the cached index from disk for [packageRoot]. + /// + /// Returns `null` if the cache file does not exist, is corrupted, or fails + /// to load. + Map? load() => FunctionUtils.tryOrNull(_load); + + Map _load() { + if (jsonDecode(_file.readAsStringSync()) case { + 'config': final cachedConfig, + 'files': final Map filesMap, + }) { + if (!const DeepCollectionEquality().equals( + cachedConfig, + currentParams.toJson(), + )) { + throw ArgumentError('Configuration mismatch'); + } + + return { + for (final MapEntry(:key, :value) in filesMap.entries) + if (PathUtils.normalizePath(key, packageRoot) case final absoluteKey) + if (value is Map) + if (FunctionUtils.tryOrNull(() => FileCacheEntry.fromJson(value)) + case final parsed?) + absoluteKey: parsed, + }; + } + + throw ArgumentError('Bad json'); + } + + /// Saves the [index] to disk for [packageRoot]. + void save(Map index) => FunctionUtils.tryOrNull( + () { + resourceProvider.ensureFolderExists(packageRoot, _cacheDirName); + _file.writeAsStringSync( + jsonEncode({ + 'version': 1, + 'config': currentParams.toJson(), + 'files': index.map( + (k, v) => MapEntry( + PathUtils.relativePath(k, packageRoot), + v.toJson(), + ), + ), + }), + ); + }, + ); + + /// Deletes the cache file for [packageRoot] if it exists. + void delete() => FunctionUtils.tryOrNull(_file.delete); +} diff --git a/lib/src/lints/avoid_duplicate_code/utils/context_root_extensions.dart b/lib/src/lints/avoid_duplicate_code/utils/context_root_extensions.dart new file mode 100644 index 00000000..11e33731 --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/utils/context_root_extensions.dart @@ -0,0 +1,12 @@ +import 'package:analyzer/dart/analysis/context_root.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/utils/path_utils.dart'; + +/// Extension methods for [ContextRoot] to help with file analysis checks. +extension ContextRootExtensions on ContextRoot { + /// Checks if [filePath] is excluded from analysis by this context root. + /// + /// Returns `true` only if the file is within the context root but is + /// explicitly excluded (e.g., via analysis_options.yaml). + bool isFileExcluded(String filePath) => + PathUtils.isWithinOrEqual(root.path, filePath) && !isAnalyzed(filePath); +} diff --git a/lib/src/lints/avoid_duplicate_code/utils/debouncer.dart b/lib/src/lints/avoid_duplicate_code/utils/debouncer.dart new file mode 100644 index 00000000..8500a155 --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/utils/debouncer.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +/// A utility class for debouncing quick, successive operations. +/// +/// It guarantees that an action is executed only once after a specified [delay] +/// of inactivity, cancelling any previously scheduled execution if a new one is +/// requested before the delay completes. +class Debouncer { + /// The duration to wait before executing the action. + final Duration delay; + Timer? _timer; + + /// Creates a new [Debouncer] with the given [delay]. + Debouncer(this.delay); + + /// Schedules [action] to be executed after the configured [delay]. + /// + /// If [run] is called again before the delay expires, the previous timer + /// is cancelled and a new one is started. + void run(void Function() action) { + _timer?.cancel(); + _timer = Timer(delay, action); + } + + /// Cancels any scheduled action from executing. + void cancel() { + _timer?.cancel(); + } +} diff --git a/lib/src/lints/avoid_duplicate_code/utils/hash_entry_list_extension.dart b/lib/src/lints/avoid_duplicate_code/utils/hash_entry_list_extension.dart new file mode 100644 index 00000000..4656646e --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/utils/hash_entry_list_extension.dart @@ -0,0 +1,15 @@ +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/duplicate_location.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/hash_entry.dart'; + +/// Extension methods for [List] of [HashEntry]. +extension HashEntryListExtension on List { + /// Converts entries into (hash, DuplicateLocation) tuple pairs for indexing. + Iterable<(int, DuplicateLocation)> asIndexEntries( + String absoluteFilePath, + ) => map( + (e) => ( + e.hash, + DuplicateLocation(entry: e, filePath: absoluteFilePath), + ), + ); +} diff --git a/lib/src/lints/avoid_duplicate_code/utils/jenkins_hasher.dart b/lib/src/lints/avoid_duplicate_code/utils/jenkins_hasher.dart new file mode 100644 index 00000000..7985fbce --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/utils/jenkins_hasher.dart @@ -0,0 +1,33 @@ +/// A custom implementation of the Jenkins One-at-a-Time hash algorithm. +/// +/// This incrementally computes a hash without allocating large strings, +/// significantly reducing GC pressure during tree traversal. +class JenkinsHasher { + static const _hashMask = 0x1fffffff; + static const _shift10Mask = 0x0007ffff; + static const _shift3Mask = 0x03ffffff; + static const _shift15Mask = 0x00003fff; + + int _hash = 0; + + /// Resets the hash to its initial state. + void reset() => _hash = 0; + + /// Adds a single 32-bit integer [value] to the hash. + void add(int value) { + _hash = _mix(_hashMask & (_hash + value), _shift10Mask, 10); + _hash ^= _hash >> 6; + } + + /// Adds all code units of [value] to the hash. + void addString(String value) => value.codeUnits.forEach(add); + + /// Finalizes and returns the computed hash. + int get hash { + final h = _mix(_hash, _shift3Mask, 3); + return _mix(h ^ (h >> 11), _shift15Mask, 15); + } + + static int _mix(int hash, int shiftMask, int shiftAmount) => + _hashMask & (hash + ((shiftMask & hash) << shiftAmount)); +} diff --git a/lib/src/lints/avoid_duplicate_code/utils/path_utils.dart b/lib/src/lints/avoid_duplicate_code/utils/path_utils.dart new file mode 100644 index 00000000..a75a520e --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/utils/path_utils.dart @@ -0,0 +1,25 @@ +import 'package:path/path.dart' as p; + +/// Utility methods for path manipulation and comparison. +abstract final class PathUtils { + /// Normalizes a file path, ensuring it is absolute and resolved correctly + /// relative to the given [root] if it isn't already absolute. + static String normalizePath(String filePath, String root) { + return p.isAbsolute(filePath) + ? p.normalize(filePath) + : p.normalize(p.join(root, filePath)); + } + + /// Converts a file path to a relative path from [root]. + static String relativePath(String filePath, String root) { + if (p.isAbsolute(filePath)) { + return p.relative(filePath, from: root); + } + return filePath; + } + + /// Checks if [filePath] is equal to [parentPath] or is located within it. + static bool isWithinOrEqual(String parentPath, String filePath) { + return p.equals(parentPath, filePath) || p.isWithin(parentPath, filePath); + } +} diff --git a/lib/src/lints/avoid_duplicate_code/utils/range_extension.dart b/lib/src/lints/avoid_duplicate_code/utils/range_extension.dart new file mode 100644 index 00000000..4331377b --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/utils/range_extension.dart @@ -0,0 +1,20 @@ +/// Extension on an (offset, length) record to check if it is strictly +/// within another parent (offset, length) range. +extension RangeExtension on (int, int) { + /// Returns `true` if this range is strictly inside the [parent] range + /// (meaning it is within the bounds but not exactly equal to the parent). + bool isStrictlyWithin((int, int) parent) { + return this.$1 >= parent.$1 && + (this.$1 + this.$2) <= (parent.$1 + parent.$2) && + !(this.$1 == parent.$1 && this.$2 == parent.$2); + } +} + +/// Extension on an iterable of (offset, length) ranges. +extension RangeIterableExtension on Iterable<(int, int)> { + /// Returns `true` if any range in this iterable strictly contains the [child] + /// range. + bool anyContainsStrictly((int, int) child) { + return any(child.isStrictlyWithin); + } +} diff --git a/lib/src/lints/avoid_duplicate_code/utils/token_utils.dart b/lib/src/lints/avoid_duplicate_code/utils/token_utils.dart new file mode 100644 index 00000000..7578392f --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/utils/token_utils.dart @@ -0,0 +1,17 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; + +/// Utility methods for token manipulation. +extension AstNodeTokenCount on AstNode { + /// Returns the total number of non-EOF tokens within this node. + int get tokenCount { + final end = endToken; + var count = 1; + + for (Token t = beginToken; t != end && t != t.next; t = t.next ?? end) { + count++; + } + + return count; + } +} diff --git a/lib/src/lints/avoid_duplicate_code/visitors/ast_structural_hash_visitor.dart b/lib/src/lints/avoid_duplicate_code/visitors/ast_structural_hash_visitor.dart new file mode 100644 index 00000000..49ea8816 --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/visitors/ast_structural_hash_visitor.dart @@ -0,0 +1,206 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/utils/jenkins_hasher.dart'; + +/// A [UnifyingAstVisitor] that builds a structural fingerprint of an AST +/// subtree for clone detection. +/// +/// The fingerprint captures the structure of the code (node types, operators, +/// and optionally literal values) while ignoring identifier names (like local +/// variables), whitespace, and comments. This enables Type 2 clone detection +/// where two code blocks with identical structure but different variable names +/// are considered clones. +class AstStructuralHashVisitor extends UnifyingAstVisitor { + static final _typeNameCache = {}; + static const _pipeAscii = 0x7C; // '|' + + final _hasher = JenkinsHasher(); + final _localVariableIds = {}; + final bool _ignoreLiterals; + final bool _ignoreIdentifiers; + + /// Creates a new [AstStructuralHashVisitor]. + AstStructuralHashVisitor({ + required bool ignoreLiterals, + required bool ignoreIdentifiers, + }) : _ignoreLiterals = ignoreLiterals, + _ignoreIdentifiers = ignoreIdentifiers; + + /// Computes the structural hash for the given [node]. + /// + /// Visits the entire subtree of [node] and returns an integer hash + /// of the accumulated structural fingerprint. + int computeHash(AstNode node) { + _hasher.reset(); + _localVariableIds.clear(); + node.accept(this); + return _hasher.hash; + } + + void _append(String value) => _hasher + ..addString(value) + ..add(_pipeAscii); + + void _appendHash(int hashCode) => _hasher + ..add(hashCode) + ..add(_pipeAscii); + + @override + void visitNode(AstNode node) { + // Use the type name string instead of runtimeType.hashCode, because + // identity-based hashCode changes between VM/isolate restarts, making + // hashes stored in the persistent cache incompatible with newly computed + // ones. This caused "flapping" warnings that appeared and disappeared. + // + // The result is cached in a static map to avoid repeated toString() + // reflection calls during tree traversal. + final type = node.runtimeType; + _append(_typeNameCache.putIfAbsent(type, type.toString)); + node.visitChildren(this); + _append('^'); + } + + @override + void visitVariableDeclarationStatement(VariableDeclarationStatement node) { + if (node.variables.keyword?.lexeme case final l?) _append(l); + super.visitVariableDeclarationStatement(node); + } + + @override + void visitIfStatement(IfStatement node) { + _appendHash(node.elseKeyword != null ? 1 : 0); + super.visitIfStatement(node); + } + + @override + void visitTryStatement(TryStatement node) { + _appendHash(node.finallyBlock != null ? 1 : 0); + super.visitTryStatement(node); + } + + @override + void visitYieldStatement(YieldStatement node) { + _appendHash(node.star != null ? 1 : 0); + super.visitYieldStatement(node); + } + + @override + void visitBinaryExpression(BinaryExpression node) { + _append(node.operator.lexeme); + super.visitBinaryExpression(node); + } + + @override + void visitPrefixExpression(PrefixExpression node) { + if (node case PrefixExpression( + operator: Token(type: TokenType.MINUS || TokenType.PLUS), + operand: IntegerLiteral() || DoubleLiteral(), + ) when _ignoreLiterals) { + node.operand.accept(this); + } else { + _append(node.operator.lexeme); + super.visitPrefixExpression(node); + } + } + + @override + void visitPostfixExpression(PostfixExpression node) { + _append(node.operator.lexeme); + super.visitPostfixExpression(node); + } + + @override + void visitAssignmentExpression(AssignmentExpression node) { + _append(node.operator.lexeme); + super.visitAssignmentExpression(node); + } + + @override + void visitIsExpression(IsExpression node) { + _appendHash(node.notOperator != null ? 1 : 0); + super.visitIsExpression(node); + } + + @override + void visitNamedExpression(NamedExpression node) { + _append(node.name.label.name); + super.visitNamedExpression(node); + } + + // --- Literals --- + + @override + void visitIntegerLiteral(IntegerLiteral node) { + if (!_ignoreLiterals) { + _append(node.literal.lexeme); + } + super.visitIntegerLiteral(node); + } + + @override + void visitDoubleLiteral(DoubleLiteral node) { + if (!_ignoreLiterals) { + _append(node.literal.lexeme); + } + super.visitDoubleLiteral(node); + } + + @override + void visitSimpleStringLiteral(SimpleStringLiteral node) { + if (!_ignoreLiterals) { + _append(node.value); + } + super.visitSimpleStringLiteral(node); + } + + @override + void visitInterpolationString(InterpolationString node) { + if (!_ignoreLiterals) { + _append(node.value); + } + super.visitInterpolationString(node); + } + + @override + void visitBooleanLiteral(BooleanLiteral node) { + if (!_ignoreLiterals) { + _appendHash(node.value ? 1 : 0); + } + super.visitBooleanLiteral(node); + } + + // --- Identifiers --- + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + final element = node.element; + + switch (element) { + // Smart ignore: ignore ONLY local variables and parameters. + // This preserves field names, getters, methods, class names, etc. + case LocalVariableElement() || FormalParameterElement() + when _ignoreIdentifiers: + // It is a local variable or parameter. + // Assign it a local ID (De Bruijn Indexing) to distinguish clones + // that wire their variables differently. + _appendHash( + _localVariableIds.putIfAbsent( + element!, + () => _localVariableIds.length, + ), + ); + default: + _append(node.name); + } + + super.visitSimpleIdentifier(node); + } + + @override + void visitNamedType(NamedType node) { + _append(node.name.lexeme); + super.visitNamedType(node); + } +} diff --git a/lib/src/lints/avoid_duplicate_code/visitors/avoid_duplicate_code_visitor.dart b/lib/src/lints/avoid_duplicate_code/visitors/avoid_duplicate_code_visitor.dart new file mode 100644 index 00000000..c39435f5 --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/visitors/avoid_duplicate_code_visitor.dart @@ -0,0 +1,303 @@ +import 'package:analyzer/dart/analysis/context_root.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/diagnostic/diagnostic.dart'; +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:collection/collection.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/body_candidate.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/cross_file_match.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/duplicate_location.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/hash_entry.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/services/global_hash_registry.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/utils/context_root_extensions.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/utils/range_extension.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/utils/token_utils.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/visitors/ast_structural_hash_visitor.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/visitors/candidate_visitor.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/visitors/descendant_visitor.dart'; +import 'package:solid_lints/src/models/solid_diagnostic_message.dart'; + +/// A visitor that detects duplicate code blocks (at the function level and/or +/// statement block level) within a single compilation unit and across files. +class AvoidDuplicateCodeVisitor extends RecursiveAstVisitor { + static const _duplicateContextMessage = 'Duplicate'; + static final _packageRootCache = {}; + + final AvoidDuplicateCodeRule _rule; + final AvoidDuplicateCodeParameters _parameters; + final String _filePath; + final int _modificationStamp; + final ContextRoot? _contextRoot; + final ResourceProvider _resourceProvider; + + /// Creates a new instance of [AvoidDuplicateCodeVisitor]. + AvoidDuplicateCodeVisitor( + this._rule, + this._parameters, { + required String filePath, + required int modificationStamp, + ContextRoot? contextRoot, + ResourceProvider? resourceProvider, + }) : _filePath = filePath, + _modificationStamp = modificationStamp, + _contextRoot = contextRoot, + _resourceProvider = + resourceProvider ?? PhysicalResourceProvider.INSTANCE; + + @override + void visitCompilationUnit(CompilationUnit node) { + if (_filePath.isEmpty) return; + GlobalHashRegistry.instance.resourceProvider = _resourceProvider; + if (_tryReportFromCache( + _filePath, + _contextRoot?.root.path ?? _findPackageRoot(_filePath) ?? '', + )) { + return; + } + final hasher = AstStructuralHashVisitor( + ignoreLiterals: _parameters.ignoreLiterals, + ignoreIdentifiers: _parameters.ignoreIdentifiers, + ); + final candidates = _collectCandidates(node); + final candidateHashes = { + for (final BodyCandidate(:node) in candidates) + node: hasher.computeHash(node), + }; + final crossFileDuplicatesByHash = _findAndSaveCrossFileMatches( + _filePath, + candidates + .map( + (c) => HashEntry( + hash: candidateHashes[c.node]!, + lineNumber: node.lineInfo.getLocation(c.node.offset).lineNumber, + offset: c.node.offset, + length: c.node.length, + tokenCount: c.node.tokenCount, + ), + ) + .toList(), + _contextRoot?.root.path ?? _findPackageRoot(_filePath) ?? '', + ); + _groupAndReportDuplicates( + _filePath, + candidates, + candidateHashes, + hasher, + crossFileDuplicatesByHash, + ); + } + + bool _tryReportFromCache(String filePath, String packageRoot) { + if (packageRoot.isEmpty) return false; + + final registry = GlobalHashRegistry.instance; + final cachedStamp = registry.getModificationStamp( + filePath, + parameters: _parameters, + packageRoot: packageRoot, + ); + + if (cachedStamp != _modificationStamp) return false; + + final cachedEntries = registry.getFileEntries( + filePath, + parameters: _parameters, + packageRoot: packageRoot, + ); + if (cachedEntries == null) return false; + + final crossMatches = registry.findCrossFileMatches( + filePath, + cachedEntries, + parameters: _parameters, + packageRoot: packageRoot, + isFileExcluded: (p) => _contextRoot?.isFileExcluded(p) ?? false, + ); + + final hashGroups = groupBy(cachedEntries, (entry) => entry.hash); + + final hasIntraDuplicates = hashGroups.values.any( + (group) => group.length > 1, + ); + final hasCrossDuplicates = crossMatches.isNotEmpty; + + if (!hasIntraDuplicates && !hasCrossDuplicates) { + return true; // We checked the cache, and there are no duplicates. + } + + final suppressedRanges = <(int, int)>[]; + final reportedOffsets = {}; + + final crossFileDuplicatesByHash = crossMatches.toDuplicatesByHash(); + + for (final entry in cachedEntries) { + final group = hashGroups[entry.hash]; + if (suppressedRanges.anyContainsStrictly(entry.range) || + reportedOffsets.contains(entry.offset) || + group == null) { + continue; + } + + final activeGroup = group + .where((e) => !suppressedRanges.anyContainsStrictly(e.range)) + .toList(); + + final externalPartners = + crossFileDuplicatesByHash[entry.hash] ?? const []; + final internalPartners = activeGroup.where((e) => e != entry).toList(); + + if (internalPartners.isNotEmpty || externalPartners.isNotEmpty) { + final contextMessages = _buildContextMessages( + currentFilePath: filePath, + externalPartners: externalPartners, + internalPartners: internalPartners.map((e) => (e.offset, e.length)), + ); + + _rule.reportAtOffset( + entry.offset, + entry.length, + contextMessages: contextMessages, + ); + + reportedOffsets.add(entry.offset); + suppressedRanges.add((entry.offset, entry.length)); + } + } + + return true; // Cache hit and handled + } + + List _collectCandidates(CompilationUnit node) { + final collector = CandidateVisitor(_parameters); + node.accept(collector); + return collector.candidates; + } + + Map> _findAndSaveCrossFileMatches( + String filePath, + List hashEntries, + String packageRoot, + ) { + if (packageRoot.isEmpty) return const {}; + + final registry = GlobalHashRegistry.instance; + final crossMatches = registry.findCrossFileMatches( + filePath, + hashEntries, + parameters: _parameters, + packageRoot: packageRoot, + isFileExcluded: (p) => _contextRoot?.isFileExcluded(p) ?? false, + ); + registry.updateFile( + filePath, + hashEntries, + modificationStamp: _modificationStamp, + parameters: _parameters, + packageRoot: packageRoot, + ); + + return crossMatches.toDuplicatesByHash(); + } + + void _groupAndReportDuplicates( + String filePath, + List candidates, + Map candidateHashes, + AstStructuralHashVisitor hasher, + Map> crossFileDuplicatesByHash, + ) { + final groups = groupBy( + candidates, + (c) => candidateHashes[c.node] ?? hasher.computeHash(c.node), + ); + + final suppressed = {}; + + for (final candidate in candidates.toSet()) { + // Skip candidates that are descendants of an already reported duplicate + // block to prevent nested warnings. + if (suppressed.contains(candidate.node)) continue; + + final hash = + candidateHashes[candidate.node] ?? hasher.computeHash(candidate.node); + final internalPartners = groups[hash]?.toList(); + if (internalPartners == null) continue; + + final externalPartners = crossFileDuplicatesByHash[hash] ?? const []; + internalPartners + ..removeWhere((c) => suppressed.contains(c.node)) + ..remove(candidate); + + if (internalPartners.isEmpty && externalPartners.isEmpty) continue; + + _rule.reportAtNode( + candidate.node, + contextMessages: _buildContextMessages( + currentFilePath: filePath, + externalPartners: externalPartners, + internalPartners: internalPartners.map( + (c) => (c.node.offset, c.node.length), + ), + ), + ); + + _suppressDescendants(candidate.node, suppressed); + } + } + + void _suppressDescendants(AstNode root, Set suppressed) { + final descendantCollector = DescendantVisitor(suppressed, root); + root.accept(descendantCollector); + } + + List _buildContextMessages({ + required String currentFilePath, + required List externalPartners, + required Iterable<(int offset, int length)> internalPartners, + }) { + return [ + for (final dup in externalPartners) + SolidDiagnosticMessage( + filePath: dup.filePath, + offset: dup.entry.offset, + length: dup.entry.length, + message: _duplicateContextMessage, + ), + for (final (offset, length) in internalPartners) + SolidDiagnosticMessage( + filePath: currentFilePath, + offset: offset, + length: length, + message: _duplicateContextMessage, + ), + ]; + } + + /// Clears the cached package root lookups. Should be called when + /// the registry is cleared to avoid stale project path references. + static void clearPackageRootCache() => _packageRootCache.clear(); + + String? _findPackageRoot(String filePath) { + if (filePath.isEmpty) return null; + final pathContext = _resourceProvider.pathContext; + final dirPath = pathContext.dirname(filePath); + return _packageRootCache.putIfAbsent(dirPath, () { + var dir = _resourceProvider.getFolder(dirPath); + while (true) { + final pubspec = dir.getChildAssumingFile('pubspec.yaml'); + if (pubspec.exists) { + return dir.path; + } + final parent = dir.parent; + if (parent.path == dir.path) { + break; + } + dir = parent; + } + return null; + }); + } +} diff --git a/lib/src/lints/avoid_duplicate_code/visitors/candidate_visitor.dart b/lib/src/lints/avoid_duplicate_code/visitors/candidate_visitor.dart new file mode 100644 index 00000000..6b840a4d --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/visitors/candidate_visitor.dart @@ -0,0 +1,48 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/body_candidate.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/utils/token_utils.dart'; + +/// A visitor that collects all block-level duplicate code candidates. +class CandidateVisitor extends RecursiveAstVisitor { + /// The list of collected candidates. + final List candidates = []; + + /// The parameters for the lint rule. + final AvoidDuplicateCodeParameters parameters; + + /// Creates a new instance of [CandidateVisitor]. + CandidateVisitor(this.parameters); + + @override + void visitBlock(Block node) => + // If checkBlocks is false, only consider blocks that represent function + // bodies. + !parameters.checkBlocks && node.parent is! BlockFunctionBody + ? super.visitBlock(node) + : _visit(node, super.visitBlock); + + @override + void visitExpressionFunctionBody(ExpressionFunctionBody node) => + _visit(node, super.visitExpressionFunctionBody); + + void _visit( + T node, + void Function(T) visitSuper, + ) { + if (node.tokenCount < parameters.minTokens) return; + + final declaration = node.thisOrAncestorOfType(); + if (declaration != null && parameters.exclude.shouldIgnore(declaration)) { + return; + } + + candidates.add(( + node: node, + enclosingDeclaration: declaration, + )); + + visitSuper(node); + } +} diff --git a/lib/src/lints/avoid_duplicate_code/visitors/descendant_visitor.dart b/lib/src/lints/avoid_duplicate_code/visitors/descendant_visitor.dart new file mode 100644 index 00000000..3c22b9dc --- /dev/null +++ b/lib/src/lints/avoid_duplicate_code/visitors/descendant_visitor.dart @@ -0,0 +1,26 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; + +/// A visitor that collects descendant candidate blocks to suppress them. +class DescendantVisitor extends RecursiveAstVisitor { + /// The set of suppressed nodes. + final Set suppressed; + + /// The root node from which to start the descent. + final AstNode root; + + /// Creates a new instance of [DescendantVisitor]. + DescendantVisitor(this.suppressed, this.root); + + @override + void visitBlock(Block node) => _visit(node, super.visitBlock); + + @override + void visitExpressionFunctionBody(ExpressionFunctionBody node) => + _visit(node, super.visitExpressionFunctionBody); + + void _visit(T node, void Function(T) visitSuper) { + if (node != root) suppressed.add(node); + visitSuper(node); + } +} diff --git a/lib/src/models/solid_diagnostic_message.dart b/lib/src/models/solid_diagnostic_message.dart new file mode 100644 index 00000000..85d2ce09 --- /dev/null +++ b/lib/src/models/solid_diagnostic_message.dart @@ -0,0 +1,32 @@ +import 'package:analyzer/diagnostic/diagnostic.dart'; + +/// A concrete implementation of [DiagnosticMessage] for reporting diagnostics +/// with context messages in solid_lints rules. +class SolidDiagnosticMessage implements DiagnosticMessage { + @override + final String filePath; + + @override + final int length; + + @override + final int offset; + + @override + String? get url => null; + + final String _message; + + /// Creates a new instance of [SolidDiagnosticMessage]. + SolidDiagnosticMessage({ + required this.filePath, + required this.length, + required String message, + required this.offset, + }) : _message = message; + + @override + String messageText({required bool includeUrl}) { + return _message; + } +} diff --git a/lib/src/utils/function_utils.dart b/lib/src/utils/function_utils.dart new file mode 100644 index 00000000..a1059251 --- /dev/null +++ b/lib/src/utils/function_utils.dart @@ -0,0 +1,12 @@ +/// Utility functions. +abstract final class FunctionUtils { + /// Executes the [f], returning its result or `null` if any + /// exception/error is thrown. + static T? tryOrNull(T Function() f) { + try { + return f(); + } catch (_) { + return null; + } + } +} diff --git a/lib/src/utils/iterable_utils.dart b/lib/src/utils/iterable_utils.dart index 3dcf538b..c5f195f6 100644 --- a/lib/src/utils/iterable_utils.dart +++ b/lib/src/utils/iterable_utils.dart @@ -26,6 +26,28 @@ extension IterablePairwise on Iterable { } } +/// Extension on [Iterable] that provides a [tryMap] method. +extension IterableTryMap on Iterable { + /// Maps each element using [f], catching exceptions and returning null for + /// those elements. + Iterable tryMap(U Function(T) f) => map((e) { + try { + return f(e); + } catch (_) { + return null; + } + }); +} + +/// Extension on [Iterable] of [MapEntry] to filter and convert to [Map]. +extension MapEntryIterableExtension on Iterable> { + /// Filters entries by key and returns a new [Map]. + Map whereKey(bool Function(K key) test) => { + for (final entry in this) + if (test(entry.key)) entry.key: entry.value, + }; +} + /// Extension on [Iterable] to zip elements with another iterable. extension IterableZip on Iterable { /// Zips this iterable with [other]. diff --git a/lib/src/utils/resource_provider_utils.dart b/lib/src/utils/resource_provider_utils.dart new file mode 100644 index 00000000..6541026e --- /dev/null +++ b/lib/src/utils/resource_provider_utils.dart @@ -0,0 +1,9 @@ +import 'package:analyzer/file_system/file_system.dart'; + +/// Extension on [ResourceProvider] to provide folder helpers. +extension ResourceProviderUtils on ResourceProvider { + /// Ensures that a folder at the path joined from [root] and [dir] exists, + /// creating it if it doesn't. + void ensureFolderExists(String root, String dir) => + getFolder(pathContext.join(root, dir)).create(); +} diff --git a/lib/src/utils/set_dictionary.dart b/lib/src/utils/set_dictionary.dart new file mode 100644 index 00000000..cb5b1050 --- /dev/null +++ b/lib/src/utils/set_dictionary.dart @@ -0,0 +1,46 @@ +/// A dictionary that maps a key to a set of values. +class SetDictionary { + final _map = >{}; + + /// Adds a [value] to the set associated with the [key]. + void add(K key, V value) { + (_map[key] ??= {}).add(value); + } + + /// Adds all [entries] (key-value pairs) to the dictionary. + void addAll(Iterable<(K, V)> entries) { + for (final (key, value) in entries) { + add(key, value); + } + } + + /// Removes a [value] from the set associated with the [key]. + /// + /// If the set becomes empty after removing the value, the [key] is removed + /// from the dictionary. + void remove(K key, V value) { + final set = _map[key]; + if (set == null) return; + + set.remove(value); + if (set.isEmpty) { + _map.remove(key); + } + } + + /// Removes all [entries] (key-value pairs) from the dictionary. + void removeAll(Iterable<(K, V)> entries) { + for (final (key, value) in entries) { + remove(key, value); + } + } + + /// Returns the set of values associated with the [key], or `null` if the + /// key is not in the dictionary. + Set? operator [](K key) => _map[key]; + + /// Removes all keys and values from the dictionary. + void clear() { + _map.clear(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index a5276bf7..09b05007 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: analyzer: ^10.0.1 collection: ^1.19.0 analysis_server_plugin: ^0.3.3 + equatable: ^2.1.0 glob: ^2.1.3 path: ^1.9.1 yaml: ^3.1.3 diff --git a/test/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule_test.dart b/test/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule_test.dart new file mode 100644 index 00000000..04573387 --- /dev/null +++ b/test/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule_test.dart @@ -0,0 +1,485 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/common/parameters/excluded_identifier_parameter.dart'; +import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/avoid_duplicate_code_rule.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/services/global_hash_registry.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/visitors/avoid_duplicate_code_visitor.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../utils/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidDuplicateCodeRuleTest); + }); +} + +@reflectiveTest +class AvoidDuplicateCodeRuleTest extends AnalysisRuleTest + with AutoTestLintOffsets { + static const _mockAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + avoid_duplicate_code: + min_tokens: 15 + check_blocks: true + exclude: + - method_name: excluded + '''; + + @override + void setUp() { + GlobalHashRegistry.instance.clear(); + GlobalHashRegistry.instance.enablePhysicalFileCleanup = false; + rule = AvoidDuplicateCodeRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); + } + + @override + Future tearDown() async { + GlobalHashRegistry.instance.clear(); + await super.tearDown(); + } + + // --- Base Tests (min_tokens: 15, check_blocks: true, default) --- + + Future test_reports_when_two_functions_have_identical_bodies() async { + await assertAutoDiagnostics(''' +void first() ${expectLint(r'''{ + final x = 1; + if (x > 0) { + print(x); + } + print('done'); +}''')} + +void second() ${expectLint(r'''{ + final x = 1; + if (x > 0) { + print(x); + } + print('done'); +}''')} +'''); + } + + Future + test_reports_when_functions_have_same_structure_different_names() async { + await assertAutoDiagnostics(''' +void fetchUsers() ${expectLint(r'''{ + final response = 'data'; + if (response.isEmpty) { + throw Exception('error'); + } + print(response); +}''')} + +void fetchOrders() ${expectLint(r'''{ + final result = 'data'; + if (result.isEmpty) { + throw Exception('error'); + } + print(result); +}''')} +'''); + } + + Future + test_does_not_report_when_functions_have_different_structure() async { + await assertNoDiagnostics(r''' +void first() { + final x = 1; + if (x > 0) { + print(x); + } + print('done'); +} + +void second() { + final x = 1; + while (x > 0) { + print(x); + } + return; +} +'''); + } + + Future test_does_not_report_when_body_below_min_tokens() async { + await assertNoDiagnostics(r''' +void first() { + print('hello'); + print('world'); +} + +void second() { + print('hello'); + print('world'); +} +'''); + } + + Future test_reports_on_methods_in_same_class() async { + await assertAutoDiagnostics(''' +class MyClass { + void first() ${expectLint(r'''{ + final x = 1; + if (x > 0) { + print(x); + } + print('done'); + }''')} + + void second() ${expectLint(r'''{ + final y = 1; + if (y > 0) { + print(y); + } + print('done'); + }''')} +} +'''); + } + + Future test_reports_on_third_clone_also() async { + await assertAutoDiagnostics(''' +void first() ${expectLint(r'''{ + final x = 1; + if (x > 0) { + print(x); + } + print('done'); +}''')} + +void second() ${expectLint(r'''{ + final y = 1; + if (y > 0) { + print(y); + } + print('done'); +}''')} + +void third() ${expectLint(r'''{ + final z = 1; + if (z > 0) { + print(z); + } + print('done'); +}''')} +'''); + } + + // --- Ignore Literals Tests --- + + Future test_reports_when_only_literal_values_differ() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + avoid_duplicate_code: + min_tokens: 15 + ignore_literals: true +''', + ); + await assertAutoDiagnostics(''' +void first() ${expectLint(r'''{ + final x = 1; + if (x > 0) { + print('hello'); + } + print('world'); +}''')} + +void second() ${expectLint(r'''{ + final y = 2; + if (y > 0) { + print('foo'); + } + print('bar'); +}''')} +'''); + } + + Future + test_does_not_report_when_literal_values_differ_without_flag() async { + await assertNoDiagnostics(r''' +void first() { + final x = 1; + if (x > 0) { + print('a'); + } + print('b'); +} + +void second() { + final y = 99; + if (y > 0) { + print('c'); + } + print('d'); +} +'''); + } + + // --- Ignore Identifiers Tests --- + + Future + test_does_not_report_when_identifiers_differ_without_flag() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + avoid_duplicate_code: + min_tokens: 15 + ignore_identifiers: false +''', + ); + await assertNoDiagnostics(r''' +void first() { + final x = 1; + if (x > 0) { + print(x); + } + print('done'); +} + +void second() { + final y = 1; + if (y > 0) { + print(y); + } + print('done'); +} +'''); + } + + // --- Exclude Tests --- + + Future test_does_not_report_on_excluded_function() async { + await assertNoDiagnostics(r''' +void first() { + final x = 1; + if (x > 0) { + print(x); + } + print('done'); +} + +void excluded() { + final y = 1; + if (y > 0) { + print(y); + } + print('done'); +} +'''); + } + + // --- Check Blocks Tests --- + + Future + test_reports_duplicate_blocks_inside_different_functions() async { + await assertAutoDiagnostics(''' +void one() { + final x = 1; + if (x > 0) ${expectLint(r'''{ + print('hello'); + print('world'); + print('done'); + }''')} +} + +void two() { + final y = 2; + ${expectLint(r'''{ + print('hello'); + print('world'); + print('done'); + }''')} +} +'''); + } + + Future + test_reports_parent_but_not_nested_blocks_when_parent_is_reported() async { + await assertAutoDiagnostics(''' +void one() ${expectLint(r'''{ + final x = 1; + if (x > 0) { + print('hello'); + print('world'); + print('done'); + } + print('done'); +}''')} + +void two() ${expectLint(r'''{ + final y = 1; + if (y > 0) { + print('hello'); + print('world'); + print('done'); + } + print('done'); +}''')} +'''); + } + + Future test_reports_cross_file_duplicate_when_in_registry() async { + final otherFile = newFile('$testPackageLibPath/other.dart', ''' +void otherMethod() { + final x = 1; + if (x > 0) { + print(x); + } + print('done'); +} +'''); + + final resolvedOther = await resolveFile(otherFile.path); + final visitor = AvoidDuplicateCodeVisitor( + rule as AvoidDuplicateCodeRule, + AvoidDuplicateCodeParameters( + minTokens: 15, + ignoreLiterals: false, + ignoreIdentifiers: true, + checkBlocks: true, + exclude: ExcludedIdentifiersListParameter( + exclude: [ExcludedIdentifierParameter(methodName: 'excluded')], + ), + ), + filePath: otherFile.path, + modificationStamp: 1, + contextRoot: resolvedOther.session.analysisContext.contextRoot, + resourceProvider: resourceProvider, + ); + resolvedOther.unit.accept(visitor); + + await assertAutoDiagnostics(''' +void mainMethod() ${expectLint(r'''{ + final x = 1; + if (x > 0) { + print(x); + } + print('done'); +}''')} +'''); + } + + Future test_reports_on_expression_function_bodies() async { + // 15+ tokens are needed. We repeat a pattern to ensure token count. + await assertAutoDiagnostics(''' +int first(int a, int b) ${expectLint(r'''=> a + b + a + b + + a + b + a + b + a + b + a + b;''')} + +int second(int x, int y) ${expectLint(r'''=> x + y + x + y + + x + y + x + y + x + y + x + y;''')} +'''); + } + + Future test_reports_despite_different_comments_and_formatting() async { + await assertAutoDiagnostics(''' +void first() ${expectLint(r'''{ + final x = 1; + // This is a comment + if (x > 0) { + print(x); + } + print('done'); +}''')} + +void second() ${expectLint(r'''{ + final y = 1; + + + if (y > 0) { + /* multiline + comment */ + print(y); + } + + print('done'); +}''')} +'''); + } + + Future test_ignores_nested_blocks_when_check_blocks_false() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + avoid_duplicate_code: + min_tokens: 15 + check_blocks: false +''', + ); + + // The nested blocks are identical (>15 tokens), but check_blocks is false, + // and the outer function bodies differ significantly. + await assertNoDiagnostics(r''' +void one() { + final x = 1; + if (x > 0) { + print('hello'); + print('world'); + print('done'); + } +} + +void two() { + print('completely different start'); + if (true) { + print('hello'); + print('world'); + print('done'); + } +} +'''); + } + + Future + test_does_not_report_when_identifiers_are_different_method_calls() async { + // Tests that ignore_identifiers=true still differentiates method calls. + // Local variables are ignored, but external method names are preserved. + await assertNoDiagnostics(''' +void doSomething(int x) {} +void doAnotherThing(int x) {} + +void first() { + final x = 1; + if (x > 0) { + doSomething(x); + } + print('done'); +} + +void second() { + final y = 1; + if (y > 0) { + doAnotherThing(y); + } + print('done'); +} +'''); + } +} diff --git a/test/src/lints/avoid_duplicate_code/global_hash_registry_test.dart b/test/src/lints/avoid_duplicate_code/global_hash_registry_test.dart new file mode 100644 index 00000000..93c8de1e --- /dev/null +++ b/test/src/lints/avoid_duplicate_code/global_hash_registry_test.dart @@ -0,0 +1,548 @@ +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:analyzer/file_system/memory_file_system.dart'; +import 'package:analyzer/file_system/physical_file_system.dart'; +import 'package:path/path.dart' as p; +import 'package:solid_lints/src/common/parameters/excluded_identifier_parameter.dart'; +import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/avoid_duplicate_code_parameters.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/file_cache_entry.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/models/hash_entry.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/services/global_hash_registry.dart'; +import 'package:solid_lints/src/lints/avoid_duplicate_code/services/hash_cache_storage.dart'; +import 'package:test/test.dart'; + +void main() { + group('GlobalHashRegistry', () { + late GlobalHashRegistry registry; + late MemoryResourceProvider memoryResourceProvider; + + setUp(() { + memoryResourceProvider = MemoryResourceProvider(); + registry = GlobalHashRegistry.instance + ..resourceProvider = memoryResourceProvider + ..clear() + ..enablePhysicalFileCleanup = false; + }); + + tearDown( + () => registry + ..clear() + ..resourceProvider = PhysicalResourceProvider.INSTANCE + ..enablePhysicalFileCleanup = true, + ); + + test('updateFile stores entries', () { + final entries = [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + const HashEntry(hash: 456, lineNumber: 20, tokenCount: 3), + ]; + + registry.updateFile('file_a.dart', entries, modificationStamp: 1); + + expect(registry.fileCount, equals(1)); + }); + + test('findCrossFileMatches finds duplicate in other files', () { + final fileAEntries = [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ]; + final fileBEntries = [ + const HashEntry(hash: 123, lineNumber: 15, tokenCount: 5), + ]; + + registry.updateFile('file_a.dart', fileAEntries, modificationStamp: 1); + + final matches = registry.findCrossFileMatches( + 'file_b.dart', + fileBEntries, + ); + + expect(matches, hasLength(1)); + expect(matches.first.duplicates, hasLength(1)); + final expectedPath = p.normalize( + p.join(io.Directory.current.path, 'file_a.dart'), + ); + expect(matches.first.duplicates.first.filePath, equals(expectedPath)); + expect(matches.first.duplicates.first.entry.hash, equals(123)); + expect(matches.first.duplicates.first.entry.lineNumber, equals(10)); + }); + + test('findCrossFileMatches ignores same file', () { + final entries = [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ]; + + registry.updateFile('file_a.dart', entries, modificationStamp: 1); + + final matches = registry.findCrossFileMatches('file_a.dart', entries); + + expect(matches, isEmpty); + }); + + test('updateFile replaces previous entries', () { + final oldEntries = [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ]; + final newEntries = [ + const HashEntry(hash: 456, lineNumber: 20, tokenCount: 3), + ]; + + registry.updateFile('file_a.dart', oldEntries, modificationStamp: 1); + registry.updateFile('file_a.dart', newEntries, modificationStamp: 2); + + expect(registry.fileCount, equals(1)); + + // File B tries to match against the old hash 123, should find nothing + final matches1 = registry.findCrossFileMatches('file_b.dart', [ + const HashEntry(hash: 123, lineNumber: 15, tokenCount: 5), + ]); + expect(matches1, isEmpty); + + // File B tries to match against the new hash 456, should match + final matches2 = registry.findCrossFileMatches('file_b.dart', [ + const HashEntry(hash: 456, lineNumber: 25, tokenCount: 3), + ]); + expect(matches2, hasLength(1)); + }); + + test('removeFile clears entries for specific file', () { + registry.updateFile('file_a.dart', [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ], modificationStamp: 1); + registry.updateFile('file_b.dart', [ + const HashEntry(hash: 456, lineNumber: 20, tokenCount: 5), + ], modificationStamp: 1); + + expect(registry.fileCount, equals(2)); + + registry.removeFile('file_a.dart'); + + expect(registry.fileCount, equals(1)); + + final matches = registry.findCrossFileMatches('file_c.dart', [ + const HashEntry(hash: 123, lineNumber: 30, tokenCount: 5), + ]); + expect(matches, isEmpty); + }); + + test('clear empties the registry', () { + registry.updateFile('file_a.dart', [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ], modificationStamp: 1); + expect(registry.fileCount, equals(1)); + + registry.clear(); + + expect(registry.fileCount, equals(0)); + }); + + test('findCrossFileMatches groups multiple duplicate locations', () { + registry.updateFile('file_a.dart', [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ], modificationStamp: 1); + registry.updateFile('file_b.dart', [ + const HashEntry(hash: 123, lineNumber: 20, tokenCount: 5), + ], modificationStamp: 1); + + final matches = registry.findCrossFileMatches('file_c.dart', [ + const HashEntry(hash: 123, lineNumber: 30, tokenCount: 5), + ]); + + expect(matches, hasLength(1)); + expect(matches.first.duplicates, hasLength(2)); + final expectedPathA = p.normalize( + p.join(io.Directory.current.path, 'file_a.dart'), + ); + final expectedPathB = p.normalize( + p.join(io.Directory.current.path, 'file_b.dart'), + ); + expect(matches.first.duplicates[0].filePath, equals(expectedPathA)); + expect(matches.first.duplicates[1].filePath, equals(expectedPathB)); + }); + + test('HashCacheStorage saves and loads index', () { + final absoluteFilePath = p.normalize( + p.join(io.Directory.current.path, 'file_a.dart'), + ); + final index = { + absoluteFilePath: const FileCacheEntry( + modificationStamp: 123456, + entries: [HashEntry(hash: 123, lineNumber: 10, tokenCount: 5)], + ), + }; + + final storage = HashCacheStorage( + packageRoot: io.Directory.current.path, + resourceProvider: memoryResourceProvider, + ); + + storage.save(index); + + final loaded = storage.load(); + expect(loaded, isNotNull); + expect(loaded!.length, equals(1)); + expect(loaded[absoluteFilePath]!.entries, hasLength(1)); + expect(loaded[absoluteFilePath]!.modificationStamp, equals(123456)); + + final entry = loaded[absoluteFilePath]!.entries.first; + expect(entry.hash, equals(123)); + expect(entry.lineNumber, equals(10)); + expect(entry.tokenCount, equals(5)); + + storage.delete(); + }); + + test('findCrossFileMatches cleans up absolute paths of deleted files', () { + registry.enablePhysicalFileCleanup = true; + final tempPath = p.normalize( + p.join(io.Directory.systemTemp.path, 'temp_test_file.dart'), + ); + memoryResourceProvider.newFile(tempPath, 'void main() {}'); + + registry.updateFile(tempPath, [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ], modificationStamp: 1); + expect(registry.fileCount, equals(1)); + + // Delete the file from the memory resource provider + memoryResourceProvider.deleteFile(tempPath); + + // Trigger matching, which should clean up the deleted tempPath + // from registry + final matches = registry.findCrossFileMatches('other_file.dart', [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ]); + + expect(matches, isEmpty); + expect(registry.fileCount, equals(0)); + }); + + test('findCrossFileMatches cleans up absolute paths of excluded files', () { + final absoluteExcludedPath = p.normalize( + '/workspace/project/lib/excluded.dart', + ); + + registry.updateFile(absoluteExcludedPath, [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ], modificationStamp: 1); + expect(registry.fileCount, equals(1)); + + // Trigger matching with a callback that considers absoluteExcludedPath + // as excluded + final matches = registry.findCrossFileMatches('other_file.dart', [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ], isFileExcluded: (path) => path == absoluteExcludedPath); + + expect(matches, isEmpty); + expect(registry.fileCount, equals(0)); + }); + + test('HashCacheStorage invalidates cache on config change', () { + final absoluteFilePath = p.normalize( + p.join(io.Directory.current.path, 'file.dart'), + ); + final index = { + absoluteFilePath: const FileCacheEntry( + modificationStamp: 123456, + entries: [HashEntry(hash: 123, lineNumber: 10, tokenCount: 5)], + ), + }; + + final params1 = AvoidDuplicateCodeParameters.empty(); + final params2 = AvoidDuplicateCodeParameters( + minTokens: 5, + ignoreLiterals: true, + ignoreIdentifiers: false, + checkBlocks: true, + exclude: params1.exclude, + ); + + final storage1 = HashCacheStorage( + packageRoot: io.Directory.current.path, + resourceProvider: memoryResourceProvider, + currentParams: params1, + ); + + // Save with params1 + storage1.save(index); + + // Loading with params1 should succeed + final loaded1 = storage1.load(); + expect(loaded1, isNotNull); + + // Loading with params2 (different config) should return null + // (invalidated) + final storage2 = HashCacheStorage( + packageRoot: io.Directory.current.path, + resourceProvider: memoryResourceProvider, + currentParams: params2, + ); + final loaded2 = storage2.load(); + expect(loaded2, isNull); + + storage1.delete(); + }); + + test('findCrossFileMatches processes multiple candidates correctly', () { + registry.updateFile('file_a.dart', [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ], modificationStamp: 1); + + final matches = registry.findCrossFileMatches('file_b.dart', [ + const HashEntry(hash: 123, lineNumber: 20, tokenCount: 5), // Match + const HashEntry(hash: 999, lineNumber: 30, tokenCount: 10), // No match + ]); + + expect(matches, hasLength(1)); + expect(matches.first.duplicates.first.entry.hash, equals(123)); + }); + + test('HashCacheStorage.load returns null when cache file is missing', () { + final storage = HashCacheStorage( + packageRoot: io.Directory.current.path, + resourceProvider: memoryResourceProvider, + ); + + // Ensure any existing cache is deleted + storage.delete(); + + final loaded = storage.load(); + + expect(loaded, isNull); + }); + + test('HashCacheStorage.load returns null and does not throw when cache ' + 'file is corrupted', () { + final storage = HashCacheStorage( + packageRoot: io.Directory.current.path, + resourceProvider: memoryResourceProvider, + ); + + final cachePath = p.normalize( + p.join( + io.Directory.current.path, + '.dart_tool', + 'solid_lints', + 'duplicate_index.json', + ), + ); + memoryResourceProvider.newFile( + cachePath, + '["invalid", "json", "structure", "not", "a", "map"]', + ); + + final loaded = storage.load(); + expect(loaded, isNull); + + storage.delete(); + }); + + test('AvoidDuplicateCodeParameters value equality', () { + final params1 = AvoidDuplicateCodeParameters( + minTokens: 30, + ignoreLiterals: false, + ignoreIdentifiers: true, + checkBlocks: true, + exclude: ExcludedIdentifiersListParameter( + exclude: [ + const ExcludedIdentifierParameter( + methodName: 'foo', + className: 'Bar', + ), + ], + ), + ); + + final params2 = AvoidDuplicateCodeParameters( + minTokens: 30, + ignoreLiterals: false, + ignoreIdentifiers: true, + checkBlocks: true, + exclude: ExcludedIdentifiersListParameter( + exclude: [ + const ExcludedIdentifierParameter( + methodName: 'foo', + className: 'Bar', + ), + ], + ), + ); + + final paramsDifferentExclude = AvoidDuplicateCodeParameters( + minTokens: 30, + ignoreLiterals: false, + ignoreIdentifiers: true, + checkBlocks: true, + exclude: ExcludedIdentifiersListParameter( + exclude: [const ExcludedIdentifierParameter(methodName: 'different')], + ), + ); + + expect(params1, equals(params2)); + expect(params1.hashCode, equals(params2.hashCode)); + + expect(params1, isNot(equals(paramsDifferentExclude))); + }); + + test( + 'does not match or clear files from sibling directories with prefixing names', + () { + final currentRoot = io.Directory.current.path; + final siblingRoot = '${currentRoot}_sibling'; + final siblingFilePath = p.normalize(p.join(siblingRoot, 'file.dart')); + final projectFilePath = p.normalize(p.join(currentRoot, 'file.dart')); + + registry.updateFile(projectFilePath, [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ], modificationStamp: 1); + + registry.updateFile(siblingFilePath, [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ], modificationStamp: 1); + + expect(registry.fileCount, equals(2)); + + // 1. findCrossFileMatches should not find duplicate in siblingFilePath + // if limited to currentRoot. + final matches = registry.findCrossFileMatches(projectFilePath, [ + const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5), + ], packageRoot: currentRoot); + expect(matches, isEmpty); + + // 2. clearEntriesForRoot should not clear siblingFilePath when + // clearing currentRoot. + final newParams = AvoidDuplicateCodeParameters( + minTokens: 40, + ignoreLiterals: false, + ignoreIdentifiers: false, + checkBlocks: true, + exclude: AvoidDuplicateCodeParameters.empty().exclude, + ); + + registry.updateFile( + projectFilePath, + [const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5)], + modificationStamp: 1, + parameters: newParams, + packageRoot: currentRoot, + ); + + expect( + registry.getFileEntries(siblingFilePath, packageRoot: siblingRoot), + isNotNull, + ); + }, + ); + + test( + 'debounces save operations independently for different package roots', + () async { + final tempDir1 = '/temp/package1'; + final tempDir2 = '/temp/package2'; + + final file1 = p.normalize(p.join(tempDir1, 'file.dart')); + final file2 = p.normalize(p.join(tempDir2, 'file.dart')); + + registry.updateFile( + file1, + [const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5)], + modificationStamp: 1, + packageRoot: tempDir1, + ); + + registry.updateFile( + file2, + [const HashEntry(hash: 456, lineNumber: 10, tokenCount: 5)], + modificationStamp: 1, + packageRoot: tempDir2, + ); + + // Wait for debounce duration (500ms + some buffer) + await Future.delayed(const Duration(milliseconds: 600)); + + // Both caches should be saved on disk + final loaded1 = HashCacheStorage( + packageRoot: tempDir1, + resourceProvider: memoryResourceProvider, + ).load(); + final loaded2 = HashCacheStorage( + packageRoot: tempDir2, + resourceProvider: memoryResourceProvider, + ).load(); + + expect(loaded1, isNotNull); + expect(loaded1!.keys.first, equals(file1)); + + expect(loaded2, isNotNull); + expect(loaded2!.keys.first, equals(file2)); + }, + ); + + test('uses correct package-specific parameters during debounced save ' + 'in multi-package workspace', () async { + final tempDir1 = '/temp/package1'; + final tempDir2 = '/temp/package2'; + + final file1 = p.normalize(p.join(tempDir1, 'file.dart')); + final file2 = p.normalize(p.join(tempDir2, 'file.dart')); + + final params1 = AvoidDuplicateCodeParameters( + minTokens: 30, + ignoreLiterals: false, + ignoreIdentifiers: false, + checkBlocks: true, + exclude: AvoidDuplicateCodeParameters.empty().exclude, + ); + + final params2 = AvoidDuplicateCodeParameters( + minTokens: 40, + ignoreLiterals: false, + ignoreIdentifiers: false, + checkBlocks: true, + exclude: AvoidDuplicateCodeParameters.empty().exclude, + ); + + registry.updateFile( + file1, + [const HashEntry(hash: 123, lineNumber: 10, tokenCount: 5)], + modificationStamp: 1, + parameters: params1, + packageRoot: tempDir1, + ); + + registry.updateFile( + file2, + [const HashEntry(hash: 456, lineNumber: 10, tokenCount: 5)], + modificationStamp: 1, + parameters: params2, + packageRoot: tempDir2, + ); + + // Wait for debounce duration (500ms + some buffer) + await Future.delayed(const Duration(milliseconds: 600)); + + final cachePath1 = p.normalize( + p.join(tempDir1, '.dart_tool/solid_lints/duplicate_index.json'), + ); + final cachePath2 = p.normalize( + p.join(tempDir2, '.dart_tool/solid_lints/duplicate_index.json'), + ); + + final cacheFile1 = memoryResourceProvider.getFile(cachePath1); + final cacheFile2 = memoryResourceProvider.getFile(cachePath2); + + expect(cacheFile1.exists, isTrue); + expect(cacheFile2.exists, isTrue); + + final content1 = + jsonDecode(cacheFile1.readAsStringSync()) as Map; + final content2 = + jsonDecode(cacheFile2.readAsStringSync()) as Map; + + expect(content1['config']?['min_tokens'], equals(30)); + expect(content2['config']?['min_tokens'], equals(40)); + }); + }); +} diff --git a/test/src/lints/avoid_duplicate_code/utils/jenkins_hasher_test.dart b/test/src/lints/avoid_duplicate_code/utils/jenkins_hasher_test.dart new file mode 100644 index 00000000..a2055031 --- /dev/null +++ b/test/src/lints/avoid_duplicate_code/utils/jenkins_hasher_test.dart @@ -0,0 +1,72 @@ +import 'package:solid_lints/src/lints/avoid_duplicate_code/utils/jenkins_hasher.dart'; +import 'package:test/test.dart'; + +void main() { + group('JenkinsHasher', () { + late JenkinsHasher hasher; + + setUp(() { + hasher = JenkinsHasher(); + }); + + test('initial hash is 0', () { + expect(hasher.hash, 0); + }); + + test('addString updates hash consistently', () { + hasher.addString('hello'); + final hash1 = hasher.hash; + + hasher.reset(); + hasher.addString('hello'); + final hash2 = hasher.hash; + + expect(hash1, hash2); + expect(hash1, isNot(0)); + }); + + test('different strings produce different hashes', () { + hasher.addString('hello'); + final hash1 = hasher.hash; + + hasher.reset(); + hasher.addString('world'); + final hash2 = hasher.hash; + + expect(hash1, isNot(hash2)); + }); + + test('addString is equivalent to adding characters sequentially', () { + hasher.addString('hello'); + final combinedHash = hasher.hash; + + hasher.reset(); + hasher.addString('he'); + hasher.addString('llo'); + final sequentialHash = hasher.hash; + + expect(combinedHash, sequentialHash); + }); + + test('add updates hash correctly', () { + hasher.add(1); + hasher.add(2); + final hash1 = hasher.hash; + + hasher.reset(); + hasher.add(1); + hasher.add(2); + final hash2 = hasher.hash; + + expect(hash1, hash2); + }); + + test('reset clears the state', () { + hasher.addString('some data'); + expect(hasher.hash, isNot(0)); + + hasher.reset(); + expect(hasher.hash, 0); + }); + }); +} From 34fef8ad853cfd16cd3505234ebe84fd3136c245 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 22 Jul 2026 18:03:03 +0300 Subject: [PATCH 48/49] Add feature_envy rule (#323) * feat: add feature_envy rule * refactor: simplify feature envy logic and improve code readability using pattern matching and collection methods * refactor: simplify metrics calculation and use multiSortedBy for external access evaluation * refactor: convert FeatureEnvyParameters.fromJson factory to a generative constructor * refactor: simplify public member extraction using extension methods and set operations * refactor: simplify instance member checks, element traversal, and member element resolution logic * refactor: simplify test code in IsDataClassTest by introducing a shared _test helper method * refactor: introduce _expectMetrics helper to simplify feature envy metrics unit tests * refactor: reorder constructor parameters and convert empty factory to generative constructor for FeatureEnvyParameters --------- Co-authored-by: Illia Aihistov --- README.md | 28 +- lib/analysis_options.yaml | 5 + lib/main.dart | 2 + .../lints/feature_envy/feature_envy_rule.dart | 89 ++ .../models/feature_envy_metrics.dart | 52 ++ .../models/feature_envy_parameters.dart | 51 ++ .../models/project_class_cache.dart | 13 + .../utils/member_access_utils.dart | 106 +++ .../visitors/feature_envy_visitor.dart | 58 ++ .../visitors/member_access_visitor.dart | 183 ++++ lib/src/utils/node_utils.dart | 159 ++++ lib/src/utils/types_utils.dart | 57 ++ test/src/common/utils/is_data_class_test.dart | 66 ++ .../feature_envy/feature_envy_rule_test.dart | 798 ++++++++++++++++++ .../models/feature_envy_metrics_test.dart | 94 +++ 15 files changed, 1760 insertions(+), 1 deletion(-) create mode 100644 lib/src/lints/feature_envy/feature_envy_rule.dart create mode 100644 lib/src/lints/feature_envy/models/feature_envy_metrics.dart create mode 100644 lib/src/lints/feature_envy/models/feature_envy_parameters.dart create mode 100644 lib/src/lints/feature_envy/models/project_class_cache.dart create mode 100644 lib/src/lints/feature_envy/utils/member_access_utils.dart create mode 100644 lib/src/lints/feature_envy/visitors/feature_envy_visitor.dart create mode 100644 lib/src/lints/feature_envy/visitors/member_access_visitor.dart create mode 100644 test/src/common/utils/is_data_class_test.dart create mode 100644 test/src/lints/feature_envy/feature_envy_rule_test.dart create mode 100644 test/src/lints/feature_envy/models/feature_envy_metrics_test.dart diff --git a/README.md b/README.md index 1c5cd07b..0448e374 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ dev_dependencies: Enable the plugin and include `solid_lints` in your project's top-level `analysis_options.yaml`: +### Option 1: Using the `plugins` block + ```yaml include: package:solid_lints/analysis_options.yaml @@ -28,6 +30,14 @@ plugins: solid_lints: ``` +### Option 2: Using the top-level `solid_lints` block + +```yaml +include: package:solid_lints/analysis_options.yaml + +solid_lints: +``` + Also, you can use a specialized rule set designed for Dart tests. Add an `analysis_options.yaml` file under the `test/` directory, and include the ruleset: @@ -43,9 +53,25 @@ dart analyze; # Configuration -You can customize individual rule settings in your `analysis_options.yaml` under the `solid_lints` configuration block: +You can customize individual rule settings in your `analysis_options.yaml`. + +### Option 1: Inside the `plugins` block (Recommended) ```yaml +plugins: + solid_lints: + diagnostics: + cyclomatic_complexity: + max_complexity: 10 + avoid_non_null_assertion: true +``` + +### Option 2: Separate top-level `solid_lints` block + +```yaml +plugins: + solid_lints: + solid_lints: diagnostics: cyclomatic_complexity: diff --git a/lib/analysis_options.yaml b/lib/analysis_options.yaml index 77071fad..fe91dccb 100644 --- a/lib/analysis_options.yaml +++ b/lib/analysis_options.yaml @@ -52,6 +52,7 @@ solid_lints: - IMap - BuiltMap avoid_returning_widgets: true + avoid_similar_names: true avoid_unnecessary_return_variable: true avoid_unnecessary_setstate: true avoid_unnecessary_type_assertions: true @@ -67,6 +68,10 @@ solid_lints: max_complexity: 10 double_literal_format: true + feature_envy: + atfd_threshold: 4 + laa_threshold: 0.33 + fdp_threshold: 2 function_lines_of_code: max_lines: 200 diff --git a/lib/main.dart b/lib/main.dart index ce9a5bfa..1274efa2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,6 +19,7 @@ import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_param import 'package:solid_lints/src/lints/avoid_using_api/avoid_using_api_rule.dart'; import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; +import 'package:solid_lints/src/lints/feature_envy/feature_envy_rule.dart'; import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart'; import 'package:solid_lints/src/lints/member_ordering/member_ordering_rule.dart'; import 'package:solid_lints/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart'; @@ -74,6 +75,7 @@ class SolidLintsPlugin extends Plugin { AvoidUsingApiRule(analysisOptionsLoader: analysisLoader), CyclomaticComplexityRule(analysisOptionsLoader: analysisLoader), DoubleLiteralFormatRule(), + FeatureEnvyRule(analysisOptionsLoader: analysisLoader), FunctionLinesOfCodeRule(analysisOptionsLoader: analysisLoader), MemberOrderingRule(analysisOptionsLoader: analysisLoader), NamedParametersOrderingRule(analysisOptionsLoader: analysisLoader), diff --git a/lib/src/lints/feature_envy/feature_envy_rule.dart b/lib/src/lints/feature_envy/feature_envy_rule.dart new file mode 100644 index 00000000..c2f9e59e --- /dev/null +++ b/lib/src/lints/feature_envy/feature_envy_rule.dart @@ -0,0 +1,89 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/feature_envy/models/feature_envy_parameters.dart'; +import 'package:solid_lints/src/lints/feature_envy/visitors/feature_envy_visitor.dart'; +import 'package:solid_lints/src/models/solid_lint_rule.dart'; + +/// Warns if a method accesses fields or methods from a different class +/// more often than from its own class (feature envy). +/// +/// ### Example +/// BAD: +/// ```dart +/// class A { +/// int field; +/// A(this.field); +/// } +/// +/// class B { +/// int method(A a) => a.field * a.field; // LINT +/// } +/// ``` +/// +/// GOOD: +/// ```dart +/// class A { +/// int field; +/// A(this.field); +/// +/// int method() => field * field; +/// } +/// +/// class B { +/// int method(A a) => a.method(); +/// } +/// ``` +/// +/// ### Detection Algorithm +/// The rule detects feature envy using three metrics: +/// - **ATFD** (Access to Foreign Data): Accesses to members of a single +/// external class. Triggers if ATFD >= threshold (default 4). +/// - **LAA** (Locality of Attribute Access): The ratio of internal accesses +/// to total accesses. Triggers if LAA < threshold (default 0.33). +/// - **FDP** (Foreign Data Providers): The number of unique external classes +/// accessed. Triggers if FDP <= threshold (default 2). +/// +/// Accesses to other instances of the same class, non-project classes, +/// closures, nested functions, and data classes are ignored. +class FeatureEnvyRule extends SolidLintRule { + /// Name of the lint. + static const lintName = 'feature_envy'; + + static const _code = LintCode( + lintName, + "Avoid accessing members of `{0}` in `{1}` more often than own members.", + correctionMessage: + "Consider moving the related logic into the `{0}` class.", + ); + + @override + DiagnosticCode get diagnosticCode => _code; + + /// Creates a new instance of [FeatureEnvyRule]. + FeatureEnvyRule({ + required super.analysisOptionsLoader, + }) : super.withParameters( + name: lintName, + description: + 'Warns if a method accesses members of another class more ' + 'often than its own (feature envy).', + parametersParser: FeatureEnvyParameters.fromJson, + ); + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + super.registerNodeProcessors(registry, context); + + final parameters = + getParametersForContext(context) ?? FeatureEnvyParameters.empty(); + + registry.addMethodDeclaration( + this, + FeatureEnvyVisitor(this, parameters), + ); + } +} diff --git a/lib/src/lints/feature_envy/models/feature_envy_metrics.dart b/lib/src/lints/feature_envy/models/feature_envy_metrics.dart new file mode 100644 index 00000000..8a4ea44e --- /dev/null +++ b/lib/src/lints/feature_envy/models/feature_envy_metrics.dart @@ -0,0 +1,52 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:collection/collection.dart'; +import 'package:solid_lints/src/lints/feature_envy/models/feature_envy_parameters.dart'; +import 'package:solid_lints/src/utils/iterable_utils.dart'; + +/// Calculated Feature Envy metrics for a method. +class FeatureEnvyMetrics { + /// Locality of Attribute Access (LAA) metric. + final double laa; + + /// Foreign Data Providers (FDP) metric. + final int fdp; + + /// Access to Foreign Data (ATFD) metric. + final int atfd; + + /// The class element that is accessed the most externally. + final InterfaceElement? maxEnvyElement; + + const FeatureEnvyMetrics._({ + required this.laa, + required this.fdp, + required this.atfd, + required this.maxEnvyElement, + }); + + /// Checks if these metrics exceed the thresholds defined in [parameters], + /// indicating a feature envy code smell. + bool exceedsThresholds(FeatureEnvyParameters parameters) => + atfd >= parameters.atfdThreshold && + laa < parameters.laaThreshold && + fdp <= parameters.fdpThreshold; + + /// Calculates metrics based on collected accesses. + factory FeatureEnvyMetrics.calculate({ + required int internalAccesses, + required Map externalAccessCounts, + }) { + final totalAccesses = internalAccesses + externalAccessCounts.values.sum; + final maxEntry = externalAccessCounts.entries.multiSortedBy([ + (e) => -e.value, + (e) => e.key.name ?? '', + ]).firstOrNull; + + return FeatureEnvyMetrics._( + laa: totalAccesses == 0 ? 1.0 : internalAccesses / totalAccesses, + fdp: externalAccessCounts.length, + atfd: maxEntry?.value ?? 0, + maxEnvyElement: maxEntry?.key, + ); + } +} diff --git a/lib/src/lints/feature_envy/models/feature_envy_parameters.dart b/lib/src/lints/feature_envy/models/feature_envy_parameters.dart new file mode 100644 index 00000000..01686bba --- /dev/null +++ b/lib/src/lints/feature_envy/models/feature_envy_parameters.dart @@ -0,0 +1,51 @@ +import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart'; + +/// Configuration parameters for the feature_envy rule. +class FeatureEnvyParameters { + /// A list of methods that should be excluded from the lint. + final ExcludedIdentifiersListParameter exclude; + + /// Access to Foreign Data (ATFD) threshold. + /// Triggered if ATFD >= atfdThreshold. + final int atfdThreshold; + + /// Locality of Attribute Access (LAA) threshold. + /// Triggered if LAA < laaThreshold. + final double laaThreshold; + + /// Foreign Data Providers (FDP) threshold. + /// Triggered only if FDP <= fdpThreshold. + final int fdpThreshold; + + /// Default Access to Foreign Data (ATFD) threshold. + static const _defaultAtfdThreshold = 4; + + /// Default Locality of Attribute Access (LAA) threshold. + static const _defaultLaaThreshold = 0.33; + + /// Default Foreign Data Providers (FDP) threshold. + static const _defaultFdpThreshold = 2; + + /// Constructor for [FeatureEnvyParameters] model. + const FeatureEnvyParameters({ + required this.exclude, + required this.atfdThreshold, + required this.laaThreshold, + required this.fdpThreshold, + }); + + /// Empty [FeatureEnvyParameters] model with default values. + FeatureEnvyParameters.empty() + : exclude = ExcludedIdentifiersListParameter(exclude: []), + atfdThreshold = _defaultAtfdThreshold, + laaThreshold = _defaultLaaThreshold, + fdpThreshold = _defaultFdpThreshold; + + /// Creates a [FeatureEnvyParameters] model from JSON data. + FeatureEnvyParameters.fromJson(Map json) + : exclude = ExcludedIdentifiersListParameter.defaultFromJson(json), + atfdThreshold = json['atfd_threshold'] as int? ?? _defaultAtfdThreshold, + laaThreshold = + (json['laa_threshold'] as num?)?.toDouble() ?? _defaultLaaThreshold, + fdpThreshold = json['fdp_threshold'] as int? ?? _defaultFdpThreshold; +} diff --git a/lib/src/lints/feature_envy/models/project_class_cache.dart b/lib/src/lints/feature_envy/models/project_class_cache.dart new file mode 100644 index 00000000..3d5a4683 --- /dev/null +++ b/lib/src/lints/feature_envy/models/project_class_cache.dart @@ -0,0 +1,13 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:solid_lints/src/utils/node_utils.dart'; + +/// A cache for checking if an [InterfaceElement] belongs to the analyzed +/// project. +class ProjectClassCache { + final _cache = {}; + + /// Returns `true` if [element] belongs to the analyzed project. + /// Results are cached for better performance during static analysis. + bool isProjectClass(InterfaceElement element) => + _cache.putIfAbsent(element, () => element.isFromProject); +} diff --git a/lib/src/lints/feature_envy/utils/member_access_utils.dart b/lib/src/lints/feature_envy/utils/member_access_utils.dart new file mode 100644 index 00000000..707a7153 --- /dev/null +++ b/lib/src/lints/feature_envy/utils/member_access_utils.dart @@ -0,0 +1,106 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:solid_lints/src/lints/feature_envy/models/project_class_cache.dart'; +import 'package:solid_lints/src/utils/node_utils.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; + +/// Utility methods for analyzing member accesses within the feature envy lint. +abstract final class MemberAccessUtils { + /// Resolves the base element of a member access target expression. + static Element? resolveTargetElement( + Expression? target, { + required bool isPatternField, + }) { + if (isPatternField || target == null) return null; + + final baseElement = switch (target.unwrapTarget) { + ExtensionOverride(:final argumentList) => + argumentList.arguments.firstOrNull?.staticType?.element, + final expr => expr?.staticType?.element, + }; + + return switch (baseElement?.resolveTypeParameter) { + ExtensionElement(:final extendedType) => + extendedType.element?.resolveTypeParameter, + final resolved => resolved, + }; + } + + /// Checks if the access to [c] is considered internal to [currentClass]. + static bool isInternalAccess( + Expression? target, + InterfaceElement c, { + required InterfaceElement currentClass, + bool isPatternField = false, + }) => + currentClass.isSameOrSubclassOf(c) && + (target != null || !isPatternField) && + switch (target?.unwrapTarget) { + ExtensionOverride(:final argumentList) => + argumentList.arguments.firstOrNull?.unwrapTarget.isThisOrSuper ?? + false, + final expr => expr.isThisOrSuperOrNull, + }; + + /// Finds the highest level access expression that starts with [node]. + static Expression getAccessExpression(SimpleIdentifier node) => + findAccessExpression(node, node.parent); + + /// Recursively finds the top-most expression of an access. + static Expression findAccessExpression(Expression child, AstNode? parent) => + switch (parent) { + ParenthesizedExpression(:final expression) when expression == child => + findAccessExpression(parent, parent.parent), + PropertyAccess(:final propertyName, :final target) + when propertyName == child && target.unwrapTarget.isThisOrSuper => + findAccessExpression(parent, parent.parent), + _ => child, + }; + + /// Determines if [node] is the target of an access to an external member. + static bool isTargetOfExternalAccess( + SimpleIdentifier node, { + required InterfaceElement currentClass, + required ProjectClassCache projectClassCache, + }) { + final enclosing = node.element?.enclosingInterface; + if (enclosing == null || !currentClass.isSameOrSubclassOf(enclosing)) { + return false; + } + + final expr = getAccessExpression(node); + + bool isExternal(Element? e) => isExternalMember( + e, + currentClass: currentClass, + projectClassCache: projectClassCache, + ); + + return switch (expr.parent) { + MethodInvocation(:final target, :final methodName) when target == expr => + isExternal(methodName.element), + PrefixedIdentifier(:final prefix, :final identifier) + when prefix == expr => + isExternal(identifier.element), + PropertyAccess(:final target, :final propertyName) when target == expr => + isExternal(propertyName.element), + CascadeExpression(:final target, :final cascadeSections) + when target == expr => + cascadeSections.any((s) => isExternal(s.memberElement)), + _ => false, + }; + } + + /// Determines if [element] represents a member of an external class. + static bool isExternalMember( + Element? element, { + required InterfaceElement currentClass, + required ProjectClassCache projectClassCache, + }) => switch (element?.enclosingInterface) { + final enclosing? => + !currentClass.isSameOrSubclassOf(enclosing) && + !enclosing.isDataClass && + projectClassCache.isProjectClass(enclosing), + _ => false, + }; +} diff --git a/lib/src/lints/feature_envy/visitors/feature_envy_visitor.dart b/lib/src/lints/feature_envy/visitors/feature_envy_visitor.dart new file mode 100644 index 00000000..6e836a97 --- /dev/null +++ b/lib/src/lints/feature_envy/visitors/feature_envy_visitor.dart @@ -0,0 +1,58 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:solid_lints/src/lints/feature_envy/feature_envy_rule.dart'; +import 'package:solid_lints/src/lints/feature_envy/models/feature_envy_metrics.dart'; +import 'package:solid_lints/src/lints/feature_envy/models/feature_envy_parameters.dart'; +import 'package:solid_lints/src/lints/feature_envy/models/project_class_cache.dart'; +import 'package:solid_lints/src/lints/feature_envy/visitors/member_access_visitor.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; + +/// The AST visitor that checks every method for feature envy. +class FeatureEnvyVisitor extends SimpleAstVisitor { + final FeatureEnvyRule _rule; + final FeatureEnvyParameters _parameters; + final _projectClassCache = ProjectClassCache(); + + /// Creates a new instance of [FeatureEnvyVisitor]. + FeatureEnvyVisitor(this._rule, this._parameters); + + @override + void visitMethodDeclaration(MethodDeclaration node) { + if (node.isStatic || + node.body is EmptyFunctionBody || + _parameters.exclude.shouldIgnore(node)) { + return; + } + + final interfaceElement = node.declaredFragment?.element.enclosingElement; + if (interfaceElement is! InterfaceElement || + isWidgetOrSubclass(interfaceElement.thisType) || + isWidgetStateOrSubclass(interfaceElement.thisType)) { + return; + } + + final accessVisitor = MemberAccessVisitor( + interfaceElement, + _projectClassCache, + ); + node.accept(accessVisitor); + + if (accessVisitor.externalAccessCounts.isEmpty) return; + + final metrics = FeatureEnvyMetrics.calculate( + internalAccesses: accessVisitor.internalAccesses, + externalAccessCounts: accessVisitor.externalAccessCounts, + ); + + if (!metrics.exceedsThresholds(_parameters)) return; + + _rule.reportAtToken( + node.name, + arguments: [ + metrics.maxEnvyElement?.name ?? '', + interfaceElement.name ?? '', + ], + ); + } +} diff --git a/lib/src/lints/feature_envy/visitors/member_access_visitor.dart b/lib/src/lints/feature_envy/visitors/member_access_visitor.dart new file mode 100644 index 00000000..dcba78a1 --- /dev/null +++ b/lib/src/lints/feature_envy/visitors/member_access_visitor.dart @@ -0,0 +1,183 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:solid_lints/src/lints/feature_envy/models/project_class_cache.dart'; +import 'package:solid_lints/src/lints/feature_envy/utils/member_access_utils.dart'; +import 'package:solid_lints/src/utils/node_utils.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; + +/// A visitor that collects internal and external member accesses for a class. +class MemberAccessVisitor extends RecursiveAstVisitor { + final InterfaceElement _currentClass; + final ProjectClassCache _projectClassCache; + + /// The number of accesses to members of the current class. + int internalAccesses = 0; + + /// The counts of accesses to members of external classes. + final externalAccessCounts = {}; + + /// Creates a new instance of [MemberAccessVisitor]. + MemberAccessVisitor(this._currentClass, this._projectClassCache); + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + super.visitSimpleIdentifier(node); + + if (node.element case final element? + when element is! SetterElement && element.isInstanceMember) { + if (MemberAccessUtils.isTargetOfExternalAccess( + node, + currentClass: _currentClass, + projectClassCache: _projectClassCache, + )) { + return; + } + + _processElement( + element: element, + target: node.memberAccessTarget, + ); + } + } + + @override + void visitPatternField(PatternField node) { + super.visitPatternField(node); + + if (node.element case final element? when element.isInstanceMember) { + _processElement( + element: element, + target: node.parent?.matchedPatternExpression, + isPatternField: true, + ); + } + } + + @override + void visitBinaryExpression(BinaryExpression node) { + super.visitBinaryExpression(node); + + if (node.element case final element? + when element.isInstanceMethod && !node.isNullCheck) { + _processElement( + element: element, + target: node.leftOperand, + ); + } + } + + @override + void visitIndexExpression(IndexExpression node) { + super.visitIndexExpression(node); + + if (node.element case final element? when element.isInstanceMethod) { + _processElement( + element: element, + target: node.realTarget, + ); + } + } + + void _processOperatorAndWrite( + Element? opElement, + Element? writeElement, + Expression operand, + ) { + if (opElement case final e? when e.isInstanceMethod) { + _processElement(element: e, target: operand); + } + if (writeElement case final e? when e.isInstanceMember) { + _processElement(element: e, target: operand.targetExpression); + } + } + + @override + void visitPrefixExpression(PrefixExpression node) { + super.visitPrefixExpression(node); + _processOperatorAndWrite(node.element, node.writeElement, node.operand); + } + + @override + void visitPostfixExpression(PostfixExpression node) { + super.visitPostfixExpression(node); + _processOperatorAndWrite(node.element, node.writeElement, node.operand); + } + + @override + void visitAssignmentExpression(AssignmentExpression node) { + super.visitAssignmentExpression(node); + + _processOperatorAndWrite( + node.element, + node.writeElement, + node.leftHandSide, + ); + } + + @override + void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { + super.visitFunctionExpressionInvocation(node); + + if (node.function.staticType case InterfaceType(:final element)) { + if (element.getMethod('call') case final callMethod? + when callMethod.isInstanceMethod) { + _processElement( + element: callMethod, + target: node.function, + ); + } + } + } + + @override + void visitFunctionExpression(FunctionExpression node) { + // Stop traversal to ignore accesses inside closures. + } + + @override + void visitFunctionDeclaration(FunctionDeclaration node) { + // Stop traversal to ignore accesses inside nested functions. + } + + void _processElement({ + required Element element, + required Expression? target, + bool isPatternField = false, + }) { + if (target == null && element.enclosingElement is ExtensionElement) { + internalAccesses++; + return; + } + + final targetElement = MemberAccessUtils.resolveTargetElement( + target, + isPatternField: isPatternField, + ); + + final enclosingInterface = (targetElement ?? element).enclosingInterface; + if (enclosingInterface == null) return; + + if (MemberAccessUtils.isInternalAccess( + target, + enclosingInterface, + currentClass: _currentClass, + isPatternField: isPatternField, + )) { + internalAccesses++; + return; + } + + if (enclosingInterface == _currentClass) return; + + if (_projectClassCache.isProjectClass(enclosingInterface) && + !enclosingInterface.isDataClass) { + externalAccessCounts.update( + enclosingInterface, + (count) => count + 1, + ifAbsent: () => 1, + ); + } + } +} diff --git a/lib/src/utils/node_utils.dart b/lib/src/utils/node_utils.dart index 9b526184..15562947 100644 --- a/lib/src/utils/node_utils.dart +++ b/lib/src/utils/node_utils.dart @@ -63,6 +63,22 @@ extension SimpleIdentifierExtension on SimpleIdentifier { /// Returns the library URI string of the element, or null. String? get sourceUrl => element?.libraryUri; + + /// Returns the target expression if this identifier is part of a member + /// access (e.g., `target.member`, `target.method()`, `target.field`), + /// otherwise null. + Expression? get memberAccessTarget => switch (parent) { + PropertyAccess(:final propertyName, :final realTarget) + when propertyName == this => + realTarget, + MethodInvocation(:final methodName, :final realTarget) + when methodName == this => + realTarget, + PrefixedIdentifier(:final identifier, :final prefix) + when identifier == this => + prefix, + _ => null, + }; } /// Extension on [AstNode] to provide generic context/traversal checks. @@ -107,6 +123,32 @@ extension AstNodeExtension on AstNode { final p = parent is PrefixExpression ? parent?.parent : parent; return p is IndexExpression; } + + /// Traverses up the AST from a pattern node to find the root expression + /// being matched. + Expression? get matchedPatternExpression { + AstNode? current = this; + while (current != null) { + switch (current) { + case PatternField() || + ListPattern() || + MapPattern() || + RecordPattern() || + ForEachPartsWithPattern(): + return null; + case PatternVariableDeclaration(:final expression) || + PatternAssignment(:final expression) || + SwitchStatement(:final expression) || + SwitchExpression(:final expression) || + IfStatement(:final expression) || + IfElement(:final expression): + return expression; + default: + current = current.parent; + } + } + return null; + } } /// Extension on [NamedType] to provide source URL utility. @@ -145,6 +187,52 @@ extension ElementExtension on Element { return false; } + + /// Checks whether this element is a non-static instance member. + bool get isInstanceMember => switch (this) { + PropertyAccessorElement(:final isStatic) || + MethodElement(:final isStatic) || + FieldElement(:final isStatic) => !isStatic, + _ => false, + }; + + /// Checks whether this element is a non-static instance method. + bool get isInstanceMethod => this is MethodElement && isInstanceMember; + + /// Checks whether this element belongs to the analyzed project. + bool get isFromProject { + final session = this.session; + if (session == null) return false; + + final sourcePath = library?.firstFragment.source.fullName; + if (sourcePath == null) return false; + + return session.analysisContext.contextRoot.isAnalyzed(sourcePath); + } + + /// Returns the enclosing [InterfaceElement] of this element (recursively), + /// or null if none. + InterfaceElement? get enclosingInterface => + enclosingElements.whereType().firstOrNull; + + /// Returns an iterable of this element and all its enclosing elements. + Iterable get enclosingElements sync* { + for (Element? e = this; e != null; e = e.enclosingElement) { + yield e; + } + } + + /// Resolves type parameters to their bounds recursively to prevent cycle + /// loops. + Element? get resolveTypeParameter { + Element? current = this; + final visited = {}; + while (current is TypeParameterElement) { + if (!visited.add(current)) break; + current = current.bound?.element; + } + return current; + } } /// Extension on [VariableDeclaration] to check declared type. @@ -155,3 +243,74 @@ extension VariableDeclarationExtension on VariableDeclaration { return element is VariableElement ? element.type : null; } } + +/// Extension on [BinaryExpression] to check binary expressions. +extension BinaryExpressionExtension on BinaryExpression { + /// Returns `true` if this is an equality expression (== or !=). + bool get isEquality => + operator.type == TokenType.EQ_EQ || operator.type == TokenType.BANG_EQ; + + /// Returns `true` if one of the operands is a null literal. + bool get hasNullOperand => + leftOperand is NullLiteral || rightOperand is NullLiteral; + + /// Returns `true` if this expression is a null check. + bool get isNullCheck => isEquality && hasNullOperand; +} + +/// Extension on [Expression] to provide target unwrapping and resolving +/// utilities. +extension ExpressionExtension on Expression { + /// Returns the target of a property access, prefixed identifier, or index + /// expression, or null for other expressions. + Expression? get targetExpression => switch (this) { + PropertyAccess(:final realTarget) => realTarget, + PrefixedIdentifier(:final prefix) => prefix, + IndexExpression(:final realTarget) => realTarget, + _ => null, + }; + + /// Returns the member element referenced or operated on by this expression, + /// or null if none. + Element? get memberElement => switch (this) { + MethodInvocation(:final methodName) => methodName.element, + PropertyAccess(:final propertyName) => propertyName.element, + AssignmentExpression(:final writeElement, :final readElement) || + PostfixExpression(:final writeElement, :final readElement) || + PrefixExpression( + :final writeElement, + :final readElement, + ) => writeElement ?? readElement, + IndexExpression(:final element) => element, + BinaryExpression(:final element) => element, + _ => null, + }; +} + +/// Extension on nullable [Expression] to provide helper checks. +extension ExpressionNullableExtension on Expression? { + /// Unwraps casts ([AsExpression]) and null assertions (e.g. `expr!`) + /// recursively down to the core expression. + Expression? get unwrapTarget { + var current = this?.unParenthesized; + while (true) { + if (current case AsExpression(:final expression)) { + current = expression.unParenthesized; + } else if (current case PostfixExpression( + :final operand, + :final operator, + ) when operator.type == TokenType.BANG) { + current = operand.unParenthesized; + } else { + return current; + } + } + } + + /// Returns `true` if this expression is `this` or `super` or null. + bool get isThisOrSuperOrNull => + this == null || this is ThisExpression || this is SuperExpression; + + /// Returns `true` if this expression is `this` or `super`. + bool get isThisOrSuper => this is ThisExpression || this is SuperExpression; +} diff --git a/lib/src/utils/types_utils.dart b/lib/src/utils/types_utils.dart index d3ebac5e..184387be 100644 --- a/lib/src/utils/types_utils.dart +++ b/lib/src/utils/types_utils.dart @@ -87,6 +87,63 @@ extension TypeString on String { String replaceGenericString() => replaceFirst(_genericRegex, ''); } +extension _PropertyAccessorElementExt on PropertyAccessorElement { + bool get isPublicInstanceOrigin => + isPublic && !isStatic && isOriginDeclaration; +} + +extension _PropertyAccessorsExt on Iterable { + Iterable get publicInstanceMemberNames => + where((e) => e.isPublicInstanceOrigin).map((e) => e.displayName); +} + +extension InterfaceElementExt on InterfaceElement { + /// Checks whether this element is the same as or a subclass of [superType]. + bool isSameOrSubclassOf(InterfaceElement superType) => + this == superType || + allSupertypes.any((type) => type.element == superType); + + /// Checks if a class is a Data Class based on Weight of Class. + bool get isDataClass { + const dataClassWeightThreshold = 0.33; + + final elements = [ + this, + ...allSupertypes.where((s) => !s.isDartCoreObject).map((s) => s.element), + ]; + + final publicProperties = { + for (final el in elements) ...[ + ...el.getters.publicInstanceMemberNames.toSet().difference({ + 'hashCode', + 'runtimeType', + }), + ...el.setters.publicInstanceMemberNames, + ], + }; + + final publicMethods = elements + .expand((e) => e.methods) + .where((m) => m.isPublic && !m.isStatic) + .map((m) => m.name) + .nonNulls + .toSet() + .difference({ + 'toString', + '==', + 'copyWith', + 'toJson', + 'noSuchMethod', + }); + + final totalMembers = publicProperties.length + publicMethods.length; + if (totalMembers == 0) return true; + + final weightOfClass = publicMethods.length / totalMembers; + return weightOfClass < dataClassWeightThreshold; + } +} + bool hasWidgetType(DartType type) => (isWidgetOrSubclass(type) || _isIterable(type) || diff --git a/test/src/common/utils/is_data_class_test.dart b/test/src/common/utils/is_data_class_test.dart new file mode 100644 index 00000000..4d853ae7 --- /dev/null +++ b/test/src/common/utils/is_data_class_test.dart @@ -0,0 +1,66 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart'; +import 'package:solid_lints/src/utils/types_utils.dart'; +import 'package:test/test.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(IsDataClassTest); + }); +} + +@reflectiveTest +class IsDataClassTest extends PubPackageResolutionTest { + Future test_isDataClass_for_pure_data_class() async => _test(''' +class Rectangle { + final int width; + final int height; + const Rectangle(this.width, this.height); +} +''', isTrue); + + Future test_isDataClass_for_non_data_class() async => _test(''' +class Rectangle { + final int width; + final int height; + const Rectangle(this.width, this.height); + int area() => width * height; + void printMe() {} +} +''', isFalse); + + Future test_isDataClass_collision_case() async => _test( + ''' +class Base { + int get foo => 42; +} + +class Sub extends Base { + @override + int foo() => 42; +} +''', + isFalse, + (d) => d.namePart.typeName.lexeme == 'Sub', + ); + + Future _test( + String source, + dynamic matcher, [ + bool Function(ClassDeclaration)? test, + ]) async { + newFile(testFile.path, source); + final file = await resolveFile(testFile.path); + + expect( + file.unit.declarations + .whereType() + .firstWhere(test ?? (_) => true) + .declaredFragment + ?.element + .isDataClass, + matcher, + ); + } +} diff --git a/test/src/lints/feature_envy/feature_envy_rule_test.dart b/test/src/lints/feature_envy/feature_envy_rule_test.dart new file mode 100644 index 00000000..e46f9e42 --- /dev/null +++ b/test/src/lints/feature_envy/feature_envy_rule_test.dart @@ -0,0 +1,798 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/feature_envy/feature_envy_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../utils/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(FeatureEnvyRuleTest); + }); +} + +@reflectiveTest +class FeatureEnvyRuleTest extends AnalysisRuleTest with AutoTestLintOffsets { + static const _importFlutterWidgets = "import 'package:flutter/widgets.dart';"; + static const _mockFlutterWidgetsContent = ''' +abstract class Widget { + const Widget(); +} + +class StatelessWidget implements Widget { + const StatelessWidget(); +} + +class StatefulWidget implements Widget { + const StatefulWidget(); +} + +abstract class State { + T get widget => throw 'unimplemented'; +} +'''; + + static const _mockAnalysisOptionsContent = ''' +plugins: + solid_lints: + diagnostics: + feature_envy: + atfd_threshold: 2 + fdp_threshold: 5 + '''; + + @override + void setUp() { + rule = FeatureEnvyRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + newPackage('flutter') + ..addFile('lib/widgets.dart', _mockFlutterWidgetsContent); + super.setUp(); + + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +$_mockAnalysisOptionsContent''', + ); + } + + Future test_reports_when_atfd_exceeds_threshold() => + assertAutoDiagnostics(''' +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); + + int get area => width * height; + int getArea() => width * height; + void dummy() {} +} + +class MathHelper { + int ${expectLint('sumOfAreas')}(Rectangle r1, Rectangle r2) => (r1.width * r1.height) + (r2.width * r2.height); +} +'''); + + Future test_does_not_report_when_atfd_below_threshold() => + assertNoDiagnostics(r''' +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); + int get area => width * height; +} + +class MathHelper { + int getWidth(Rectangle r1) => r1.width; +} +'''); + + Future test_reports_when_atfd_meets_threshold() => + assertAutoDiagnostics(''' +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); + int get area => width * height; + int getArea() => width * height; + void dummy() {} +} + +class MathHelper { + int ${expectLint('sumOfAreas')}(Rectangle r1, Rectangle r2) => r1.area + r2.area; +} +'''); + + Future test_reports_when_external_exceeds_internal() => + assertAutoDiagnostics(''' +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); + int get area => width * height; + void dummy() {} + void dummy2() {} +} + +class NormalClass { + int value = 0; + + void ${expectLint('doSomething')}(Rectangle rect) { + print(rect.width); + print(rect.height); + print(rect.area); + } +} +'''); + + Future test_does_not_report_when_internal_exceeds_external() => + assertNoDiagnostics(r''' +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); + int get area => width * height; +} + +class NormalClass { + int value = 0; + + void doSomethingElse(Rectangle rect) { + print(rect.width); + print(rect.height); + print(value); + print(value); + print(value); + } +} +'''); + + Future test_reports_class_with_most_accesses() => + assertAutoDiagnostics(''' +class Circle { + final int radius; + const Circle(this.radius); + int get area => radius * radius; + void dummy() {} + void dummy2() {} +} + +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); + int get area => width * height; + void dummy() {} + void dummy2() {} +} + +class MathHelper { + int ${expectLint('sumOfAreas', name: 'feature_envy', messageContainsAll: ['Rectangle'])}(Rectangle r1, Rectangle r2, Circle c1) { + return (r1.width * r1.height) + (r2.width * r2.height) + c1.area; + } +} +'''); + + Future test_reports_when_accessing_inherited_methods() => + assertAutoDiagnostics(''' +class BaseExternal { + int get baseValue => 1; + void dummy() {} +} + +class External extends BaseExternal { + int get childValue => 2; +} + +class MyClass { + int ${expectLint('doSomething')}(External ext) { + return ext.baseValue + ext.childValue; + } +} +'''); + + Future test_reports_when_using_fields_as_targets() => + assertAutoDiagnostics(''' +class External { + void step1() {} + void step2() {} + void step3() {} +} + +class MyClass { + final External myField = External(); + + void ${expectLint('doSomething')}() { + myField.step1(); + myField.step2(); + myField.step3(); + } +} +'''); + + Future test_reports_when_using_fields_with_this_as_targets() => + assertAutoDiagnostics(''' +class External { + void step1() {} + void step2() {} + void step3() {} +} + +class MyClass { + final External myField = External(); + + void ${expectLint('doSomething')}() { + this.myField.step1(); + this.myField.step2(); + this.myField.step3(); + } +} +'''); + + Future test_reports_with_cascades() => assertAutoDiagnostics(''' +class External { + void step1() {} + void step2() {} + void step3() {} +} + +class MyClass { + final External myField = External(); + + void ${expectLint('doSomething')}() { + myField..step1()..step2()..step3(); + } +} +'''); + + Future test_reports_with_cascaded_assignments() => + assertAutoDiagnostics(''' +class External { + int value1 = 0; + int value2 = 0; + int value3 = 0; + void dummy1() {} + void dummy2() {} + void dummy3() {} +} + +class MyClass { + final External myField = External(); + + void ${expectLint('doSomething')}() { + myField..value1 = 1..value2 = 2..value3 = 3; + } +} +'''); + + Future test_reports_when_accessing_nested_foreign_fields() => + assertAutoDiagnostics(''' +class DeepService { + void execute() {} +} + +class Middleware { + final DeepService service = DeepService(); +} + +class Client { + final Middleware middleware = Middleware(); +} + +class MyClass { + void ${expectLint('process')}(Client client) { + client.middleware.service.execute(); + client.middleware.service.execute(); + client.middleware.service.execute(); + } +} +'''); + + Future test_reports_with_operator_overloads() => + assertAutoDiagnostics(''' +class Vector { + int x; + int y; + Vector(this.x, this.y); + + Vector operator +(Vector other) => Vector(x + other.x, y + other.y); +} + +class MathProcessor { + void ${expectLint('process')}(Vector v1, Vector v2, Vector v3) { + final result = v1 + v2 + v3; + print(result.x); + } +} +'''); + + Future test_patterns_threshold_2() => assertAutoDiagnostics(''' +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); + void dummy() {} +} + +class MathHelper { + void ${expectLint('doSomething')}(Rectangle r) { + if (r case Rectangle(:final height, :final width)) { + print(height); + } + } +} +'''); + + Future test_patterns_no_diagnostic_below_threshold() => + assertNoDiagnostics(''' +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); +} + +class MathHelper { + void doSomething(Rectangle r) { + if (r case Rectangle(:final height)) { + print(height); + } + } +} +'''); + + Future test_patterns_explicit_no_diagnostic_below_threshold() => + assertNoDiagnostics(''' +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); +} + +class MathHelper { + void doSomething(Rectangle r) { + if (r case Rectangle(height: final h)) { + print(h); + } + } +} +'''); + + Future test_index_assignment_should_trigger_lint() => + assertAutoDiagnostics(''' +class MyList { + int operator [](int index) => 0; + void operator []=(int index, int value) {} +} + +class MathHelper { + void ${expectLint('doSomething')}(MyList list) { + list[0] = 5; + list[1] = 6; + } +} +'''); + + Future test_compound_index_assignment_should_trigger_lint() => + assertAutoDiagnostics(''' +class MyList { + int operator [](int index) => 0; + void operator []=(int index, int value) {} +} + +class MathHelper { + void ${expectLint('doSomething')}(MyList list) { + list[0] += 5; + list[0] += 6; + } +} +'''); + + Future test_static_member_accesses_ignored() => assertNoDiagnostics(''' +class External { + static int value = 0; + static void staticMethod() {} +} + +class MathHelper { + void doSomething() { + External.value = 5; + External.staticMethod(); + External.staticMethod(); + } +} +'''); + + Future test_constructor_invocations_ignored() => assertNoDiagnostics(''' +class External { + const External(); +} + +class MathHelper { + void doSomething() { + final e1 = External(); + final e2 = External(); + final e3 = const External(); + } +} +'''); + + Future test_extension_override_on_this_internal() => + assertNoDiagnostics(''' +extension MyExtension on MathHelper { + void extMethod() {} +} + +class MathHelper { + void doSomething() { + MyExtension(this).extMethod(); + MyExtension(this).extMethod(); + } +} +'''); + + Future test_explicit_extension_override_on_external_target() => + assertAutoDiagnostics(''' +extension MyExtension on Rectangle { + void extMethod() {} +} + +class Rectangle { + final int width; + final int height; + const Rectangle(this.width, this.height); + void dummy() {} + void dummy2() {} +} + +class MathHelper { + void ${expectLint('doSomething')}(Rectangle r) { + MyExtension(r).extMethod(); + MyExtension(r).extMethod(); + } +} +'''); + + Future test_default_threshold_is_four() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + analysisOptionsContent(rules: [rule.name]), + ); + + await assertNoDiagnostics(r''' +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); + int get area => width * height; +} + +class MathHelper { + void doSomething(Rectangle r) { + print(r.width); + print(r.height); + print(r.area); + } +} +'''); + + await assertAutoDiagnostics(r''' +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); + int get area => width * height; + int getArea() => width * height; +} + +class MathHelper { + void ${expectLint('doSomething')}(Rectangle r) { + print(r.width); + print(r.height); + print(r.area); + print(r.getArea()); + } +} +'''); + } + + Future test_does_not_count_accesses_in_closures() => + assertNoDiagnostics(r''' +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); +} + +class MathHelper { + void doSomething(List list) { + list.forEach((r) { + print(r.width); + print(r.height); + print(r.width); + }); + } +} +'''); + + Future test_does_not_count_accesses_in_nested_functions() => + assertNoDiagnostics(r''' +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); +} + +class MathHelper { + void doSomething(Rectangle r) { + void helper() { + print(r.width); + print(r.height); + print(r.width); + } + helper(); + } +} +'''); + + Future test_does_not_report_on_widgets_and_states() => + assertNoDiagnostics(''' +$_importFlutterWidgets + +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); +} + +class MyWidget extends StatelessWidget { + Widget build(Rectangle r) { + print(r.width); + print(r.height); + print(r.width); + return MyWidget(); + } +} + +class MyWidgetState extends State { + void doSomething(Rectangle r) { + print(r.width); + print(r.height); + print(r.width); + } +} +'''); + + Future test_reports_on_cascades_of_instance_creation() => + assertAutoDiagnostics(''' +class External { + void step1() {} + void step2() {} + void step3() {} +} + +class MyClass { + void ${expectLint('doSomething')}() { + External()..step1()..step2()..step3(); + } +} +'''); + + Future test_profile_notifier_mimic() => assertNoDiagnostics(r''' +class Model { + final String field1; + final String field2; + + const Model({ + required this.field1, + required this.field2, + }); +} + +class Service { + Model? get model => null; +} + +class PageNotifier { + String _val1 = ''; + String _val2 = ''; + + void updateFromModel(Service service) { + final data = service.model; + if (data == null) return; + + _val1 = data.field1; + _val2 = data.field2; + } +} +'''); + + Future test_custom_laa_and_fdp_thresholds() async { + newAnalysisOptionsYamlFile(testPackageRootPath, ''' +plugins: + solid_lints: + rules: + - feature_envy + diagnostics: + feature_envy: + atfd_threshold: 2 + laa_threshold: 0.33 + fdp_threshold: 1 +'''); + + await assertNoDiagnostics(r''' +class External { + void method1() {} + void method2() {} + void method3() {} +} + +class Notifier { + int val = 0; + void update(External ext) { + ext.method1(); + ext.method2(); + ext.method3(); + val = 1; + val = 2; + } +} +'''); + + await assertNoDiagnostics(r''' +class External1 { + void method1() {} + void method2() {} +} + +class External2 { + void method3() {} +} + +class Notifier { + void update(External1 ext1, External2 ext2) { + ext1.method1(); + ext1.method2(); + ext2.method3(); + } +} +'''); + } + + Future test_simple_assignment_does_not_double_count() => + assertNoDiagnostics(r''' +class External { + int value = 0; +} + +class MyClass { + void doSomething(External ext) { + ext.value = 5; + } +} +'''); + + Future test_does_not_report_on_data_class_accesses() => + assertNoDiagnostics(r''' +class DataClass { + final int a; + final int b; + const DataClass(this.a, this.b); + + @override + String toString() => '$a, $b'; + + @override + bool operator ==(Object other) => other is DataClass && other.a == a && other.b == b; + + @override + int get hashCode => Object.hash(a, b); +} + +class MyClass { + void doSomething(DataClass data) { + print(data.a); + print(data.b); + print(data.a); + print(data.b); + } +} +'''); + + Future test_reports_on_non_data_class_accesses() => + assertAutoDiagnostics(''' +class NormalClass { + final int a; + final int b; + const NormalClass(this.a, this.b); + + void customMethod() {} +} + +class MyClass { + void ${expectLint('doSomething')}(NormalClass normal) { + print(normal.a); + print(normal.b); + print(normal.a); + print(normal.b); + } +} +'''); + + Future + test_reports_envy_when_accesses_spread_across_multiple_classes() async { + await assertAutoDiagnostics(''' +class Circle { + final int radius; + const Circle(this.radius); + int get area => radius * radius; + void dummy() {} +} + +class Rectangle { + final int height; + final int width; + const Rectangle(this.height, this.width); + int get area => width * height; + void dummy() {} + void dummy2() {} +} + +class MathHelper { + int value = 0; + + int ${expectLint('sumOfAreas')}(Rectangle r, Circle c) { + value = 1; + return r.width + r.height + c.radius + c.area; + } +} +'''); + } + + Future test_implicit_extension_override_on_this_internal() => + assertNoDiagnostics(r''' +extension MyExtension on MathHelper { + void extMethod() {} +} + +class Rectangle { + final int width; + final int height; + const Rectangle(this.width, this.height); +} + +class MathHelper { + void doSomething(Rectangle r) { + extMethod(); + extMethod(); + print(r.width); + print(r.height); + } +} +'''); + + Future test_reports_intermediate_external_accesses_correctly() => + assertAutoDiagnostics(''' +class External1 { + final External2 ext2; + const External1(this.ext2); + void customMethod1() {} +} + +class External2 { + final int value; + const External2(this.value); + void customMethod2() {} +} + +class MathHelper { + void ${expectLint('doSomething', name: 'feature_envy', messageContainsAll: ['External1'])}(External1 ext1) { + print(ext1.ext2.value); + print(ext1.ext2.value); + } +} +'''); +} diff --git a/test/src/lints/feature_envy/models/feature_envy_metrics_test.dart b/test/src/lints/feature_envy/models/feature_envy_metrics_test.dart new file mode 100644 index 00000000..8786d7dd --- /dev/null +++ b/test/src/lints/feature_envy/models/feature_envy_metrics_test.dart @@ -0,0 +1,94 @@ +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer_testing/src/analysis_rule/pub_package_resolution.dart'; +import 'package:solid_lints/src/lints/feature_envy/models/feature_envy_metrics.dart'; +import 'package:test/test.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(FeatureEnvyMetricsTest); + }); +} + +@reflectiveTest +class FeatureEnvyMetricsTest extends PubPackageResolutionTest { + Future _resolveCode(String code) { + newFile(testFile.path, code); + return resolveFile(testFile.path); + } + + ClassElement _getClassElement(ResolvedUnitResult result, String className) { + final declaration = result.unit.declarations + .whereType() + .firstWhere((d) => d.namePart.typeName.lexeme == className); + return declaration.declaredFragment!.element; + } + + Future test_standard_calculations() async { + final resolved = await _resolveCode(''' +class ExternalA {} +'''); + + final extA = _getClassElement(resolved, 'ExternalA'); + + _expectMetrics( + internalAccesses: 1, + externalAccessCounts: {extA: 2}, + laa: 1 / 3, + fdp: 1, + atfd: 2, + maxEnvyElement: extA, + ); + } + + Future test_multiple_external_classes() async { + final resolved = await _resolveCode(''' +class ExternalA {} +class ExternalB {} +'''); + + final extA = _getClassElement(resolved, 'ExternalA'); + final extB = _getClassElement(resolved, 'ExternalB'); + + _expectMetrics( + internalAccesses: 2, + externalAccessCounts: {extA: 1, extB: 3}, + laa: 2 / 6, + fdp: 2, + atfd: 3, + maxEnvyElement: extB, + ); + } + + Future test_no_accesses_at_all() async { + _expectMetrics( + internalAccesses: 0, + externalAccessCounts: const {}, + laa: 1.0, + fdp: 0, + atfd: 0, + maxEnvyElement: null, + ); + } + + void _expectMetrics({ + required int internalAccesses, + required Map externalAccessCounts, + required double laa, + required int fdp, + required int atfd, + required InterfaceElement? maxEnvyElement, + }) { + final metrics = FeatureEnvyMetrics.calculate( + internalAccesses: internalAccesses, + externalAccessCounts: externalAccessCounts, + ); + + expect(metrics.laa, equals(laa)); + expect(metrics.fdp, equals(fdp)); + expect(metrics.atfd, equals(atfd)); + expect(metrics.maxEnvyElement, equals(maxEnvyElement)); + } +} From ab09847c252c45fc9ba69191a8b29e2e921dff0e Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 22 Jul 2026 18:20:07 +0300 Subject: [PATCH 49/49] feat: add avoid_similar_names and feature_envy rules to changelog (#324) Co-authored-by: Illia Aihistov --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17527b5c..dbfc06cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ - Added `use_nearest_context` rule. - Added `avoid_duplicate_code` rule. - Added `use_descriptive_names_for_type_parameters` rule. +- Added `avoid_similar_names` rule. +- Added `feature_envy` rule. ## 0.3.3