Add a fix when an extension or mixin has a primary constructor

Neither is allowed to have a primary constructor, This adds a fix to
remove the primary constructor.

Closes https://github.com/dart-lang/sdk/issues/63073

Change-Id: I363d2fa02d18e2a63a2bfc8c64a57256f861195d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/500584
Reviewed-by: Keerti Parthasarathy <keertip@google.com>
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Brian Wilkerson
2026-05-05 09:18:55 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 00cda4d60f
commit e1cacdc23a
4 changed files with 117 additions and 2 deletions
@@ -26,6 +26,15 @@ class RemoveConstructor extends ResolvedCorrectionProducer {
Future<void> compute(ChangeBuilder builder) async {
var container = _findContainer();
if (container == null) {
// If there's no container, then it must be a primary constructor.
var primary = _findPrimaryConstructor();
if (primary != null) {
await builder.addDartFileEdit(file, (builder) {
builder.addDeletion(
range.startEnd(primary.leftParen, primary.rightParen),
);
});
}
return;
}
@@ -83,6 +92,40 @@ class RemoveConstructor extends ResolvedCorrectionProducer {
}
return null;
}
_PrimaryConstructor? _findPrimaryConstructor() {
switch (node) {
case ExtensionDeclaration extension:
var leftParen =
extension.typeParameters?.endToken.next ?? extension.name?.next;
var rightParen = extension.onClause?.onKeyword.previous;
if (leftParen != null &&
leftParen.type == TokenType.OPEN_PAREN &&
rightParen != null &&
rightParen.type == TokenType.CLOSE_PAREN) {
return _PrimaryConstructor(
leftParen: leftParen,
rightParen: rightParen,
);
}
case MixinDeclaration mixin:
var leftParen = mixin.typeParameters?.endToken.next ?? mixin.name.next;
var rightParen =
mixin.onClause?.onKeyword.previous ??
mixin.implementsClause?.implementsKeyword.previous ??
mixin.body.beginToken.previous;
if (leftParen != null &&
leftParen.type == TokenType.OPEN_PAREN &&
rightParen != null &&
rightParen.type == TokenType.CLOSE_PAREN) {
return _PrimaryConstructor(
leftParen: leftParen,
rightParen: rightParen,
);
}
}
return null;
}
}
class _Container {
@@ -91,3 +134,10 @@ class _Container {
_Container({required this.leftBracket, required this.members});
}
class _PrimaryConstructor {
final Token leftParen;
final Token rightParen;
_PrimaryConstructor({required this.leftParen, required this.rightParen});
}
@@ -715,7 +715,7 @@ extension_override_with_cascade:
extension_override_without_access:
status: noFix
extension_primary_constructor:
status: needsEvaluation
status: hasFix
extension_type_constructor_with_super_formal_parameter:
status: needsFix
notes: |-
@@ -1141,7 +1141,7 @@ mixin_of_type_alias_expands_to_type_parameter:
mixin_on_type_alias_expands_to_type_parameter:
status: noFix
mixin_primary_constructor:
status: needsEvaluation
status: hasFix
mixin_subtype_of_base_is_not_base:
status: hasFix
mixin_subtype_of_final_is_not_base:
@@ -572,6 +572,7 @@ final _builtInNonLintGenerators = <DiagnosticCode, List<ProducerGenerator>>{
diag.extensionTypeDeclaresInstanceField: [ConvertIntoGetter.new],
diag.extensionOverrideAccessToStaticMember: [ReplaceWithExtensionName.new],
diag.extensionOverrideWithCascade: [ReplaceCascadeWithDot.new],
diag.extensionPrimaryConstructor: [RemoveConstructor.new],
diag.extensionTypeWithAbstractMember: [ConvertIntoBlockBody.missingBody],
diag.extraPositionalArguments: [CreateConstructor.new],
diag.extraPositionalArgumentsCouldBeNamed: [
@@ -659,6 +660,7 @@ final _builtInNonLintGenerators = <DiagnosticCode, List<ProducerGenerator>>{
diag.missingRequiredArgument: [AddMissingRequiredArgument.new],
diag.mixinApplicationNotImplementedInterface: [ExtendClassForMixin.new],
diag.mixinClassDeclarationExtendsNotObject: [RemoveExtendsClause.new],
diag.mixinPrimaryConstructor: [RemoveConstructor.new],
diag.mixinSubtypeOfBaseIsNotBase: [AddClassModifier.baseModifier],
diag.mixinSubtypeOfFinalIsNotBase: [AddClassModifier.baseModifier],
diag.mixinOfDisallowedClass: [RemoveNameFromDeclarationClause.new],
@@ -103,6 +103,24 @@ extension E on int {
await assertHasFix('''
extension E on int {
}
''');
}
Future<void> test_primaryConstructor() async {
await resolveTestCode('''
extension E() on int;
''');
await assertHasFix('''
extension E on int;
''');
}
Future<void> test_primaryConstructor_withTypeParameters() async {
await resolveTestCode('''
extension E<T>() on int;
''');
await assertHasFix('''
extension E<T> on int;
''');
}
}
@@ -195,6 +213,51 @@ mixin M {
await assertHasFix('''
mixin M {
}
''');
}
Future<void> test_primaryConstructor_emptyBody() async {
await resolveTestCode('''
mixin M() {}
''');
await assertHasFix('''
mixin M {}
''');
}
Future<void> test_primaryConstructor_implements() async {
await resolveTestCode('''
mixin M() implements Object {}
''');
await assertHasFix('''
mixin M implements Object {}
''');
}
Future<void> test_primaryConstructor_semicolon() async {
await resolveTestCode('''
mixin M();
''');
await assertHasFix('''
mixin M;
''');
}
Future<void> test_primaryConstructor_withOnClause() async {
await resolveTestCode('''
mixin M() on Object {}
''');
await assertHasFix('''
mixin M on Object {}
''');
}
Future<void> test_primaryConstructor_withTypeParameters() async {
await resolveTestCode('''
mixin M<T>() {}
''');
await assertHasFix('''
mixin M<T> {}
''');
}
}