Augment. Report augmentationReturnTypeMismatch.

Add a compile-time diagnostic for augmentations that declare an explicit
return type different from the introductory declaration. Check top-level
functions, methods, and getters by comparing the augmentation annotation
against the introductory element return type using normal type equality.

Keep executable element return types initialized from the first fragment
only, so later augmentation fragments cannot overwrite the introductory
signature before validation. This also keeps synthetic getter/setter
variables based on the introductory declaration.

Change-Id: I08d55497e235ed2619a6915e0060bd5d8c8a45b6
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/502200
Reviewed-by: Samuel Rawlins <srawlins@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
Konstantin Shcheglov
2026-05-09 11:44:34 -07:00
committed by dart-scoped@luci-project-accounts.iam.gserviceaccount.com
parent 8028bd03fb
commit 6d6b2ae57f
10 changed files with 481 additions and 23 deletions
@@ -243,6 +243,8 @@ augmentation_of_different_declaration_kind:
status: noFix
augmentation_of_mixin_application_class:
status: needsEvaluation
augmentation_return_type_mismatch:
status: needsEvaluation
augmentation_type_parameter_bound:
status: noFix
augmentation_type_parameter_count:
@@ -956,6 +956,28 @@ const DiagnosticWithoutArguments augmentationOfMixinApplicationClass =
expectedTypes: [],
);
/// Parameters:
/// Type expectedType: the return type of the declaration
/// Type actualType: the return type of the augmentation
const DiagnosticWithArguments<
LocatableDiagnostic Function({
required DartType expectedType,
required DartType actualType,
})
>
augmentationReturnTypeMismatch = DiagnosticWithArguments(
name: 'augmentation_return_type_mismatch',
problemMessage:
"The augmentation's return type '{1}' must be the same as the introductory "
"declaration's return type '{0}'.",
correctionMessage:
"Try changing the augmentation's return type to match the declaration.",
type: DiagnosticType.COMPILE_TIME_ERROR,
uniqueName: 'augmentation_return_type_mismatch',
withArguments: _withArgumentsAugmentationReturnTypeMismatch,
expectedTypes: [ExpectedType.type, ExpectedType.type],
);
/// No parameters.
const DiagnosticWithoutArguments augmentationTypeParameterBound =
DiagnosticWithoutArgumentsImpl(
@@ -18279,6 +18301,16 @@ LocatableDiagnostic _withArgumentsAugmentationOfDifferentDeclarationKind({
]);
}
LocatableDiagnostic _withArgumentsAugmentationReturnTypeMismatch({
required DartType expectedType,
required DartType actualType,
}) {
return LocatableDiagnosticImpl(diag.augmentationReturnTypeMismatch, [
expectedType,
actualType,
]);
}
LocatableDiagnostic _withArgumentsAugmentedExpressionNotOperator({
required String operator,
}) {
@@ -74,6 +74,7 @@ const List<DiagnosticCode> diagnosticCodeValues = [
diag.augmentationModifierMissing,
diag.augmentationOfDifferentDeclarationKind,
diag.augmentationOfMixinApplicationClass,
diag.augmentationReturnTypeMismatch,
diag.augmentationTypeParameterBound,
diag.augmentationTypeParameterCount,
diag.augmentationTypeParameterName,
@@ -1136,6 +1136,10 @@ class ErrorVerifier extends RecursiveAstVisitor<void>
nameOrKeywordToken: node.name,
typeParameterList: node.functionExpression.typeParameters,
);
_checkForAugmentationReturnTypeMismatch(
fragment: fragment,
returnTypeNode: node.returnType,
);
if (element.enclosingElement is! LibraryElement) {
_hiddenElements!.declare(element);
@@ -1382,6 +1386,10 @@ class ErrorVerifier extends RecursiveAstVisitor<void>
nameOrKeywordToken: node.name,
typeParameterList: node.typeParameters,
);
_checkForAugmentationReturnTypeMismatch(
fragment: fragment,
returnTypeNode: node.returnType,
);
_withEnclosingExecutable(
element,
@@ -2555,6 +2563,29 @@ class ErrorVerifier extends RecursiveAstVisitor<void>
}
}
void _checkForAugmentationReturnTypeMismatch({
required ExecutableFragmentImpl fragment,
required TypeAnnotation? returnTypeNode,
}) {
if (!fragment.isAugmentation) {
return;
}
if (returnTypeNode == null) {
return;
}
var expectedType = fragment.element.returnType;
var actualType = returnTypeNode.typeOrThrow;
if (!typeSystem.isEqualTo(actualType, expectedType)) {
diagnosticReporter.report(
diag.augmentationReturnTypeMismatch
.withArguments(expectedType: expectedType, actualType: actualType)
.at(returnTypeNode),
);
}
}
void _checkForAugmentationTypeParameters({
required FragmentImpl fragment,
required List<TypeParameterFragmentImpl> firstTypeParameters,
@@ -281,21 +281,22 @@ class TypesBuilder {
}
void _functionDeclaration(FunctionDeclarationImpl node) {
var returnType = node.returnType?.type;
if (returnType == null) {
if (node.isSetter) {
returnType = _voidType;
} else {
returnType = _dynamicType;
}
}
var fragment = node.declaredFragment!;
var element = fragment.element;
if (fragment.previousFragment == null) {
var returnType = node.returnType?.type;
if (returnType == null) {
if (node.isSetter) {
returnType = _voidType;
} else {
returnType = _dynamicType;
}
}
element.returnType = returnType;
_setSyntheticVariableType(element);
}
_setSyntheticVariableType(element);
}
void _functionTypeAlias(FunctionTypeAliasImpl node) {
@@ -346,21 +347,24 @@ class TypesBuilder {
}
void _methodDeclaration(MethodDeclarationImpl node) {
var returnType = node.returnType?.type;
if (returnType == null) {
if (node.isSetter) {
returnType = _voidType;
} else if (node.isOperator && node.name.lexeme == '[]=') {
returnType = _voidType;
} else {
returnType = _dynamicType;
}
}
var fragment = node.declaredFragment!;
var element = fragment.element;
element.returnType = returnType;
_setSyntheticVariableType(element);
if (fragment.previousFragment == null) {
var returnType = node.returnType?.type;
if (returnType == null) {
if (node.isSetter) {
returnType = _voidType;
} else if (node.isOperator && node.name.lexeme == '[]=') {
returnType = _voidType;
} else {
returnType = _dynamicType;
}
}
element.returnType = returnType;
_setSyntheticVariableType(element);
}
}
void _mixinDeclaration(MixinDeclarationImpl node) {
+9
View File
@@ -1876,6 +1876,15 @@ CompileTimeErrorCode:
problemMessage: "Mixin application classes can't be augmented."
correctionMessage: Try removing the 'augment' keyword, or making the target a normal class.
hasPublishedDocs: false
augmentationReturnTypeMismatch:
type: compileTimeError
parameters:
Type expectedType: the return type of the declaration
Type actualType: the return type of the augmentation
experiment: augmentations
problemMessage: "The augmentation's return type '#actualType' must be the same as the introductory declaration's return type '#expectedType'."
correctionMessage: Try changing the augmentation's return type to match the declaration.
hasPublishedDocs: false
augmentedExpressionIsNotSetter:
type: compileTimeError
parameters: none
@@ -0,0 +1,248 @@
// 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/src/diagnostic/diagnostic.dart' as diag;
import 'package:test_reflective_loader/test_reflective_loader.dart';
import '../dart/resolution/context_collection_resolution.dart';
main() {
defineReflectiveSuite(() {
defineReflectiveTests(AugmentationReturnTypeMismatchTest);
});
}
@reflectiveTest
class AugmentationReturnTypeMismatchTest extends PubPackageResolutionTest {
test_class_getter_int_String() async {
await assertErrorsInCode(
r'''
class A {
int get foo => 0;
}
augment class A {
augment String get foo => '';
}
''',
[
error(diag.augmentationReturnTypeMismatch, 61, 6),
error(diag.returnOfInvalidTypeFromFunction, 79, 2),
],
);
}
test_class_method_void_int() async {
await assertErrorsInCode(
r'''
class A {
void foo() {}
}
augment class A {
augment int foo() => 0;
}
''',
[error(diag.augmentationReturnTypeMismatch, 57, 3)],
);
}
test_class_method_void_void() async {
await assertNoErrorsInCode(r'''
class A {
void foo() {}
}
augment class A {
augment void foo() {}
}
''');
}
test_extension_getter_int_String() async {
await assertErrorsInCode(
r'''
extension E on int {
int get foo => 0;
}
augment extension E {
augment String get foo => '';
}
''',
[
error(diag.augmentationReturnTypeMismatch, 76, 6),
error(diag.returnOfInvalidTypeFromFunction, 94, 2),
],
);
}
test_extension_method_void_int() async {
await assertErrorsInCode(
r'''
extension E on int {
void foo() {}
}
augment extension E {
augment int foo() => 0;
}
''',
[error(diag.augmentationReturnTypeMismatch, 72, 3)],
);
}
test_extensionType_getter_int_String() async {
await assertErrorsInCode(
r'''
extension type A(int it) {
int get foo => 0;
}
augment extension type A(int it) {
augment String get foo => '';
}
''',
[
error(diag.augmentationReturnTypeMismatch, 95, 6),
error(diag.returnOfInvalidTypeFromFunction, 113, 2),
],
);
}
test_extensionType_method_void_int() async {
await assertErrorsInCode(
r'''
extension type A(int it) {
void foo() {}
}
augment extension type A(int it) {
augment int foo() => 0;
}
''',
[error(diag.augmentationReturnTypeMismatch, 91, 3)],
);
}
test_mixin_getter_int_String() async {
await assertErrorsInCode(
r'''
mixin M {
int get foo => 0;
}
augment mixin M {
augment String get foo => '';
}
''',
[
error(diag.augmentationReturnTypeMismatch, 61, 6),
error(diag.returnOfInvalidTypeFromFunction, 79, 2),
],
);
}
test_mixin_method_void_int() async {
await assertErrorsInCode(
r'''
mixin M {
void foo() {}
}
augment mixin M {
augment int foo() => 0;
}
''',
[error(diag.augmentationReturnTypeMismatch, 57, 3)],
);
}
test_topLevelFunction_int_int_withImportPrefix() async {
await assertNoErrorsInCode(r'''
import 'dart:core';
import 'dart:core' as core;
int foo() => 0;
augment core.int foo() => 0;
''');
}
test_topLevelFunction_void_int() async {
await assertErrorsInCode(
r'''
void foo() {}
augment int foo() => 0;
''',
[error(diag.augmentationReturnTypeMismatch, 23, 3)],
);
}
test_topLevelFunction_void_int_viaTypeAlias() async {
await assertErrorsInCode(
r'''
typedef IntAlias = int;
void foo() {}
augment IntAlias foo() => 0;
''',
[error(diag.augmentationReturnTypeMismatch, 48, 8)],
);
}
test_topLevelFunction_void_int_withImportPrefix() async {
await assertErrorsInCode(
r'''
import 'dart:core' as core;
void foo() {}
augment core.int foo() => 0;
''',
[error(diag.augmentationReturnTypeMismatch, 51, 8)],
);
}
test_topLevelFunction_void_nothing() async {
await assertNoErrorsInCode(r'''
void foo() {}
augment foo() {}
''');
}
test_topLevelFunction_void_void() async {
await assertNoErrorsInCode(r'''
void foo() {}
augment void foo() {}
''');
}
test_topLevelFunction_void_void_viaTypeAlias() async {
await assertNoErrorsInCode(r'''
typedef VoidAlias = void;
void foo() {}
augment VoidAlias foo() {}
''');
}
test_topLevelGetter_int_String() async {
await assertErrorsInCode(
r'''
int get foo => 0;
augment String get foo => '';
''',
[
error(diag.augmentationReturnTypeMismatch, 27, 6),
error(diag.returnOfInvalidTypeFromFunction, 45, 2),
],
);
}
}
@@ -49,6 +49,8 @@ import 'augmentation_of_different_declaration_kind_test.dart'
as augmentation_of_different_declaration_kind;
import 'augmentation_of_mixin_application_class_test.dart'
as augmentation_of_mixin_application_class;
import 'augmentation_return_type_mismatch_test.dart'
as augmentation_return_type_mismatch;
import 'augmentation_type_parameter_bound_test.dart'
as augmentation_type_parameter_bound;
import 'augmentation_type_parameter_count_test.dart'
@@ -990,6 +992,7 @@ main() {
augmentation_modifier_missing.main();
augmentation_of_different_declaration_kind.main();
augmentation_of_mixin_application_class.main();
augmentation_return_type_mismatch.main();
augmentation_type_parameter_bound.main();
augmentation_type_parameter_count.main();
augmentation_type_parameter_name.main();
@@ -33153,6 +33153,61 @@ library
''');
}
test_getter_augmentation_chain_returnType_int_String() async {
var library = await buildLibrary(r'''
class A {
int get foo => 0;
}
augment class A {
augment String get foo => '';
}
''');
configuration.withConstructors = false;
checkElementText(library, r'''
library
reference: <testLibrary>
fragments
#F0 <testLibraryFragment>
element: <testLibrary>
classes
#F1 class A (nameOffset:6) (firstTokenOffset:0) (offset:6)
element: <testLibrary>::@class::A
nextFragment: #F2
fields
#F3 isOriginGetterSetter foo (nameOffset:<null>) (firstTokenOffset:<null>) (offset:6)
element: <testLibrary>::@class::A::@field::foo
getters
#F4 isCompleteDeclaration isOriginDeclaration foo (nameOffset:20) (firstTokenOffset:12) (offset:20)
element: <testLibrary>::@class::A::@getter::foo
nextFragment: #F5
#F2 isAugmentation class A (nameOffset:47) (firstTokenOffset:33) (offset:47)
element: <testLibrary>::@class::A
previousFragment: #F1
getters
#F5 isAugmentation isCompleteDeclaration isOriginDeclaration foo (nameOffset:72) (firstTokenOffset:53) (offset:72)
element: <testLibrary>::@class::A::@getter::foo
previousFragment: #F4
classes
isSimplyBounded class A
reference: <testLibrary>::@class::A
firstFragment: #F1
fields
isOriginGetterSetter foo
reference: <testLibrary>::@class::A::@field::foo
firstFragment: #F3
type: int
getter: <testLibrary>::@class::A::@getter::foo
getters
isOriginDeclaration foo
reference: <testLibrary>::@class::A::@getter::foo
firstFragment: #F4
returnType: int
variable: <testLibrary>::@class::A::@field::foo
''');
}
test_getter_augmentation_chain_twoInSameDeclaration() async {
var library = await buildLibrary(r'''
class A {
@@ -38878,6 +38933,51 @@ library
''');
}
test_method_augmentation_chain_returnType_void_int() async {
var library = await buildLibrary(r'''
class A {
void foo() {}
}
augment class A {
augment int foo() => 0;
}
''');
configuration.withConstructors = false;
checkElementText(library, r'''
library
reference: <testLibrary>
fragments
#F0 <testLibraryFragment>
element: <testLibrary>
classes
#F1 class A (nameOffset:6) (firstTokenOffset:0) (offset:6)
element: <testLibrary>::@class::A
nextFragment: #F2
methods
#F3 isCompleteDeclaration isOriginDeclaration foo (nameOffset:17) (firstTokenOffset:12) (offset:17)
element: <testLibrary>::@class::A::@method::foo
nextFragment: #F4
#F2 isAugmentation class A (nameOffset:43) (firstTokenOffset:29) (offset:43)
element: <testLibrary>::@class::A
previousFragment: #F1
methods
#F4 isAugmentation isCompleteDeclaration isOriginDeclaration foo (nameOffset:61) (firstTokenOffset:49) (offset:61)
element: <testLibrary>::@class::A::@method::foo
previousFragment: #F3
classes
isSimplyBounded class A
reference: <testLibrary>::@class::A
firstFragment: #F1
methods
isOriginDeclaration foo
reference: <testLibrary>::@class::A::@method::foo
firstFragment: #F3
returnType: void
''');
}
test_method_augmentation_chain_twoDeclarations() async {
var library = await buildLibrary(r'''
class A {
@@ -1863,6 +1863,34 @@ library
''');
}
test_function_augmentation_chain_returnType_void_int() async {
var library = await buildLibrary(r'''
void foo() {}
augment int foo() => 0;
''');
configuration.withConstructors = false;
checkElementText(library, r'''
library
reference: <testLibrary>
fragments
#F0 <testLibraryFragment>
element: <testLibrary>
functions
#F1 isCompleteDeclaration isOriginDeclaration isStatic foo (nameOffset:5) (firstTokenOffset:0) (offset:5)
element: <testLibrary>::@function::foo
nextFragment: #F2
#F2 isAugmentation isCompleteDeclaration isOriginDeclaration isStatic foo (nameOffset:26) (firstTokenOffset:14) (offset:26)
element: <testLibrary>::@function::foo
previousFragment: #F1
functions
isOriginDeclaration isStatic foo
reference: <testLibrary>::@function::foo
firstFragment: #F1
returnType: void
''');
}
test_function_augmentation_chain_typeParameters_count_111() async {
var library = await buildLibrary(r'''
void foo<T>() {}