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 [!{}!] +'''); + } +}