From f8285aeb4a09bc047e2bafd33ebe066387b15e0b Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Tue, 14 Apr 2026 16:34:32 +0200 Subject: [PATCH 01/10] Migrate avoid_global_state rule and tests --- lint_test/lib/main.dart | 20 ++++++++++++++++++++ lint_test/pubspec.yaml | 4 +++- pubspec.yaml | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 lint_test/lib/main.dart 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 10e64475..2879bd20 100644 --- a/lint_test/pubspec.yaml +++ b/lint_test/pubspec.yaml @@ -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 a7c16536..f85b1309 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,3 +26,4 @@ dev_dependencies: args: ^2.6.0 analyzer_testing: ^0.1.9 test_reflective_loader: ^0.3.0 + From 07e75aaafb6740a64c61f201f1d7a28aea43450d Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 05:47:03 +0200 Subject: [PATCH 02/10] applied code review suggestions --- lib/main.dart | 1 - .../avoid_global_state_rule_visitor.dart | 51 +++++++++++++++++++ lint_test/lib/main.dart | 20 -------- lint_test/pubspec.yaml | 4 +- 4 files changed, 52 insertions(+), 24 deletions(-) create mode 100644 lib/src/lints/avoid_global_state/avoid_global_state_rule_visitor.dart delete mode 100644 lint_test/lib/main.dart diff --git a/lib/main.dart b/lib/main.dart index 78ee2a4c..4b1be247 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,5 @@ import 'package:analysis_server_plugin/plugin.dart'; import 'package:analysis_server_plugin/registry.dart'; -import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; diff --git a/lib/src/lints/avoid_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 + From 9fdf42e7635f4e9c0cf7ad286e65096af1d0a84a Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 07:48:43 +0200 Subject: [PATCH 03/10] migrated avoid debug print in release and tests --- lib/main.dart | 4 ++ ...d_debug_print_in_release_rule_visitor.dart | 61 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 lib/src/lints/avoid_debug_print_in_release/visitors/avoid_debug_print_in_release_rule_visitor.dart diff --git a/lib/main.dart b/lib/main.dart index 4b1be247..09264105 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:analysis_server_plugin/plugin.dart'; import 'package:analysis_server_plugin/registry.dart'; +import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; @@ -25,5 +26,8 @@ class SolidLintsPlugin extends Plugin { registry.registerLintRule( AvoidNonNullAssertionRule(), ); + registry.registerLintRule( + AvoidDebugPrintInReleaseRule(), + ); } } 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; + } +} From fb4dc04336ab6b6f3b2d12e4ec9f05a4e4ea28db Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 12:34:56 +0200 Subject: [PATCH 04/10] applied suggestions from code review --- .../{ => visitors}/avoid_global_state_rule_visitor.dart | 0 lint_test/pubspec.yaml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename lib/src/lints/avoid_global_state/{ => visitors}/avoid_global_state_rule_visitor.dart (100%) 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 From 11dbd6d02adae8b93e540525a826a7581577b40f Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 12:51:15 +0200 Subject: [PATCH 05/10] edited main documentation and changed avoid_global_state visitor naming --- .../avoid_global_state_rule_visitor.dart | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 lib/src/lints/avoid_global_state/visitors/avoid_global_state_rule_visitor.dart 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_rule_visitor.dart deleted file mode 100644 index 916c839c..00000000 --- a/lib/src/lints/avoid_global_state/visitors/avoid_global_state_rule_visitor.dart +++ /dev/null @@ -1,51 +0,0 @@ -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; - } -} From 0b8da3c837688d40325e6779bf6f9ed7354df4ad Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 15:40:51 +0200 Subject: [PATCH 06/10] migrated proper super calls rule and tests --- .../proper_super_calls_rule.dart | 152 +++++------------- .../visitors/proper_super_calls_visitor.dart | 65 ++++++++ lint_test/proper_super_calls_test.dart | 82 ---------- test/proper_super_calls_rule_test.dart | 83 ++++++++++ 4 files changed, 184 insertions(+), 198 deletions(-) create mode 100644 lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart delete mode 100644 lint_test/proper_super_calls_test.dart create mode 100644 test/proper_super_calls_rule_test.dart diff --git a/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart b/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart index 34df0dbc..9ef1f277 100644 --- a/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart +++ b/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart @@ -1,8 +1,8 @@ -import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; -import 'package:solid_lints/src/models/solid_lint_rule.dart'; +import 'package:analyzer/analysis_rule/analysis_rule.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:solid_lints/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart'; /// Ensures that `super` calls are made in the correct order for the following /// StatefulWidget methods: @@ -43,130 +43,50 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// super.dispose(); // OK /// } /// ``` -class ProperSuperCallsRule extends SolidLintRule { - /// This lint rule represents - /// the error whether the initState and dispose methods - /// are called in the incorrect order - static const lintName = 'proper_super_calls'; - static const _initState = 'initState'; - static const _dispose = 'dispose'; - static const _override = 'override'; +class ProperSuperCallsRule extends AnalysisRule { + /// This lint rule name. + static const String lintName = 'proper_super_calls'; - /// This lint rule represents - /// the error whether super.initState() should be called first + /// Error code for when super.initState() is not the first statement. static const _superInitStateCode = LintCode( - name: lintName, - problemMessage: "super.initState() should be first", + lintName, + "super.initState() should be first", ); - /// This lint rule represents - /// the error whether super.dispose() should be called last + /// Error code for when super.dispose() is not the last statement. static const _superDisposeCode = LintCode( - name: lintName, - problemMessage: "super.dispose() should be last", + lintName, + "super.dispose() should be last", ); - ProperSuperCallsRule._(super.config); + /// Creates a new instance of [ProperSuperCallsRule]. + ProperSuperCallsRule() + : super( + name: lintName, + description: + 'Ensures that `super` calls are made in the correct order ', + ); - /// Creates a new instance of [ProperSuperCallsRule] - /// based on the lint configuration. - factory ProperSuperCallsRule.createRule(CustomLintConfigs configs) { - final rule = RuleConfig( - name: lintName, - configs: configs, - problemMessage: (_) => 'Proper super calls issue', - ); - - return ProperSuperCallsRule._(rule); - } + @override + LintCode get diagnosticCode => _superInitStateCode; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addMethodDeclaration( - (node) { - final methodName = node.name.toString(); - final body = node.body; - - if (methodName case _initState || _dispose - when body is BlockFunctionBody) { - final statements = body.block.statements; - - _checkSuperCalls( - node, - methodName, - statements, - reporter, - ); - } + final visitor = ProperSuperCallsVisitor( + onViolation: (nameToken, {required bool isInitState}) { + // Access the reporter from the currentUnit + final reporter = context.currentUnit?.diagnosticReporter; + + reporter?.atToken( + nameToken, + isInitState ? _superInitStateCode : _superDisposeCode, + ); }, ); - } - - /// This method report an error whether `super.initState()` - /// or `super.dispose()` are called incorrect - void _checkSuperCalls( - MethodDeclaration node, - String methodName, - List statements, - DiagnosticReporter reporter, - ) { - final hasOverrideAnnotation = - node.metadata.any((annotation) => annotation.name.name == _override); - - if (!hasOverrideAnnotation) return; - if (methodName == _initState && !_isSuperInitStateCalledFirst(statements)) { - reporter.atNode( - node, - _superInitStateCode, - ); - } - if (methodName == _dispose && !_isSuperDisposeCalledLast(statements)) { - reporter.atNode( - node, - _superDisposeCode, - ); - } - } - - /// Returns `true` if `super.initState()` is called before other code in the - /// `initState` method, `false` otherwise. - bool _isSuperInitStateCalledFirst(List statements) { - if (statements.isEmpty) return false; - final firstStatement = statements.first; - - if (firstStatement is ExpressionStatement) { - final expression = firstStatement.expression; - - final isSuperInitStateCalledFirst = expression is MethodInvocation && - expression.target is SuperExpression && - expression.methodName.toString() == _initState; - - return isSuperInitStateCalledFirst; - } - - return false; - } - - /// Returns `true` if `super.dispose()` is called at the end of the `dispose` - /// method, `false` otherwise. - bool _isSuperDisposeCalledLast(List statements) { - if (statements.isEmpty) return false; - final lastStatement = statements.last; - - if (lastStatement is ExpressionStatement) { - final expression = lastStatement.expression; - - final lastStatementIsSuperDispose = expression is MethodInvocation && - expression.target is SuperExpression && - expression.methodName.toString() == _dispose; - - return lastStatementIsSuperDispose; - } - return false; + registry.addMethodDeclaration(this, visitor); } } diff --git a/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart b/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart new file mode 100644 index 00000000..5b2edaf3 --- /dev/null +++ b/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart @@ -0,0 +1,65 @@ +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/proper_super_calls/proper_super_calls_rule.dart'; + +/// Visitor for the proper [ProperSuperCallsRule]. +class ProperSuperCallsVisitor extends SimpleAstVisitor { + /// Callback to report violations. + final void Function(Token name, {required bool isInitState}) onViolation; + + static const _initState = 'initState'; + static const _dispose = 'dispose'; + static const _override = 'override'; + + /// Creates a new instance of [ProperSuperCallsVisitor]. + ProperSuperCallsVisitor({required this.onViolation}); + + @override + void visitMethodDeclaration(MethodDeclaration node) { + final methodName = node.name.lexeme; + final body = node.body; + + if ((methodName == _initState || methodName == _dispose) && + body is BlockFunctionBody) { + final hasOverride = node.metadata.any( + (annotation) => annotation.name.name == _override, + ); + if (!hasOverride) return; + + final statements = body.block.statements; + + // Logic for initState: super.initState() must be the very first statement. + if (methodName == _initState && + !_isSuperCallFirst(statements, _initState)) { + onViolation(node.name, isInitState: true); + } + + // Logic for dispose: super.dispose() must be the very last statement. + if (methodName == _dispose && !_isSuperCallLast(statements, _dispose)) { + onViolation(node.name, isInitState: false); + } + } + } + + /// Returns true if the first statement is the expected super call. + bool _isSuperCallFirst(List statements, String name) { + return statements.isNotEmpty && _isTargetSuperCall(statements.first, name); + } + + /// Returns true if the last statement is the expected super call. + bool _isSuperCallLast(List statements, String name) { + return statements.isNotEmpty && _isTargetSuperCall(statements.last, name); + } + + /// Checks if a statement is a [MethodInvocation] on [SuperExpression]. + bool _isTargetSuperCall(Statement statement, String name) { + if (statement is ExpressionStatement) { + final expr = statement.expression; + return expr is MethodInvocation && + expr.target is SuperExpression && + expr.methodName.name == name; + } + return false; + } +} diff --git a/lint_test/proper_super_calls_test.dart b/lint_test/proper_super_calls_test.dart deleted file mode 100644 index fc6e4773..00000000 --- a/lint_test/proper_super_calls_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -// ignore_for_file: prefer_match_file_name, no_empty_block - -class Widget {} - -abstract class State { - build(); - dispose() {} - initState() {} -} - -abstract class StatefulWidget extends Widget { - State createState(); - - StatefulWidget(); -} - -/// Check "check super" keyword fail -/// -/// `proper_super_calls` -class ProperSuperCallsTest1 extends StatefulWidget { - @override - State createState() => _ProperSuperCallsTest1State(); - - ProperSuperCallsTest1(); -} - -class _ProperSuperCallsTest1State extends State { - @override - Widget build() { - return Widget(); - } - - // expect_lint: proper_super_calls - @override - void initState() { - print(''); - super.initState(); - } - - // expect_lint: proper_super_calls - @override - void dispose() { - super.dispose(); - print(''); - } -} - -class ProperSuperCallsTest2 extends StatefulWidget { - @override - State createState() => _ProperSuperCallsTest2State(); - - ProperSuperCallsTest2(); -} - -class _ProperSuperCallsTest2State extends State { - @override - Widget build() { - return Widget(); - } - - @override - void initState() { - super.initState(); - print(''); - } - - @override - void dispose() { - print(''); - super.dispose(); - } -} - -class MyClass { - dispose() {} - initState() {} -} - -abstract interface class Disposable { - /// Abstract methods should be omitted by `proper_super_calls` - void dispose(); -} diff --git a/test/proper_super_calls_rule_test.dart b/test/proper_super_calls_rule_test.dart new file mode 100644 index 00000000..eb74b6ce --- /dev/null +++ b/test/proper_super_calls_rule_test.dart @@ -0,0 +1,83 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(ProperSuperCallsRuleTest); + }); +} + +@reflectiveTest +class ProperSuperCallsRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = ProperSuperCallsRule(); + super.setUp(); + } + + @override + String get analysisRule => ProperSuperCallsRule.lintName; + + /// Mocks the Flutter framework parts relevant to this lint. + String get _flutterBase => r''' +abstract class StatefulWidget {} +abstract class State { + void initState() {} + void dispose() {} +} +'''; + + void test_initState_reports_when_not_first() async { + await assertDiagnostics( + _flutterBase + + r''' +class MyWidgetState extends State { + @override + void initState() { + print('Bad'); + super.initState(); + } +} +''', + [ + lint(197, 9), // Highlights 'initState' + ]); + } + + void test_dispose_reports_when_not_last() async { + await assertDiagnostics( + _flutterBase + + r''' +class MyWidgetState extends State { + @override + void dispose() { + super.dispose(); + print('Bad'); + } +} +''', + [ + lint(197, 7), // Highlights 'dispose' + ]); + } + + void test_no_report_for_correct_flutter_usage() async { + await assertNoDiagnostics(_flutterBase + + r''' +class MyWidgetState extends State { + @override + void initState() { + super.initState(); + print('Good'); + } + + @override + void dispose() { + print('Good'); + super.dispose(); + } +} +'''); + } +} From 6df2188b75758fb5bdf4212757be1b401ff0aa52 Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Wed, 15 Apr 2026 16:22:02 +0200 Subject: [PATCH 07/10] registered ProperSuperCallsRule in the Plugin --- lib/main.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index 09264105..5657b7a1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'package:analysis_server_plugin/registry.dart'; import 'package:solid_lints/src/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule.dart'; import 'package:solid_lints/src/lints/avoid_global_state/avoid_global_state_rule.dart'; import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart'; +import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; /// The entry point for the Solid Lints analyser server plugin. /// @@ -29,5 +30,8 @@ class SolidLintsPlugin extends Plugin { registry.registerLintRule( AvoidDebugPrintInReleaseRule(), ); + registry.registerLintRule( + ProperSuperCallsRule(), + ); } } From aafa9e50ae3cd1824c86f9f5a2a5e541cfce6826 Mon Sep 17 00:00:00 2001 From: Islam-Shaaban-Ibrahim Date: Thu, 16 Apr 2026 17:03:49 +0200 Subject: [PATCH 08/10] applied code review suggestions --- .../proper_super_calls_rule.dart | 38 +++-- .../visitors/proper_super_calls_visitor.dart | 107 ++++++++++---- test/proper_super_calls_rule_test.dart | 133 +++++++++++++++--- 3 files changed, 203 insertions(+), 75 deletions(-) diff --git a/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart b/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart index 9ef1f277..fb82e8e8 100644 --- a/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart +++ b/lib/src/lints/proper_super_calls/proper_super_calls_rule.dart @@ -44,49 +44,43 @@ import 'package:solid_lints/src/lints/proper_super_calls/visitors/proper_super_c /// } /// ``` class ProperSuperCallsRule extends AnalysisRule { - /// This lint rule name. + /// Lint rule name. static const String lintName = 'proper_super_calls'; - /// Error code for when super.initState() is not the first statement. - static const _superInitStateCode = LintCode( + /// Error code for invalid `super.initState()` placement. + static const LintCode superInitStateCode = LintCode( lintName, - "super.initState() should be first", + 'super.initState() should be first', ); - /// Error code for when super.dispose() is not the last statement. - static const _superDisposeCode = LintCode( + /// Error code for invalid `super.dispose()` placement. + static const LintCode superDisposeCode = LintCode( lintName, - "super.dispose() should be last", + 'super.dispose() should be last', ); - /// Creates a new instance of [ProperSuperCallsRule]. + /// Creates an instance of [ProperSuperCallsRule]. ProperSuperCallsRule() : super( name: lintName, description: - 'Ensures that `super` calls are made in the correct order ', + 'Ensures proper ordering of Flutter lifecycle super calls.', ); @override - LintCode get diagnosticCode => _superInitStateCode; + LintCode get diagnosticCode => superInitStateCode; @override void registerNodeProcessors( RuleVisitorRegistry registry, RuleContext context, ) { - final visitor = ProperSuperCallsVisitor( - onViolation: (nameToken, {required bool isInitState}) { - // Access the reporter from the currentUnit - final reporter = context.currentUnit?.diagnosticReporter; - - reporter?.atToken( - nameToken, - isInitState ? _superInitStateCode : _superDisposeCode, - ); - }, + registry.addMethodDeclaration( + this, + ProperSuperCallsVisitor( + rule: this, + context: context, + ), ); - - registry.addMethodDeclaration(this, visitor); } } diff --git a/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart b/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart index 5b2edaf3..6cbe4759 100644 --- a/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart +++ b/lib/src/lints/proper_super_calls/visitors/proper_super_calls_visitor.dart @@ -1,65 +1,112 @@ +import 'package:analyzer/analysis_rule/rule_context.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; -/// Visitor for the proper [ProperSuperCallsRule]. +/// Visitor for [ProperSuperCallsRule]. class ProperSuperCallsVisitor extends SimpleAstVisitor { - /// Callback to report violations. - final void Function(Token name, {required bool isInitState}) onViolation; + /// The rule associated with this visitor. + final ProperSuperCallsRule rule; + + /// The context associated with this visitor. + final RuleContext context; static const _initState = 'initState'; static const _dispose = 'dispose'; - static const _override = 'override'; + static const _flutterStateClass = 'State'; /// Creates a new instance of [ProperSuperCallsVisitor]. - ProperSuperCallsVisitor({required this.onViolation}); + ProperSuperCallsVisitor({ + required this.rule, + required this.context, + }); @override void visitMethodDeclaration(MethodDeclaration node) { final methodName = node.name.lexeme; final body = node.body; - if ((methodName == _initState || methodName == _dispose) && - body is BlockFunctionBody) { - final hasOverride = node.metadata.any( - (annotation) => annotation.name.name == _override, + if ((methodName != _initState && methodName != _dispose) || + body is! BlockFunctionBody) { + return; + } + + if (!_overridesFlutterStateMethod(node)) { + return; + } + + final statements = body.block.statements; + final reporter = context.currentUnit?.diagnosticReporter; + + if (reporter == null) return; + + if (methodName == _initState && + !_isSuperCallFirst(statements, _initState)) { + reporter.atToken( + node.name, + ProperSuperCallsRule.superInitStateCode, + ); + } + + if (methodName == _dispose && !_isSuperCallLast(statements, _dispose)) { + reporter.atToken( + node.name, + ProperSuperCallsRule.superDisposeCode, ); - if (!hasOverride) return; + } + } - final statements = body.block.statements; + bool _overridesFlutterStateMethod(MethodDeclaration node) { + final classElement = node.declaredFragment?.element.enclosingElement; - // Logic for initState: super.initState() must be the very first statement. - if (methodName == _initState && - !_isSuperCallFirst(statements, _initState)) { - onViolation(node.name, isInitState: true); - } + if (classElement is! ClassElement) { + return false; + } + + final methodName = node.name.lexeme; + + final supertype = classElement.supertype; + + if (supertype == null) { + return false; + } - // Logic for dispose: super.dispose() must be the very last statement. - if (methodName == _dispose && !_isSuperCallLast(statements, _dispose)) { - onViolation(node.name, isInitState: false); - } + final isStateSubclass = _isStateSubclass(supertype); + + if (!isStateSubclass) { + return false; } + + return methodName == _initState || methodName == _dispose; + } + + bool _isStateSubclass(InterfaceType supertype) { + final isStateSubclass = supertype.element.name == _flutterStateClass || + supertype.allSupertypes.any( + (t) => t.element.name == _flutterStateClass, + ); + return isStateSubclass; } - /// Returns true if the first statement is the expected super call. bool _isSuperCallFirst(List statements, String name) { return statements.isNotEmpty && _isTargetSuperCall(statements.first, name); } - /// Returns true if the last statement is the expected super call. bool _isSuperCallLast(List statements, String name) { return statements.isNotEmpty && _isTargetSuperCall(statements.last, name); } - /// Checks if a statement is a [MethodInvocation] on [SuperExpression]. bool _isTargetSuperCall(Statement statement, String name) { - if (statement is ExpressionStatement) { - final expr = statement.expression; - return expr is MethodInvocation && - expr.target is SuperExpression && - expr.methodName.name == name; + if (statement is! ExpressionStatement) { + return false; } - return false; + + final expression = statement.expression; + + return expression is MethodInvocation && + expression.target is SuperExpression && + expression.methodName.name == name; } } diff --git a/test/proper_super_calls_rule_test.dart b/test/proper_super_calls_rule_test.dart index eb74b6ce..8969e73c 100644 --- a/test/proper_super_calls_rule_test.dart +++ b/test/proper_super_calls_rule_test.dart @@ -12,6 +12,19 @@ void main() { class ProperSuperCallsRuleTest extends AnalysisRuleTest { @override void setUp() { + final flutter = newPackage('flutter'); + flutter.addFile( + 'lib/src/widgets/framework.dart', + r''' +abstract class StatefulWidget {} + +abstract class State { + void initState() {} + void dispose() {} +} +''', + ); + rule = ProperSuperCallsRule(); super.setUp(); } @@ -19,37 +32,95 @@ class ProperSuperCallsRuleTest extends AnalysisRuleTest { @override String get analysisRule => ProperSuperCallsRule.lintName; - /// Mocks the Flutter framework parts relevant to this lint. - String get _flutterBase => r''' -abstract class StatefulWidget {} -abstract class State { - void initState() {} - void dispose() {} + void test_initState_reports_when_super_not_first() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class MyWidgetState extends State { + @override + void initState() { + print('Bad'); + super.initState(); + } } -'''; +''', + [lint(125, 9)], + ); + } - void test_initState_reports_when_not_first() async { + void test_dispose_reports_when_super_not_last() async { await assertDiagnostics( - _flutterBase + - r''' + r''' +import 'package:flutter/src/widgets/framework.dart'; + class MyWidgetState extends State { @override + void dispose() { + super.dispose(); + print('Bad'); + } +} +''', + [lint(125, 7)], + ); + } + + void test_reports_even_without_explicit_override_annotation() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class MyWidgetState extends State { void initState() { print('Bad'); super.initState(); } } ''', - [ - lint(197, 9), // Highlights 'initState' - ]); + [lint(113, 9)], + ); + } + + void test_reports_empty_body_missing_super() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class MyWidgetState extends State { + @override + void initState() {} +} +''', + [lint(125, 9)], + ); } - void test_dispose_reports_when_not_last() async { + void test_reports_when_wrong_super_method_is_called() async { await assertDiagnostics( - _flutterBase + - r''' + r''' +import 'package:flutter/src/widgets/framework.dart'; + class MyWidgetState extends State { + @override + void initState() { + super.dispose(); + print('Bad'); + } +} +''', + [lint(125, 9)], + ); + } + + void test_reports_in_deep_inheritance() async { + await assertDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +abstract class BaseState extends State {} + +class MyWidgetState extends BaseState { @override void dispose() { super.dispose(); @@ -57,14 +128,15 @@ class MyWidgetState extends State { } } ''', - [ - lint(197, 7), // Highlights 'dispose' - ]); + [lint(172, 7)], + ); } - void test_no_report_for_correct_flutter_usage() async { - await assertNoDiagnostics(_flutterBase + - r''' + void test_no_report_for_correct_usage() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + class MyWidgetState extends State { @override void initState() { @@ -78,6 +150,21 @@ class MyWidgetState extends State { super.dispose(); } } -'''); +''', + ); + } + + void test_no_report_for_other_methods() async { + await assertNoDiagnostics( + r''' +import 'package:flutter/src/widgets/framework.dart'; + +class MyWidgetState extends State { + void build() { + super.initState(); + } +} +''', + ); } } From 5aa35dff70d901f77fa4cab1f3672c6f18b7b501 Mon Sep 17 00:00:00 2001 From: vova-beloded-solid Date: Tue, 21 Apr 2026 09:30:17 +0300 Subject: [PATCH 09/10] Fix tests structure --- .../avoid_debug_print_in_release_rule_test.dart | 0 .../avoid_global_state}/avoid_global_state_rule_test.dart | 0 .../avoid_non_null_assertion_rule_test.dart | 0 .../proper_super_calls}/proper_super_calls_rule_test.dart | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename test/{ => lints/avoid_debug_print_in_release}/avoid_debug_print_in_release_rule_test.dart (100%) rename test/{ => lints/avoid_global_state}/avoid_global_state_rule_test.dart (100%) rename test/{ => lints/avoid_non_null_assertion}/avoid_non_null_assertion_rule_test.dart (100%) rename test/{ => lints/proper_super_calls}/proper_super_calls_rule_test.dart (100%) diff --git a/test/avoid_debug_print_in_release_rule_test.dart b/test/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart similarity index 100% rename from test/avoid_debug_print_in_release_rule_test.dart rename to test/lints/avoid_debug_print_in_release/avoid_debug_print_in_release_rule_test.dart diff --git a/test/avoid_global_state_rule_test.dart b/test/lints/avoid_global_state/avoid_global_state_rule_test.dart similarity index 100% rename from test/avoid_global_state_rule_test.dart rename to test/lints/avoid_global_state/avoid_global_state_rule_test.dart diff --git a/test/avoid_non_null_assertion_rule_test.dart b/test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart similarity index 100% rename from test/avoid_non_null_assertion_rule_test.dart rename to test/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule_test.dart diff --git a/test/proper_super_calls_rule_test.dart b/test/lints/proper_super_calls/proper_super_calls_rule_test.dart similarity index 100% rename from test/proper_super_calls_rule_test.dart rename to test/lints/proper_super_calls/proper_super_calls_rule_test.dart From 13feb81f081b92dc7c72b62069a05902d3bee15e Mon Sep 17 00:00:00 2001 From: vova-beloded-solid Date: Tue, 21 Apr 2026 09:33:06 +0300 Subject: [PATCH 10/10] Remove unnecessary file --- ...d_debug_print_in_release_rule_visitor.dart | 61 ------------------- 1 file changed, 61 deletions(-) delete mode 100644 lib/src/lints/avoid_debug_print_in_release/visitors/avoid_debug_print_in_release_rule_visitor.dart 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 deleted file mode 100644 index 3664f5a9..00000000 --- a/lib/src/lints/avoid_debug_print_in_release/visitors/avoid_debug_print_in_release_rule_visitor.dart +++ /dev/null @@ -1,61 +0,0 @@ -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; - } -}