From 81497032615d212e071bd4cbb1734413230422cd Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Tue, 14 Apr 2026 11:04:26 +0200 Subject: [PATCH 01/13] Add analysis_server_plugin dependency --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 80a1415e..58322307 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ environment: dependencies: analyzer: ^8.4.0 collection: ^1.19.0 + analysis_server_plugin: ^0.3.3 custom_lint_builder: ^0.8.1 glob: ^2.1.3 path: ^1.9.1 From b9ef33ada92054775a307f810f090cc567eb6e27 Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Tue, 14 Apr 2026 16:34:32 +0200 Subject: [PATCH 02/13] Migrate avoid_global_state rule and tests --- lib/solid_lints.dart | 79 ++---------- .../avoid_global_state_rule.dart | 119 +++++++++++------- lint_test/.gitignore | 45 +++++++ lint_test/.metadata | 30 +++++ lint_test/README.md | 3 + lint_test/avoid_global_state_test.dart | 66 +++++++--- lint_test/lib/main.dart | 20 +++ lint_test/pubspec.yaml | 8 +- pubspec.yaml | 4 +- 9 files changed, 238 insertions(+), 136 deletions(-) create mode 100644 lint_test/.gitignore create mode 100644 lint_test/.metadata create mode 100644 lint_test/README.md create mode 100644 lint_test/lib/main.dart diff --git a/lib/solid_lints.dart b/lib/solid_lints.dart index 4fc66f58..813837bf 100644 --- a/lib/solid_lints.dart +++ b/lib/solid_lints.dart @@ -1,77 +1,18 @@ -library solid_metrics; - -import 'package:custom_lint_builder/custom_lint_builder.dart'; +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_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(); +final plugin = SolidLintsPlugin(); -/// Initialize custom solid lints -class _SolidLints extends PluginBase { +class SolidLintsPlugin extends Plugin { @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), - ]; + String get name => 'solid_lints'; - // Return only enabled rules - return supportedRules.where((r) => r.enabled).toList(); + @override + void register(PluginRegistry registry) { + registry.registerLintRule( + AvoidGlobalStateRule(), + ); } } 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..ae037ff1 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,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/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/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; /// Avoid top-level and static mutable variables. /// @@ -23,7 +24,6 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// } /// ``` /// -/// /// #### GOOD: /// /// ```dart @@ -35,49 +35,84 @@ 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'; - - AvoidGlobalStateRule._(super.config); - - /// 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.', - ); - - return AvoidGlobalStateRule._(rule); - } +class AvoidGlobalStateRule extends AnalysisRule { + /// Lint name used for suppression and reporting. + static const String lintName = 'avoid_global_state'; + + /// 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 an instance of [AvoidGlobalStateRule]. + AvoidGlobalStateRule() + : super( + name: lintName, + description: + 'Avoid top-level or static mutable variables to reduce shared mutable state.', + ); + + @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)); - }); + final visitor = _Visitor(this); + + registry.addTopLevelVariableDeclaration(this, visitor); + registry.addFieldDeclaration(this, visitor); } } -extension on VariableDeclaration { - bool get isMutable => !isFinal && !isConst; +class _Visitor extends SimpleAstVisitor { + final AvoidGlobalStateRule rule; - bool get isPrivate => declaredFragment?.element.isPrivate ?? false; + _Visitor(this.rule); - bool get isPublicMutable => isMutable && !isPrivate; + @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 element = variable.declaredFragment?.element; + + final isFinal = element?.isFinal ?? false; + final isConst = element?.isConst ?? false; + + return !isFinal && !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 index 37028d42..05bb041b 100644 --- a/lint_test/avoid_global_state_test.dart +++ b/lint_test/avoid_global_state_test.dart @@ -1,30 +1,56 @@ -// ignore_for_file: type_annotate_public_apis, prefer_match_file_name, unused_local_variable +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'; -/// Check global mutable variable fail -/// `avoid_global_state` +@reflectiveTest +class AvoidGlobalStateRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = AvoidGlobalStateRule(); -// expect_lint: avoid_global_state -var globalMutable = 0; - -final globalFinal = 1; + super.setUp(); + } -const globalConst = 1; + 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 final int globalFinal = 1; - - // expect_lint: avoid_global_state - static int globalMutable = 0; - - final int memberFinal = 1; - - int memberMutable = 0; + static int staticMutable = 0; +} +''', + [lint(26, 17)], + ); + } - void m() { - int localMutable = 0; + void test_does_not_report_immutable_or_private_cases() async { + await assertNoDiagnostics(r''' +final globalFinal = 1; +const globalConst = 1; - final localFinal = 1; +var _privateTopLevel = 0; - const localConst = 2; +class Test { + static final int staticFinal = 1; + static const int staticConst = 2; + static int _staticPrivate = 0; + final int instanceFinal = 1; + int instanceMutable = 0; +} +'''); } } + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(AvoidGlobalStateRuleTest); + }); +} diff --git a/lint_test/lib/main.dart b/lint_test/lib/main.dart new file mode 100644 index 00000000..a7256585 --- /dev/null +++ b/lint_test/lib/main.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: Center( + child: Text('Hello World!'), + ), + ), + ); + } +} diff --git a/lint_test/pubspec.yaml b/lint_test/pubspec.yaml index cb37cd0b..2879bd20 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: @@ -12,4 +12,6 @@ dependencies: dev_dependencies: solid_lints: path: ../ - test: ^1.20.1 + analyzer_testing: ^0.1.9 + test_reflective_loader: ^0.3.0 + test: ^1.25.0 diff --git a/pubspec.yaml b/pubspec.yaml index 58322307..a8eefc85 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,10 +11,9 @@ environment: sdk: ">=3.5.0 <4.0.0" dependencies: - analyzer: ^8.4.0 + analyzer: ^10.0.1 collection: ^1.19.0 analysis_server_plugin: ^0.3.3 - custom_lint_builder: ^0.8.1 glob: ^2.1.3 path: ^1.9.1 yaml: ^3.1.3 @@ -25,3 +24,4 @@ dependencies: dev_dependencies: args: ^2.6.0 + From 65929c19665ef2ea14a5311863737a9b6e3fabbd Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 05:47:03 +0200 Subject: [PATCH 03/13] applied code review suggestions --- lib/{solid_lints.dart => main.dart} | 3 +- .../avoid_global_state_rule.dart | 52 +------------------ .../avoid_global_state_rule_visitor.dart | 51 ++++++++++++++++++ lint_test/lib/main.dart | 20 ------- lint_test/pubspec.yaml | 4 +- pubspec.yaml | 3 ++ .../avoid_global_state_rule_test.dart | 38 ++++++++++---- 7 files changed, 88 insertions(+), 83 deletions(-) rename lib/{solid_lints.dart => main.dart} (80%) create mode 100644 lib/src/lints/avoid_global_state/avoid_global_state_rule_visitor.dart delete mode 100644 lint_test/lib/main.dart rename lint_test/avoid_global_state_test.dart => test/avoid_global_state_rule_test.dart (65%) diff --git a/lib/solid_lints.dart b/lib/main.dart similarity index 80% rename from lib/solid_lints.dart rename to lib/main.dart index 813837bf..07d19258 100644 --- a/lib/solid_lints.dart +++ b/lib/main.dart @@ -1,10 +1,11 @@ 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'; +/// create plugin final plugin = SolidLintsPlugin(); +/// create plugin class class SolidLintsPlugin extends Plugin { @override String get name => 'solid_lints'; 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 ae037ff1..c601ccfc 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,9 +1,8 @@ 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/dart/ast/ast.dart'; -import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule_visitor.dart'; /// Avoid top-level and static mutable variables. /// @@ -63,56 +62,9 @@ class AvoidGlobalStateRule extends AnalysisRule { RuleVisitorRegistry registry, RuleContext context, ) { - final visitor = _Visitor(this); + final visitor = AvoidGlobalStateRuleVisitor(this); registry.addTopLevelVariableDeclaration(this, visitor); registry.addFieldDeclaration(this, visitor); } } - -class _Visitor extends SimpleAstVisitor { - final AvoidGlobalStateRule rule; - - _Visitor(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 element = variable.declaredFragment?.element; - - final isFinal = element?.isFinal ?? false; - final isConst = element?.isConst ?? false; - - return !isFinal && !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/lib/src/lints/avoid_global_state/avoid_global_state_rule_visitor.dart b/lib/src/lints/avoid_global_state/avoid_global_state_rule_visitor.dart new file mode 100644 index 00000000..916c839c --- /dev/null +++ b/lib/src/lints/avoid_global_state/avoid_global_state_rule_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 AvoidGlobalStateRuleVisitor extends SimpleAstVisitor { + /// The rule this visitor is associated with. + final AvoidGlobalStateRule rule; + + /// Creates an instance of [AvoidGlobalStateRuleVisitor]. + AvoidGlobalStateRuleVisitor(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/lib/main.dart b/lint_test/lib/main.dart deleted file mode 100644 index a7256585..00000000 --- a/lint_test/lib/main.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; - -void main() { - runApp(const MainApp()); -} - -class MainApp extends StatelessWidget { - const MainApp({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - home: Scaffold( - body: Center( - child: Text('Hello World!'), - ), - ), - ); - } -} diff --git a/lint_test/pubspec.yaml b/lint_test/pubspec.yaml index 2879bd20..18201052 100644 --- a/lint_test/pubspec.yaml +++ b/lint_test/pubspec.yaml @@ -12,6 +12,4 @@ dependencies: dev_dependencies: solid_lints: path: ../ - analyzer_testing: ^0.1.9 - test_reflective_loader: ^0.3.0 - test: ^1.25.0 + diff --git a/pubspec.yaml b/pubspec.yaml index a8eefc85..45317bec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,4 +24,7 @@ dependencies: dev_dependencies: args: ^2.6.0 + analyzer_testing: ^0.1.9 + test_reflective_loader: ^0.3.0 + test: ^1.25.0 diff --git a/lint_test/avoid_global_state_test.dart b/test/avoid_global_state_rule_test.dart similarity index 65% rename from lint_test/avoid_global_state_test.dart rename to test/avoid_global_state_rule_test.dart index 05bb041b..020f4344 100644 --- a/lint_test/avoid_global_state_test.dart +++ b/test/avoid_global_state_rule_test.dart @@ -2,12 +2,17 @@ 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(); } @@ -31,26 +36,41 @@ class Test { ); } - void test_does_not_report_immutable_or_private_cases() async { + 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; - static int _staticPrivate = 0; - final int instanceFinal = 1; - int instanceMutable = 0; } '''); } + + void test_does_not_report_class_level_private_variables() async { + await assertNoDiagnostics(r''' +class Test { + static int _staticPrivate = 0; } +'''); + } -void main() { - defineReflectiveSuite(() { - defineReflectiveTests(AvoidGlobalStateRuleTest); - }); + void test_does_not_report_local_method_variables() async { + await assertNoDiagnostics(r''' +void m() { + int localMutable = 0; +} +'''); + } } From 322278be29eb2ff66ea4cae6663c0c15c9e67fa7 Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 05:47:03 +0200 Subject: [PATCH 04/13] applied code review suggestions --- lib/{solid_lints.dart => main.dart} | 3 +- .../avoid_global_state_rule.dart | 55 +------------------ .../avoid_global_state_rule_visitor.dart | 51 +++++++++++++++++ lint_test/lib/main.dart | 20 ------- lint_test/pubspec.yaml | 4 +- pubspec.yaml | 3 + .../avoid_global_state_rule_test.dart | 38 ++++++++++--- 7 files changed, 89 insertions(+), 85 deletions(-) rename lib/{solid_lints.dart => main.dart} (80%) create mode 100644 lib/src/lints/avoid_global_state/avoid_global_state_rule_visitor.dart delete mode 100644 lint_test/lib/main.dart rename lint_test/avoid_global_state_test.dart => test/avoid_global_state_rule_test.dart (65%) diff --git a/lib/solid_lints.dart b/lib/main.dart similarity index 80% rename from lib/solid_lints.dart rename to lib/main.dart index 813837bf..07d19258 100644 --- a/lib/solid_lints.dart +++ b/lib/main.dart @@ -1,10 +1,11 @@ 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'; +/// create plugin final plugin = SolidLintsPlugin(); +/// create plugin class class SolidLintsPlugin extends Plugin { @override String get name => 'solid_lints'; 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 ae037ff1..c7b82fed 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,9 +1,8 @@ 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/dart/ast/ast.dart'; -import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule_visitor.dart'; /// Avoid top-level and static mutable variables. /// @@ -51,8 +50,7 @@ class AvoidGlobalStateRule extends AnalysisRule { AvoidGlobalStateRule() : super( name: lintName, - description: - 'Avoid top-level or static mutable variables to reduce shared mutable state.', + description: 'Avoid top-level or static mutable variables ', ); @override @@ -63,56 +61,9 @@ class AvoidGlobalStateRule extends AnalysisRule { RuleVisitorRegistry registry, RuleContext context, ) { - final visitor = _Visitor(this); + final visitor = AvoidGlobalStateRuleVisitor(this); registry.addTopLevelVariableDeclaration(this, visitor); registry.addFieldDeclaration(this, visitor); } } - -class _Visitor extends SimpleAstVisitor { - final AvoidGlobalStateRule rule; - - _Visitor(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 element = variable.declaredFragment?.element; - - final isFinal = element?.isFinal ?? false; - final isConst = element?.isConst ?? false; - - return !isFinal && !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/lib/src/lints/avoid_global_state/avoid_global_state_rule_visitor.dart b/lib/src/lints/avoid_global_state/avoid_global_state_rule_visitor.dart new file mode 100644 index 00000000..916c839c --- /dev/null +++ b/lib/src/lints/avoid_global_state/avoid_global_state_rule_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 AvoidGlobalStateRuleVisitor extends SimpleAstVisitor { + /// The rule this visitor is associated with. + final AvoidGlobalStateRule rule; + + /// Creates an instance of [AvoidGlobalStateRuleVisitor]. + AvoidGlobalStateRuleVisitor(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/lib/main.dart b/lint_test/lib/main.dart deleted file mode 100644 index a7256585..00000000 --- a/lint_test/lib/main.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; - -void main() { - runApp(const MainApp()); -} - -class MainApp extends StatelessWidget { - const MainApp({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - home: Scaffold( - body: Center( - child: Text('Hello World!'), - ), - ), - ); - } -} diff --git a/lint_test/pubspec.yaml b/lint_test/pubspec.yaml index 2879bd20..18201052 100644 --- a/lint_test/pubspec.yaml +++ b/lint_test/pubspec.yaml @@ -12,6 +12,4 @@ dependencies: dev_dependencies: solid_lints: path: ../ - analyzer_testing: ^0.1.9 - test_reflective_loader: ^0.3.0 - test: ^1.25.0 + diff --git a/pubspec.yaml b/pubspec.yaml index a8eefc85..45317bec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,4 +24,7 @@ dependencies: dev_dependencies: args: ^2.6.0 + analyzer_testing: ^0.1.9 + test_reflective_loader: ^0.3.0 + test: ^1.25.0 diff --git a/lint_test/avoid_global_state_test.dart b/test/avoid_global_state_rule_test.dart similarity index 65% rename from lint_test/avoid_global_state_test.dart rename to test/avoid_global_state_rule_test.dart index 05bb041b..020f4344 100644 --- a/lint_test/avoid_global_state_test.dart +++ b/test/avoid_global_state_rule_test.dart @@ -2,12 +2,17 @@ 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(); } @@ -31,26 +36,41 @@ class Test { ); } - void test_does_not_report_immutable_or_private_cases() async { + 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; - static int _staticPrivate = 0; - final int instanceFinal = 1; - int instanceMutable = 0; } '''); } + + void test_does_not_report_class_level_private_variables() async { + await assertNoDiagnostics(r''' +class Test { + static int _staticPrivate = 0; } +'''); + } -void main() { - defineReflectiveSuite(() { - defineReflectiveTests(AvoidGlobalStateRuleTest); - }); + void test_does_not_report_local_method_variables() async { + await assertNoDiagnostics(r''' +void m() { + int localMutable = 0; +} +'''); + } } From 62d79b6dfb5744c4c553bb28b800f587fddda60c Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 12:34:56 +0200 Subject: [PATCH 05/13] applied suggestions from code review --- lib/main.dart | 10 ++++++++-- .../avoid_global_state/avoid_global_state_rule.dart | 2 +- .../avoid_global_state_rule_visitor.dart | 0 lint_test/pubspec.yaml | 2 +- pubspec.yaml | 2 -- test/avoid_global_state_rule_test.dart | 2 +- 6 files changed, 11 insertions(+), 7 deletions(-) rename lib/src/lints/avoid_global_state/{ => visitors}/avoid_global_state_rule_visitor.dart (100%) diff --git a/lib/main.dart b/lib/main.dart index 07d19258..257ef524 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,10 +2,16 @@ 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'; -/// create plugin +/// 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(); -/// create plugin class +/// An analysis server plugin that provides Solid Lints 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'; 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 c7b82fed..1dc3f391 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 @@ -2,7 +2,7 @@ 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/avoid_global_state_rule_visitor.dart'; +import 'package:solid_lints/src/lints/avoid_global_state/visitors/avoid_global_state_rule_visitor.dart'; /// Avoid top-level and static mutable variables. /// diff --git a/lib/src/lints/avoid_global_state/avoid_global_state_rule_visitor.dart b/lib/src/lints/avoid_global_state/visitors/avoid_global_state_rule_visitor.dart similarity index 100% rename from lib/src/lints/avoid_global_state/avoid_global_state_rule_visitor.dart rename to lib/src/lints/avoid_global_state/visitors/avoid_global_state_rule_visitor.dart diff --git a/lint_test/pubspec.yaml b/lint_test/pubspec.yaml index 18201052..10e64475 100644 --- a/lint_test/pubspec.yaml +++ b/lint_test/pubspec.yaml @@ -12,4 +12,4 @@ dependencies: dev_dependencies: solid_lints: path: ../ - + test: ^1.20.1 diff --git a/pubspec.yaml b/pubspec.yaml index 45317bec..a7c16536 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,5 +26,3 @@ dev_dependencies: args: ^2.6.0 analyzer_testing: ^0.1.9 test_reflective_loader: ^0.3.0 - test: ^1.25.0 - diff --git a/test/avoid_global_state_rule_test.dart b/test/avoid_global_state_rule_test.dart index 020f4344..29616804 100644 --- a/test/avoid_global_state_rule_test.dart +++ b/test/avoid_global_state_rule_test.dart @@ -66,7 +66,7 @@ class Test { '''); } - void test_does_not_report_local_method_variables() async { + void test_does_not_report_local_variables() async { await assertNoDiagnostics(r''' void m() { int localMutable = 0; From e9c3d7695efb43c23414b0900ec61d033ad96bcf Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 07:48:43 +0200 Subject: [PATCH 06/13] migrated avoid debug print in release and tests --- lib/main.dart | 4 + .../avoid_debug_print_in_release_rule.dart | 196 +++--------------- ...d_debug_print_in_release_rule_visitor.dart | 61 ++++++ .../avoid_global_state_rule.dart | 3 +- .../avoid_debug_print_in_release_test.dart | 58 ------ ...void_debug_print_in_release_rule_test.dart | 88 ++++++++ 6 files changed, 180 insertions(+), 230 deletions(-) create mode 100644 lib/src/lints/avoid_debug_print_in_release/visitors/avoid_debug_print_in_release_rule_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 07d19258..eef611fa 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'; /// create plugin @@ -15,5 +16,8 @@ class SolidLintsPlugin extends Plugin { registry.registerLintRule( AvoidGlobalStateRule(), ); + registry.registerLintRule( + AvoidDebugPrintInReleaseRule(), + ); } } 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..cd089b2c 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,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/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/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_visitor.dart'; /// An `avoid_debug_print_in_release` rule which forbids calling or referencing /// debugPrint function from flutter/foundation in release mode. @@ -31,176 +31,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 = AvoidDebugPrintInReleaseRuleVisitor(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_rule_visitor.dart b/lib/src/lints/avoid_debug_print_in_release/visitors/avoid_debug_print_in_release_rule_visitor.dart new file mode 100644 index 00000000..3664f5a9 --- /dev/null +++ b/lib/src/lints/avoid_debug_print_in_release/visitors/avoid_debug_print_in_release_rule_visitor.dart @@ -0,0 +1,61 @@ +import 'package:analyzer/dart/ast/ast.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 AvoidDebugPrintInReleaseRuleVisitor extends SimpleAstVisitor { + /// The rule associated with this visitor. + final AvoidDebugPrintInReleaseRule rule; + + /// Creates an instance of [AvoidDebugPrintInReleaseRuleVisitor]. + AvoidDebugPrintInReleaseRuleVisitor(this.rule); + + @override + void visitMethodInvocation(MethodInvocation node) { + _check(node, node.methodName); + } + + @override + void visitSimpleIdentifier(SimpleIdentifier node) { + /// Catch cases where debugPrint is passed as a reference: + /// final x = debugPrint; + if (node.parent is! MethodInvocation) { + _check(node, node); + } + } + + void _check(AstNode node, SimpleIdentifier identifier) { + final element = identifier.element; + if (element == null) return; + +// Check the name + if (element.name == 'debugPrint') { + // Check if it's from the flutter/foundation path + final sourceUri = element.library?.uri.toString() ?? ''; + final isFlutterFoundation = + sourceUri.contains('package:flutter/foundation.dart'); + + if (isFlutterFoundation) { + if (!_isWrappedInReleaseCheck(node)) { + rule.reportAtNode(identifier); + } + } + } + } + + bool _isWrappedInReleaseCheck(AstNode node) { + AstNode? parent = node.parent; + while (parent != null) { + if (parent is IfStatement) { + final expression = parent.expression; + final source = expression.toString(); + + if (source.contains('kReleaseMode') || source.contains('kDebugMode')) { + return true; + } + } + parent = parent.parent; + } + return false; + } +} 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 c601ccfc..c7b82fed 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 @@ -50,8 +50,7 @@ class AvoidGlobalStateRule extends AnalysisRule { AvoidGlobalStateRule() : super( name: lintName, - description: - 'Avoid top-level or static mutable variables to reduce shared mutable state.', + description: 'Avoid top-level or static mutable variables ', ); @override 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..0f66b714 --- /dev/null +++ b/test/avoid_debug_print_in_release_rule_test.dart @@ -0,0 +1,88 @@ +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() { + // We only need the parts of foundation.dart that our rule cares about. + newPackage('flutter')..addFile('lib/foundation.dart', r''' + const bool kReleaseMode = false; + const bool kDebugMode = true; + void debugPrint(String? message) {} + '''); + + rule = AvoidDebugPrintInReleaseRule(); + + super.setUp(); + } + + 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(65, 10)], + ); + } + + void test_reports_debug_print_as_callback() async { + await assertDiagnostics( + r''' +import 'package:flutter/foundation.dart'; +void test() { + ['a'].forEach(debugPrint); +} +''', + [lint(72, 10)], + ); + } + + void test_does_not_report_guarded_call() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/foundation.dart'; + +void test() { + if (!kReleaseMode) { + debugPrint('This is safe'); + } +} +''', + ); + } + + void test_does_not_report_inside_kDebugMode() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/foundation.dart'; +void test() { + if (kDebugMode) { + debugPrint('Safe'); + } +} +''', + ); + } +} From aae01673c3e31b29afdfd4a708fa127800c22299 Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 12:51:15 +0200 Subject: [PATCH 07/13] edited main documentation and changed avoid_global_state visitor naming --- lib/main.dart | 2 +- .../lints/avoid_global_state/avoid_global_state_rule.dart | 4 ++-- ...te_rule_visitor.dart => avoid_global_state_visitor.dart} | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) rename lib/src/lints/avoid_global_state/visitors/{avoid_global_state_rule_visitor.dart => avoid_global_state_visitor.dart} (89%) diff --git a/lib/main.dart b/lib/main.dart index 257ef524..707269e3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,7 +8,7 @@ import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule /// allowing them to run during static analysis. final plugin = SolidLintsPlugin(); -/// An analysis server plugin that provides Solid Lints rules. +/// 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. 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 1dc3f391..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 @@ -2,7 +2,7 @@ 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_rule_visitor.dart'; +import 'package:solid_lints/src/lints/avoid_global_state/visitors/avoid_global_state_visitor.dart'; /// Avoid top-level and static mutable variables. /// @@ -61,7 +61,7 @@ class AvoidGlobalStateRule extends AnalysisRule { RuleVisitorRegistry registry, RuleContext context, ) { - final visitor = AvoidGlobalStateRuleVisitor(this); + final visitor = AvoidGlobalStateVisitor(this); registry.addTopLevelVariableDeclaration(this, visitor); registry.addFieldDeclaration(this, visitor); diff --git a/lib/src/lints/avoid_global_state/visitors/avoid_global_state_rule_visitor.dart b/lib/src/lints/avoid_global_state/visitors/avoid_global_state_visitor.dart similarity index 89% rename from lib/src/lints/avoid_global_state/visitors/avoid_global_state_rule_visitor.dart rename to lib/src/lints/avoid_global_state/visitors/avoid_global_state_visitor.dart index 916c839c..1f5a1876 100644 --- a/lib/src/lints/avoid_global_state/visitors/avoid_global_state_rule_visitor.dart +++ b/lib/src/lints/avoid_global_state/visitors/avoid_global_state_visitor.dart @@ -3,12 +3,12 @@ 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 AvoidGlobalStateRuleVisitor extends SimpleAstVisitor { +class AvoidGlobalStateVisitor extends SimpleAstVisitor { /// The rule this visitor is associated with. final AvoidGlobalStateRule rule; - /// Creates an instance of [AvoidGlobalStateRuleVisitor]. - AvoidGlobalStateRuleVisitor(this.rule); + /// Creates an instance of [AvoidGlobalStateVisitor]. + AvoidGlobalStateVisitor(this.rule); @override void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { From 8ded68783cf81684810761c22b4c478363091b19 Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 14:57:04 +0200 Subject: [PATCH 08/13] fixed main.dart conflict --- lib/main.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index f0fa5008..d2c4d983 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,8 +22,8 @@ class SolidLintsPlugin extends Plugin { registry.registerLintRule( AvoidGlobalStateRule(), ); - } - registry.registerLintRule( + registry.registerLintRule( AvoidDebugPrintInReleaseRule(), ); + } } From 5229ddad6313b810a0e085ee86f338181e0b64e1 Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 15:34:37 +0200 Subject: [PATCH 09/13] applied suggestions from review --- .../avoid_debug_print_in_release_visitor.dart | 15 ++-- ...void_debug_print_in_release_rule_test.dart | 90 +++++++++++++++++-- 2 files changed, 91 insertions(+), 14 deletions(-) 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 index 38e8953e..1ebc511b 100644 --- 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 @@ -6,6 +6,10 @@ import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_p 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'; /// Creates an instance of [AvoidDebugPrintInReleaseVisitor]. AvoidDebugPrintInReleaseVisitor(this.rule); @@ -29,11 +33,12 @@ class AvoidDebugPrintInReleaseVisitor extends SimpleAstVisitor { if (element == null) return; // Check the name - if (element.name == 'debugPrint') { - // Check if it's from the flutter/foundation path + if (element.name == _debugPrint) { final sourceUri = element.library?.uri.toString() ?? ''; - final isFlutterFoundation = - sourceUri.contains('package:flutter/foundation.dart'); + + final isFlutterFoundation = sourceUri.contains( + _foundationUri, + ); if (isFlutterFoundation) { if (!_isWrappedInReleaseCheck(node)) { @@ -50,7 +55,7 @@ class AvoidDebugPrintInReleaseVisitor extends SimpleAstVisitor { final expression = parent.expression; final source = expression.toString(); - if (source.contains('kReleaseMode') || source.contains('kDebugMode')) { + if (source.contains(_kReleaseMode) || source.contains(_kDebugMode)) { return true; } } diff --git a/test/avoid_debug_print_in_release_rule_test.dart b/test/avoid_debug_print_in_release_rule_test.dart index 0f66b714..9cd63b95 100644 --- a/test/avoid_debug_print_in_release_rule_test.dart +++ b/test/avoid_debug_print_in_release_rule_test.dart @@ -12,18 +12,33 @@ void main() { class AvoidDebugPrintInReleaseRuleTest extends AnalysisRuleTest { @override void setUp() { - // We only need the parts of foundation.dart that our rule cares about. - newPackage('flutter')..addFile('lib/foundation.dart', r''' - const bool kReleaseMode = false; - const bool kDebugMode = true; - void debugPrint(String? message) {} - '''); + // Setting up a mock Flutter package structure + final flutter = newPackage('flutter'); + + // Core foundation providing the constants and the function + flutter.addFile('lib/foundation.dart', r''' + const bool kReleaseMode = false; + const bool kDebugMode = true; + void debugPrint(String? message) {} + '''); + + // UI libraries that export foundation + 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''' @@ -59,14 +74,30 @@ void test() { ); } - void test_does_not_report_guarded_call() async { + /// Case: if (kReleaseMode) { ... } is considered safe/safe-guarded logic. + void test_does_not_report_kReleaseMode_guard() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/foundation.dart'; + +void test() { + if (kReleaseMode) { + debugPrint('This is safe because it only runs in release'); + } +} +''', + ); + } + + /// Case: if (!kDebugMode) { ... } is considered safe. + void test_does_not_report_not_kDebugMode_guard() async { await assertNoDiagnostics( r''' import 'package:flutter/foundation.dart'; void test() { - if (!kReleaseMode) { - debugPrint('This is safe'); + if (!kDebugMode) { + debugPrint('Safe'); } } ''', @@ -85,4 +116,45 @@ void test() { ''', ); } + + /// Case: debugPrint is defined locally, not from Flutter. + 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'); +} +''', + ); + } + + /// Case: debugPrint is imported via material.dart. + 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)], + ); + } + + /// Case: debugPrint is imported via cupertino.dart. + 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)], + ); + } } From 42397ea89cee5c43df18edce9c1615bd66b020e7 Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 17:06:15 +0200 Subject: [PATCH 10/13] refactored code according to suggestions from review --- .../avoid_debug_print_in_release_visitor.dart | 19 +++++- ...void_debug_print_in_release_rule_test.dart | 63 +++++++++++-------- 2 files changed, 54 insertions(+), 28 deletions(-) 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 index 1ebc511b..5c678bec 100644 --- 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 @@ -50,17 +50,30 @@ class AvoidDebugPrintInReleaseVisitor extends SimpleAstVisitor { bool _isWrappedInReleaseCheck(AstNode node) { AstNode? parent = node.parent; + while (parent != null) { if (parent is IfStatement) { - final expression = parent.expression; - final source = expression.toString(); + final condition = parent.expression; + + // if (!kReleaseMode) + if (condition is PrefixExpression && + condition.operator.lexeme == '!' && + condition.operand is SimpleIdentifier) { + final operand = condition.operand as SimpleIdentifier; + if (operand.name == _kReleaseMode) { + return true; + } + } - if (source.contains(_kReleaseMode) || source.contains(_kDebugMode)) { + // if (kDebugMode) + if (condition is SimpleIdentifier && condition.name == _kDebugMode) { return true; } } + parent = parent.parent; } + return false; } } diff --git a/test/avoid_debug_print_in_release_rule_test.dart b/test/avoid_debug_print_in_release_rule_test.dart index 9cd63b95..9e2ba4b0 100644 --- a/test/avoid_debug_print_in_release_rule_test.dart +++ b/test/avoid_debug_print_in_release_rule_test.dart @@ -12,24 +12,21 @@ void main() { class AvoidDebugPrintInReleaseRuleTest extends AnalysisRuleTest { @override void setUp() { - // Setting up a mock Flutter package structure final flutter = newPackage('flutter'); - // Core foundation providing the constants and the function flutter.addFile('lib/foundation.dart', r''' - const bool kReleaseMode = false; - const bool kDebugMode = true; - void debugPrint(String? message) {} - '''); +const bool kReleaseMode = false; +const bool kDebugMode = true; +void debugPrint(String? message) {} +'''); - // UI libraries that export foundation flutter.addFile('lib/material.dart', r''' - export 'package:flutter/foundation.dart'; - '''); +export 'package:flutter/foundation.dart'; +'''); flutter.addFile('lib/cupertino.dart', r''' - export 'package:flutter/foundation.dart'; - '''); +export 'package:flutter/foundation.dart'; +'''); rule = AvoidDebugPrintInReleaseRule(); @@ -54,11 +51,14 @@ void test() { void test_reports_aliased_debug_print_from_package() async { await assertDiagnostics( - r'''import 'package:flutter/foundation.dart' as f; + r''' +import 'package:flutter/foundation.dart' as f; + void test() { f.debugPrint('This should be flagged'); -}''', - [lint(65, 10)], +} +''', + [lint(66, 10)], ); } @@ -66,37 +66,52 @@ void test() { await assertDiagnostics( r''' import 'package:flutter/foundation.dart'; + void test() { ['a'].forEach(debugPrint); } ''', - [lint(72, 10)], + [lint(73, 10)], ); } - /// Case: if (kReleaseMode) { ... } is considered safe/safe-guarded logic. - void test_does_not_report_kReleaseMode_guard() async { - await assertNoDiagnostics( + void test_reports_inside_kReleaseMode_guard() async { + await assertDiagnostics( r''' import 'package:flutter/foundation.dart'; void test() { if (kReleaseMode) { - debugPrint('This is safe because it only runs in release'); + debugPrint('This should be flagged'); } } ''', + [lint(83, 10)], ); } - /// Case: if (!kDebugMode) { ... } is considered safe. - void test_does_not_report_not_kDebugMode_guard() async { - await assertNoDiagnostics( + 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'); } } @@ -108,6 +123,7 @@ void test() { await assertNoDiagnostics( r''' import 'package:flutter/foundation.dart'; + void test() { if (kDebugMode) { debugPrint('Safe'); @@ -117,7 +133,6 @@ void test() { ); } - /// Case: debugPrint is defined locally, not from Flutter. void test_no_report_when_debugPrint_is_not_from_foundation() async { await assertNoDiagnostics( r''' @@ -130,7 +145,6 @@ void test() { ); } - /// Case: debugPrint is imported via material.dart. void test_reports_when_imported_via_material() async { await assertDiagnostics( r''' @@ -144,7 +158,6 @@ void test() { ); } - /// Case: debugPrint is imported via cupertino.dart. void test_reports_when_imported_via_cupertino() async { await assertDiagnostics( r''' From 801c81c5221dea34842726af57dbb3ab9318b30e Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Thu, 16 Apr 2026 10:23:35 +0200 Subject: [PATCH 11/13] applied code review suggestions --- .../avoid_debug_print_in_release_rule.dart | 4 +- .../avoid_debug_print_in_release_visitor.dart | 69 +++++++++++-------- ...void_debug_print_in_release_rule_test.dart | 13 ++++ 3 files changed, 56 insertions(+), 30 deletions(-) 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 8009d628..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 @@ -35,7 +35,7 @@ class AvoidDebugPrintInReleaseRule extends AnalysisRule { static const String lintName = 'avoid_debug_print_in_release'; /// Lint code used for suppression and reporting. - static const LintCode code = LintCode( + static const LintCode _code = LintCode( lintName, 'Avoid debugPrint in release mode.', correctionMessage: 'Wrap in a kReleaseMode check or use a logging package.', @@ -46,7 +46,7 @@ class AvoidDebugPrintInReleaseRule extends AnalysisRule { : super(name: lintName, description: 'Avoid debugPrint in release mode.'); @override - LintCode get diagnosticCode => code; + LintCode get diagnosticCode => _code; @override void registerNodeProcessors( 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 index 5c678bec..d0d71195 100644 --- 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 @@ -6,8 +6,10 @@ import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_p 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 _callMethod = 'call'; static const _kReleaseMode = 'kReleaseMode'; static const _kDebugMode = 'kDebugMode'; @@ -16,14 +18,29 @@ class AvoidDebugPrintInReleaseVisitor extends SimpleAstVisitor { @override void visitMethodInvocation(MethodInvocation node) { - _check(node, node.methodName); + 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) { - /// Catch cases where debugPrint is passed as a reference: - /// final x = debugPrint; - if (node.parent is! MethodInvocation) { + final parent = node.parent; + + final isDirectInvocation = + parent is MethodInvocation && parent.methodName == node; + + final isCallTarget = parent is MethodInvocation && + parent.methodName.name == _callMethod && + parent.target == node; + + if (!isDirectInvocation && !isCallTarget) { _check(node, node); } } @@ -32,41 +49,26 @@ class AvoidDebugPrintInReleaseVisitor extends SimpleAstVisitor { final element = identifier.element; if (element == null) return; -// Check the name - if (element.name == _debugPrint) { - final sourceUri = element.library?.uri.toString() ?? ''; + if (element.name != _debugPrint) return; - final isFlutterFoundation = sourceUri.contains( - _foundationUri, - ); + final sourceUri = element.library?.uri.toString() ?? ''; + final isFlutterFoundation = sourceUri.contains(_foundationUri); - if (isFlutterFoundation) { - if (!_isWrappedInReleaseCheck(node)) { - rule.reportAtNode(identifier); - } - } + if (!isFlutterFoundation) return; + + if (!_isWrappedInSafeDebugCheck(node)) { + rule.reportAtNode(identifier); } } - bool _isWrappedInReleaseCheck(AstNode node) { + bool _isWrappedInSafeDebugCheck(AstNode node) { AstNode? parent = node.parent; while (parent != null) { if (parent is IfStatement) { final condition = parent.expression; - // if (!kReleaseMode) - if (condition is PrefixExpression && - condition.operator.lexeme == '!' && - condition.operand is SimpleIdentifier) { - final operand = condition.operand as SimpleIdentifier; - if (operand.name == _kReleaseMode) { - return true; - } - } - - // if (kDebugMode) - if (condition is SimpleIdentifier && condition.name == _kDebugMode) { + if (_isNotReleaseModeCheck(condition) || _isDebugModeCheck(condition)) { return true; } } @@ -76,4 +78,15 @@ class AvoidDebugPrintInReleaseVisitor extends SimpleAstVisitor { return false; } + + bool _isNotReleaseModeCheck(Expression condition) { + return condition is PrefixExpression && + condition.operator.lexeme == '!' && + condition.operand is SimpleIdentifier && + (condition.operand as SimpleIdentifier).name == _kReleaseMode; + } + + bool _isDebugModeCheck(Expression condition) { + return condition is SimpleIdentifier && condition.name == _kDebugMode; + } } diff --git a/test/avoid_debug_print_in_release_rule_test.dart b/test/avoid_debug_print_in_release_rule_test.dart index 9e2ba4b0..74c6554b 100644 --- a/test/avoid_debug_print_in_release_rule_test.dart +++ b/test/avoid_debug_print_in_release_rule_test.dart @@ -170,4 +170,17 @@ void test() { [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 a49b4be5f65d20fb94e02c68ed25305520cd8505 Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Thu, 16 Apr 2026 13:05:14 +0200 Subject: [PATCH 12/13] applied latest changes according to code review --- .../avoid_debug_print_in_release_visitor.dart | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) 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 index d0d71195..abbbdb28 100644 --- 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 @@ -1,4 +1,5 @@ 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'; @@ -6,12 +7,11 @@ import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_p 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 _callMethod = 'call'; static const _kReleaseMode = 'kReleaseMode'; static const _kDebugMode = 'kDebugMode'; + static const _callMethod = 'call'; /// Creates an instance of [AvoidDebugPrintInReleaseVisitor]. AvoidDebugPrintInReleaseVisitor(this.rule); @@ -31,16 +31,9 @@ class AvoidDebugPrintInReleaseVisitor extends SimpleAstVisitor { @override void visitSimpleIdentifier(SimpleIdentifier node) { - final parent = node.parent; - - final isDirectInvocation = - parent is MethodInvocation && parent.methodName == node; - - final isCallTarget = parent is MethodInvocation && - parent.methodName.name == _callMethod && - parent.target == node; - - if (!isDirectInvocation && !isCallTarget) { + /// Catch cases where debugPrint is passed as a reference: + /// final x = debugPrint; + if (node.parent is! MethodInvocation) { _check(node, node); } } @@ -49,19 +42,23 @@ class AvoidDebugPrintInReleaseVisitor extends SimpleAstVisitor { final element = identifier.element; if (element == null) return; - if (element.name != _debugPrint) return; - - final sourceUri = element.library?.uri.toString() ?? ''; - final isFlutterFoundation = sourceUri.contains(_foundationUri); +// Check the name + if (element.name == _debugPrint) { + final sourceUri = element.library?.uri.toString() ?? ''; - if (!isFlutterFoundation) return; + final isFlutterFoundation = sourceUri.contains( + _foundationUri, + ); - if (!_isWrappedInSafeDebugCheck(node)) { - rule.reportAtNode(identifier); + if (isFlutterFoundation) { + if (!_isWrappedInReleaseCheck(node)) { + rule.reportAtNode(identifier); + } + } } } - bool _isWrappedInSafeDebugCheck(AstNode node) { + bool _isWrappedInReleaseCheck(AstNode node) { AstNode? parent = node.parent; while (parent != null) { @@ -81,7 +78,7 @@ class AvoidDebugPrintInReleaseVisitor extends SimpleAstVisitor { bool _isNotReleaseModeCheck(Expression condition) { return condition is PrefixExpression && - condition.operator.lexeme == '!' && + condition.operator.type == TokenType.BANG && condition.operand is SimpleIdentifier && (condition.operand as SimpleIdentifier).name == _kReleaseMode; } From 1d5c4a0fd939d5ee466ca1ca7e044114001c69ea Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Thu, 16 Apr 2026 14:12:43 +0200 Subject: [PATCH 13/13] remove unnecessary comments in AvoidDebugPrintInReleaseVisitor --- .../visitors/avoid_debug_print_in_release_visitor.dart | 3 --- 1 file changed, 3 deletions(-) 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 index abbbdb28..8b794a1b 100644 --- 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 @@ -31,8 +31,6 @@ class AvoidDebugPrintInReleaseVisitor extends SimpleAstVisitor { @override void visitSimpleIdentifier(SimpleIdentifier node) { - /// Catch cases where debugPrint is passed as a reference: - /// final x = debugPrint; if (node.parent is! MethodInvocation) { _check(node, node); } @@ -42,7 +40,6 @@ class AvoidDebugPrintInReleaseVisitor extends SimpleAstVisitor { final element = identifier.element; if (element == null) return; -// Check the name if (element.name == _debugPrint) { final sourceUri = element.library?.uri.toString() ?? '';