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 <srawlins@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Brian Wilkerson
2026-05-08 13:55:19 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 77c65d60ec
commit f7c06eb180
9 changed files with 180 additions and 0 deletions
@@ -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:
+1
View File
@@ -72,6 +72,7 @@ linter:
- document_ignores
- empty_catches
- empty_constructor_bodies
- empty_container_bodies
- empty_statements
- eol_at_end_of_file
- exhaustive_cases
+22
View File
@@ -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]);
}
+2
View File
@@ -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';
+2
View File
@@ -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())
@@ -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<void> {
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],
);
}
}
}
+12
View File
@@ -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
+2
View File
@@ -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();
@@ -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 [!{}!]
''');
}
}