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; +} +'''); + } +}