From c19649d93b707456cea350feff600589bcfd6ccf Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 24 Jun 2026 16:48:35 +0300 Subject: [PATCH 1/4] refactor: migrate no_empty_block --- lib/main.dart | 4 + .../models/no_empty_block_parameters.dart | 8 + .../no_empty_block/no_empty_block_rule.dart | 61 +++--- .../visitors/no_empty_block_visitor.dart | 28 ++- lint_test/no_empty_block_test.dart | 82 -------- .../no_empty_block_rule_test.dart | 188 ++++++++++++++++++ 6 files changed, 248 insertions(+), 123 deletions(-) delete mode 100644 lint_test/no_empty_block_test.dart create mode 100644 test/src/lints/no_empty_block/no_empty_block_rule_test.dart diff --git a/lib/main.dart b/lib/main.dart index d9d338a8..7974182d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,7 @@ import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexit import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart'; +import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; @@ -63,6 +64,9 @@ class SolidLintsPlugin extends Plugin { CyclomaticComplexityRule( analysisOptionsLoader: analysisLoader, ), + NoEmptyBlockRule( + analysisOptionsLoader: analysisLoader, + ), UseNearestContextRule(), preferFirstRule, // TODO: Add more lint rules and use analysisLoader diff --git a/lib/src/lints/no_empty_block/models/no_empty_block_parameters.dart b/lib/src/lints/no_empty_block/models/no_empty_block_parameters.dart index d8e143f3..3a39ad3c 100644 --- a/lib/src/lints/no_empty_block/models/no_empty_block_parameters.dart +++ b/lib/src/lints/no_empty_block/models/no_empty_block_parameters.dart @@ -17,6 +17,14 @@ class NoEmptyBlockParameters { required this.allowWithComments, }); + /// Empty [NoEmptyBlockParameters] model, excludes nothing. + factory NoEmptyBlockParameters.empty() { + return NoEmptyBlockParameters( + exclude: ExcludedIdentifiersListParameter(exclude: []), + allowWithComments: false, + ); + } + /// Method for creating from json data factory NoEmptyBlockParameters.fromJson(Map json) { return NoEmptyBlockParameters( diff --git a/lib/src/lints/no_empty_block/no_empty_block_rule.dart b/lib/src/lints/no_empty_block/no_empty_block_rule.dart index faa55d6c..9e0d28df 100644 --- a/lib/src/lints/no_empty_block/no_empty_block_rule.dart +++ b/lib/src/lints/no_empty_block/no_empty_block_rule.dart @@ -1,8 +1,8 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/no_empty_block/models/no_empty_block_parameters.dart'; import 'package:solid_lints/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; // Inspired by TSLint (https://palantir.github.io/tslint/rules/no-empty/) @@ -55,40 +55,39 @@ class NoEmptyBlockRule extends SolidLintRule { /// the error whether left empty block. static const String lintName = 'no_empty_block'; - NoEmptyBlockRule._(super.config); + static const _code = LintCode( + lintName, + 'Block is empty. Empty blocks are often indicators of missing code.', + ); - /// Creates a new instance of [NoEmptyBlockRule] - /// based on the lint configuration. - factory NoEmptyBlockRule.createRule(CustomLintConfigs configs) { - final config = RuleConfig( - configs: configs, - name: lintName, - paramsParser: NoEmptyBlockParameters.fromJson, - problemMessage: (_) => - 'Block is empty. Empty blocks are often indicators of missing code.', - ); + @override + DiagnosticCode get diagnosticCode => _code; - return NoEmptyBlockRule._(config); - } + /// Creates a new instance of [NoEmptyBlockRule] + NoEmptyBlockRule({ + required super.analysisOptionsLoader, + }) : super.withParameters( + name: lintName, + description: _code.problemMessage, + parametersParser: NoEmptyBlockParameters.fromJson, + ); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addDeclaration((node) { - final isIgnored = config.parameters.exclude.shouldIgnore(node); - if (isIgnored) return; + super.registerNodeProcessors(registry, context); + + final parameters = + getParametersForContext(context) ?? NoEmptyBlockParameters.empty(); - final visitor = NoEmptyBlockVisitor( - allowWithComments: config.parameters.allowWithComments, - ); - node.accept(visitor); + final visitor = NoEmptyBlockVisitor( + rule: this, + allowWithComments: parameters.allowWithComments, + exclude: parameters.exclude, + ); - for (final emptyBlock in visitor.emptyBlocks) { - reporter.atNode(emptyBlock, code); - } - }); + registry.addCompilationUnit(this, visitor); } } diff --git a/lib/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart b/lib/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart index 3f1e50d3..15a78191 100644 --- a/lib/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart +++ b/lib/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart @@ -21,26 +21,28 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import 'package:analyzer/analysis_rule/analysis_rule.dart'; import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart'; const _todoComment = 'TODO'; /// The AST visitor that will find all empty blocks, excluding catch blocks /// and blocks containing [_todoComment] class NoEmptyBlockVisitor extends RecursiveAstVisitor { + final AnalysisRule _rule; final bool _allowWithComments; - - final _emptyBlocks = []; + final ExcludedIdentifiersListParameter _exclude; /// Constructor for [NoEmptyBlockVisitor] - /// [_allowWithComments] indicates whether to allow empty blocks that contain - /// any comments - NoEmptyBlockVisitor({required bool allowWithComments}) - : _allowWithComments = allowWithComments; - - /// All empty blocks - Iterable get emptyBlocks => _emptyBlocks; + NoEmptyBlockVisitor({ + required AnalysisRule rule, + required bool allowWithComments, + required ExcludedIdentifiersListParameter exclude, + }) : _rule = rule, + _allowWithComments = allowWithComments, + _exclude = exclude; @override void visitBlock(Block node) { @@ -51,7 +53,13 @@ class NoEmptyBlockVisitor extends RecursiveAstVisitor { if (_allowWithComments && _isPrecedingCommentAny(node)) return; if (_isPrecedingCommentToDo(node)) return; - _emptyBlocks.add(node); + final enclosingDeclaration = node.thisOrAncestorOfType(); + if (enclosingDeclaration != null && + _exclude.shouldIgnore(enclosingDeclaration)) { + return; + } + + _rule.reportAtNode(node); } static bool _isPrecedingCommentToDo(Block node) => diff --git a/lint_test/no_empty_block_test.dart b/lint_test/no_empty_block_test.dart deleted file mode 100644 index 793dd031..00000000 --- a/lint_test/no_empty_block_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -// ignore_for_file: prefer_const_declarations, prefer_match_file_name, prefer_early_return -// ignore_for_file: unused_local_variable -// ignore_for_file: cyclomatic_complexity -// ignore_for_file: avoid_unused_parameters - -/// Check the `no_empty_block` rule - -// expect_lint: no_empty_block -void fun() {} - -void anotherFun() { - if (true) { - if (true) { - // expect_lint: no_empty_block - if (true) {} - } - } -} - -// to-do comments are allowed -void toDoStuff() { - // TODO: Implement doStuff function -} - -void catchStuff() { - // expect_lint: no_empty_block - try {} catch (e) {} - - try { - print('do stuff'); - // empty catch block is allowed - } catch (e) {} -} - -void nestedFun(void Function() fun) { - return; -} - -void doStuff() { - // expect_lint: no_empty_block - nestedFun(() {}); -} - -class A { - // expect_lint: no_empty_block - void method() {} - - void toDoMethod() { - // TODO: implement toDoMethod - } -} - -// no lint -void excludeMethod() {} - -class Exclude { - // no lint - void excludeMethod() {} -} - -// no lint -void emptyMethodWithComments() { - // comment explaining why this block is empty -} - -void anotherExample() { - // no lint - nestedFun(() { - // explain why this block is empty - }); -} - -void nestedIfElse() { - if (true) { - if (true) { - // no lint - if (true) { - // explain why this block is empty - } - } - } -} diff --git a/test/src/lints/no_empty_block/no_empty_block_rule_test.dart b/test/src/lints/no_empty_block/no_empty_block_rule_test.dart new file mode 100644 index 00000000..45878247 --- /dev/null +++ b/test/src/lints/no_empty_block/no_empty_block_rule_test.dart @@ -0,0 +1,188 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:analyzer_testing/utilities/utilities.dart'; +import 'package:solid_lints/src/common/parameter_parser/analysis_options_loader.dart'; +import 'package:solid_lints/src/lints/no_empty_block/no_empty_block_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../../../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(NoEmptyBlockRuleTest); + }); +} + +@reflectiveTest +class NoEmptyBlockRuleTest extends AnalysisRuleTest with AutoTestLintOffsets { + @override + void setUp() { + rule = NoEmptyBlockRule( + analysisOptionsLoader: AnalysisOptionsLoader( + resourceProvider: resourceProvider, + ), + ); + super.setUp(); + } + + @override + String get analysisRule => NoEmptyBlockRule.lintName; + + Future test_reports_on_empty_function() async { + await assertAutoDiagnostics(''' +void fun() ${expectLint('{}')} +'''); + } + + Future test_reports_on_empty_if_block() async { + await assertAutoDiagnostics(''' +void fun() { + if (true) ${expectLint('{}')} +} +'''); + } + + Future test_does_not_report_on_catch_clause() async { + await assertNoDiagnostics(r''' +void fun() { + try { + print('hello'); + } catch (e) {} +} +'''); + } + + Future test_does_not_report_on_todo_comment() async { + await assertNoDiagnostics(r''' +// ignore_for_file: todo +void fun() { + // TODO: implement +} +'''); + } + + Future test_does_not_report_on_any_comment_if_allowed() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + no_empty_block: + allow_with_comments: true +''', + ); + + await assertNoDiagnostics(r''' +void fun() { + // some explanation comment +} +'''); + } + + Future test_does_not_report_on_excluded() async { + newAnalysisOptionsYamlFile( + testPackageRootPath, + '''${analysisOptionsContent(rules: [rule.name])} +plugins: + solid_lints: + diagnostics: + no_empty_block: + exclude: + - method_name: excludeMethod + - class_name: Exclude + method_name: excludeMethod +''', + ); + + await assertNoDiagnostics(r''' +void excludeMethod() {} + +class Exclude { + void excludeMethod() {} +} +'''); + } + + Future test_reports_on_empty_try_block() async { + await assertAutoDiagnostics(''' +void fun() { + try ${expectLint('{}')} catch (e) {} +} +'''); + } + + Future test_reports_on_empty_closure() async { + await assertAutoDiagnostics(''' +void nestedFun(void Function() fun) { + print('not empty'); +} + +void doStuff() { + nestedFun(() ${expectLint('{}')}); +} +'''); + } + + Future test_reports_on_empty_class_method() async { + await assertAutoDiagnostics(''' +class A { + void method() ${expectLint('{}')} +} +'''); + } + + Future test_does_not_report_on_class_method_with_todo() async { + await assertNoDiagnostics(r''' +// ignore_for_file: todo +class A { + void toDoMethod() { + // TODO: implement toDoMethod + } +} +'''); + } + + Future test_reports_on_empty_finally_block() async { + await assertAutoDiagnostics(''' +void fun() { + try { + print('try'); + } finally ${expectLint('{}')} +} +'''); + } + + Future test_reports_on_empty_constructor_body() async { + await assertAutoDiagnostics(''' +class A { + A() ${expectLint('{}')} +} +'''); + } + + Future test_reports_on_empty_for_loop_block() async { + await assertAutoDiagnostics(''' +void fun() { + for (var i = 0; i < 10; i++) ${expectLint('{}')} +} +'''); + } + + Future test_reports_on_empty_while_loop_block() async { + await assertAutoDiagnostics(''' +void fun() { + while (true) ${expectLint('{}')} +} +'''); + } + + Future test_reports_on_empty_else_block() async { + await assertAutoDiagnostics(''' +void fun(bool condition) { + if (condition) { + print('if'); + } else ${expectLint('{}')} +} +'''); + } +} From bc4c32d5731e593eced27ac87b859874468d9acf Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 24 Jun 2026 16:59:12 +0300 Subject: [PATCH 2/4] fix: allow multiple preceding comments to satisfy the todo check in no_empty_block rule --- .../visitors/no_empty_block_visitor.dart | 19 ++++++++++++++----- .../no_empty_block_rule_test.dart | 10 ++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart b/lib/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart index 15a78191..f58ddd00 100644 --- a/lib/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart +++ b/lib/src/lints/no_empty_block/visitors/no_empty_block_visitor.dart @@ -23,6 +23,7 @@ import 'package:analyzer/analysis_rule/analysis_rule.dart'; import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart'; @@ -40,9 +41,9 @@ class NoEmptyBlockVisitor extends RecursiveAstVisitor { required AnalysisRule rule, required bool allowWithComments, required ExcludedIdentifiersListParameter exclude, - }) : _rule = rule, - _allowWithComments = allowWithComments, - _exclude = exclude; + }) : _rule = rule, + _allowWithComments = allowWithComments, + _exclude = exclude; @override void visitBlock(Block node) { @@ -62,8 +63,16 @@ class NoEmptyBlockVisitor extends RecursiveAstVisitor { _rule.reportAtNode(node); } - static bool _isPrecedingCommentToDo(Block node) => - node.endToken.precedingComments?.lexeme.contains(_todoComment) ?? false; + static bool _isPrecedingCommentToDo(Block node) { + Token? comment = node.endToken.precedingComments; + while (comment != null) { + if (comment.lexeme.contains(_todoComment)) { + return true; + } + comment = comment.next; + } + return false; + } static bool _isPrecedingCommentAny(Block node) => node.endToken.precedingComments != null; diff --git a/test/src/lints/no_empty_block/no_empty_block_rule_test.dart b/test/src/lints/no_empty_block/no_empty_block_rule_test.dart index 45878247..3de2702f 100644 --- a/test/src/lints/no_empty_block/no_empty_block_rule_test.dart +++ b/test/src/lints/no_empty_block/no_empty_block_rule_test.dart @@ -60,6 +60,16 @@ void fun() { '''); } + Future test_does_not_report_on_todo_comment_with_other_comments() async { + await assertNoDiagnostics(r''' +// ignore_for_file: todo +void fun() { + // some other comment + // TODO: implement +} +'''); + } + Future test_does_not_report_on_any_comment_if_allowed() async { newAnalysisOptionsYamlFile( testPackageRootPath, From 273013fa74312679d380ad685b7d24cb15d47499 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Wed, 24 Jun 2026 18:19:42 +0300 Subject: [PATCH 3/4] docs: add configuration examples to no_empty_block rule documentation --- .../lints/no_empty_block/no_empty_block_rule.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/src/lints/no_empty_block/no_empty_block_rule.dart b/lib/src/lints/no_empty_block/no_empty_block_rule.dart index 9e0d28df..db3e670f 100644 --- a/lib/src/lints/no_empty_block/no_empty_block_rule.dart +++ b/lib/src/lints/no_empty_block/no_empty_block_rule.dart @@ -13,6 +13,20 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// /// An empty code block often indicates missing code. /// +/// ### Example config: +/// +/// ```yaml +/// plugins: +/// solid_lints: +/// diagnostics: +/// no_empty_block: +/// allow_with_comments: true +/// exclude: +/// - method_name: build +/// - class_name: MyClass +/// method_name: build +/// ``` +/// /// ### Example /// /// #### BAD: From 803ea40424a0f71c5e3471cd6b1fab916acc5456 Mon Sep 17 00:00:00 2001 From: Illia Aihistov Date: Thu, 25 Jun 2026 16:48:16 +0300 Subject: [PATCH 4/4] test: rename excluded method in no_empty_block_rule_test --- test/src/lints/no_empty_block/no_empty_block_rule_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/src/lints/no_empty_block/no_empty_block_rule_test.dart b/test/src/lints/no_empty_block/no_empty_block_rule_test.dart index 3de2702f..f71a29e1 100644 --- a/test/src/lints/no_empty_block/no_empty_block_rule_test.dart +++ b/test/src/lints/no_empty_block/no_empty_block_rule_test.dart @@ -100,7 +100,7 @@ plugins: exclude: - method_name: excludeMethod - class_name: Exclude - method_name: excludeMethod + method_name: excludeClassMethod ''', ); @@ -108,7 +108,7 @@ plugins: void excludeMethod() {} class Exclude { - void excludeMethod() {} + void excludeClassMethod() {} } '''); }