From f7c06eb18004949e159d72bb6593e92608cdc4e5 Mon Sep 17 00:00:00 2001 From: Brian Wilkerson Date: Fri, 8 May 2026 13:55:19 -0700 Subject: [PATCH] Add a new lint to find container bodies that could be a semicolon The purpose of this lint is to be used with other lints to maximally convert code to using the new features introduced by the primary contructors feature. This one is targeted at finding container bodies the could be replaced by a semicolon. This CL does not include a fix. That will be added in a separate CL. We will need to decide whether this lint is worth supporting beyond the testing period. Change-Id: I5d2d05117c9f1efc8c71279d5cde549eb48b480c Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/502182 Reviewed-by: Samuel Rawlins Commit-Queue: Brian Wilkerson --- .../services/correction/error_fix_status.yaml | 2 + pkg/linter/example/all.yaml | 1 + pkg/linter/lib/src/diagnostic.g.dart | 22 ++++++ pkg/linter/lib/src/lint_names.g.dart | 2 + pkg/linter/lib/src/rules.dart | 2 + .../lib/src/rules/empty_container_bodies.dart | 63 ++++++++++++++++ pkg/linter/messages.yaml | 12 +++ pkg/linter/test/rules/all.dart | 2 + .../rules/empty_container_bodies_test.dart | 74 +++++++++++++++++++ 9 files changed, 180 insertions(+) create mode 100644 pkg/linter/lib/src/rules/empty_container_bodies.dart create mode 100644 pkg/linter/test/rules/empty_container_bodies_test.dart diff --git a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml index 04c9b5aa7b6..41c9af7c1e7 100644 --- a/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml +++ b/pkg/analysis_server/lib/src/services/correction/error_fix_status.yaml @@ -2128,6 +2128,8 @@ empty_catches: status: hasFix empty_constructor_bodies: status: hasFix +empty_container_bodies: + status: needsFix empty_statements: status: hasFix eol_at_end_of_file: diff --git a/pkg/linter/example/all.yaml b/pkg/linter/example/all.yaml index 0808c126afd..4e23ad95216 100644 --- a/pkg/linter/example/all.yaml +++ b/pkg/linter/example/all.yaml @@ -72,6 +72,7 @@ linter: - document_ignores - empty_catches - empty_constructor_bodies + - empty_container_bodies - empty_statements - eol_at_end_of_file - exhaustive_cases diff --git a/pkg/linter/lib/src/diagnostic.g.dart b/pkg/linter/lib/src/diagnostic.g.dart index 5374232bd65..d6c08d93a90 100644 --- a/pkg/linter/lib/src/diagnostic.g.dart +++ b/pkg/linter/lib/src/diagnostic.g.dart @@ -1290,6 +1290,22 @@ emptyConstructorBodies = LinterLintWithoutArguments( expectedTypes: [], ); +/// Parameters: +/// String containerKind: The kind of the container, such as 'class' or +/// 'enum'. +const DiagnosticWithArguments< + LocatableDiagnostic Function({required String containerKind}) +> +emptyContainerBodies = LinterLintTemplate( + name: 'empty_container_bodies', + problemMessage: + "Empty {0} bodies should be written using a ';' rather than '{}'.", + correctionMessage: "Try replacing the {0} body with ';'.", + uniqueName: 'empty_container_bodies', + withArguments: _withArgumentsEmptyContainerBodies, + expectedTypes: [ExpectedType.string], +); + /// No parameters. const LinterLintWithoutArguments emptyStatements = LinterLintWithoutArguments( name: 'empty_statements', @@ -4129,6 +4145,12 @@ LocatableDiagnostic _withArgumentsDirectivesOrderingPackageBeforeRelative({ ]); } +LocatableDiagnostic _withArgumentsEmptyContainerBodies({ + required String containerKind, +}) { + return LocatableDiagnosticImpl(diag.emptyContainerBodies, [containerKind]); +} + LocatableDiagnostic _withArgumentsExhaustiveCases({required Object p0}) { return LocatableDiagnosticImpl(diag.exhaustiveCases, [p0]); } diff --git a/pkg/linter/lib/src/lint_names.g.dart b/pkg/linter/lib/src/lint_names.g.dart index 42354a8030c..b1bd6125253 100644 --- a/pkg/linter/lib/src/lint_names.g.dart +++ b/pkg/linter/lib/src/lint_names.g.dart @@ -229,6 +229,8 @@ abstract final class LintNames { static const String empty_constructor_bodies = 'empty_constructor_bodies'; + static const String empty_container_bodies = 'empty_container_bodies'; + static const String empty_statements = 'empty_statements'; static const String enable_null_safety = 'enable_null_safety'; diff --git a/pkg/linter/lib/src/rules.dart b/pkg/linter/lib/src/rules.dart index dc9445336ab..dfbf35c92d4 100644 --- a/pkg/linter/lib/src/rules.dart +++ b/pkg/linter/lib/src/rules.dart @@ -82,6 +82,7 @@ import 'rules/do_not_use_environment.dart'; import 'rules/document_ignores.dart'; import 'rules/empty_catches.dart'; import 'rules/empty_constructor_bodies.dart'; +import 'rules/empty_container_bodies.dart'; import 'rules/empty_statements.dart'; import 'rules/enable_null_safety.dart'; import 'rules/eol_at_end_of_file.dart'; @@ -345,6 +346,7 @@ void registerLintRules() { ..registerLintRule(DoNotUseEnvironment()) ..registerLintRule(EmptyCatches()) ..registerLintRule(EmptyConstructorBodies()) + ..registerLintRule(EmptyContainerBodies()) ..registerLintRule(EmptyStatements()) ..registerLintRule(enableNullSafety) ..registerLintRule(EolAtEndOfFile()) diff --git a/pkg/linter/lib/src/rules/empty_container_bodies.dart b/pkg/linter/lib/src/rules/empty_container_bodies.dart new file mode 100644 index 00000000000..6e655f0b9f6 --- /dev/null +++ b/pkg/linter/lib/src/rules/empty_container_bodies.dart @@ -0,0 +1,63 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +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/analysis/features.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/source/source_range.dart'; + +import '../analyzer.dart'; +import '../diagnostic.dart' as diag; + +const _desc = r'Use `;` instead of `{}` for empty container bodies.'; + +class EmptyContainerBodies extends AnalysisRule { + EmptyContainerBodies() + : super(name: LintNames.empty_container_bodies, description: _desc); + + @override + DiagnosticCode get diagnosticCode => diag.emptyContainerBodies; + + @override + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, + ) { + if (!context.isFeatureEnabled(Feature.primary_constructors)) return; + var visitor = _Visitor(this); + registry.addBlockClassBody(this, visitor); + } +} + +class _Visitor extends SimpleAstVisitor { + final AnalysisRule rule; + + _Visitor(this.rule); + + @override + void visitBlockClassBody(BlockClassBody node) { + var leftBracket = node.leftBracket; + var rightBracket = node.rightBracket; + if (leftBracket.next == rightBracket && + rightBracket.precedingComments == null) { + var kind = switch (node.parent) { + ClassDeclaration() => 'class', + MixinDeclaration() => 'mixin', + ExtensionDeclaration() => 'extension', + ExtensionTypeDeclaration() => 'extension type', + // This should never happen. + _ => 'container', + }; + var offset = leftBracket.offset; + rule.reportAtSourceRange( + SourceRange(offset, rightBracket.end - offset), + arguments: [kind], + ); + } + } +} diff --git a/pkg/linter/messages.yaml b/pkg/linter/messages.yaml index 444357d6a24..4e4d68bec87 100644 --- a/pkg/linter/messages.yaml +++ b/pkg/linter/messages.yaml @@ -5239,6 +5239,18 @@ LinterLintCode: Point(this.x, this.y); } ``` + emptyContainerBodies: + type: lint + parameters: + String containerKind: The kind of the container, such as 'class' or 'enum'. + problemMessage: "Empty #containerKind bodies should be written using a ';' rather than '{}'." + correctionMessage: "Try replacing the #containerKind body with ';'." + state: + experimental: "3.13" + categories: [style] + hasPublishedDocs: false + deprecatedDetails: |- + Use `;` instead of `{}` for empty container (class, enum, etc.) bodies. emptyStatements: type: lint parameters: none diff --git a/pkg/linter/test/rules/all.dart b/pkg/linter/test/rules/all.dart index 3b70032ddeb..c4e97a5dfe3 100644 --- a/pkg/linter/test/rules/all.dart +++ b/pkg/linter/test/rules/all.dart @@ -114,6 +114,7 @@ import 'do_not_use_environment_test.dart' as do_not_use_environment; import 'document_ignores_test.dart' as document_ignores; import 'empty_catches_test.dart' as empty_catches; import 'empty_constructor_bodies_test.dart' as empty_constructor_bodies; +import 'empty_container_bodies_test.dart' as empty_container_bodies; import 'empty_statements_test.dart' as empty_statements; import 'eol_at_end_of_file_test.dart' as eol_at_end_of_file; import 'erase_dart_type_extension_types_test.dart' @@ -417,6 +418,7 @@ void main() { document_ignores.main(); empty_catches.main(); empty_constructor_bodies.main(); + empty_container_bodies.main(); empty_statements.main(); eol_at_end_of_file.main(); erase_dart_type_extension_types.main(); diff --git a/pkg/linter/test/rules/empty_container_bodies_test.dart b/pkg/linter/test/rules/empty_container_bodies_test.dart new file mode 100644 index 00000000000..50230c10df8 --- /dev/null +++ b/pkg/linter/test/rules/empty_container_bodies_test.dart @@ -0,0 +1,74 @@ +// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../rule_test_support.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(EmptyContainerBodiesTest); + }); +} + +@reflectiveTest +class EmptyContainerBodiesTest extends LintRuleTest { + @override + String get lintRule => LintNames.empty_container_bodies; + + test_class_notEmpty() async { + await assertNoDiagnostics(r''' +class C { + C(); +} +'''); + } + + test_class_onDifferentLines() async { + await assertDiagnosticsFromMarkdown(r''' +class C [!{ +}!] +'''); + } + + test_class_onSameLine() async { + await assertDiagnosticsFromMarkdown(r''' +class C [!{}!] +'''); + } + + test_class_withComment() async { + await assertNoDiagnostics(r''' +class C { + // eol +} +'''); + } + + test_class_withDocComment() async { + await assertNoDiagnostics(r''' +class C { + /// doc +} +'''); + } + + test_extension() async { + await assertDiagnosticsFromMarkdown(r''' +extension on String [!{}!] +'''); + } + + test_extensionType() async { + await assertDiagnosticsFromMarkdown(r''' +extension type E(String self) [!{}!] +'''); + } + + test_mixin() async { + await assertDiagnosticsFromMarkdown(r''' +mixin M [!{}!] +'''); + } +}