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:
committed by
dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent
77c65d60ec
commit
f7c06eb180
@@ -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:
|
||||
|
||||
@@ -72,6 +72,7 @@ linter:
|
||||
- document_ignores
|
||||
- empty_catches
|
||||
- empty_constructor_bodies
|
||||
- empty_container_bodies
|
||||
- empty_statements
|
||||
- eol_at_end_of_file
|
||||
- exhaustive_cases
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 [!{}!]
|
||||
''');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user