[messages] Convert assignability checks to use literate diagnostic API.

Converts the code that reports diagnostic codes `invalidAssignment`
and `argumentTypeNotAssignable` to use the literate diagnostic
reporting API.

This was challenging because some of the code for reporting these
diagnostics (specifically the method
`ErrorDetectionHelpers.checkForAssignableExpressionAtType`) was
parameterized by diagnostic code, but the two diagnostics accepted a
different number of parameters. To get the type safety of the literate
diagnostic API while preserving the existing behavior, I've created an
abstract base class, `NonAssignabilityReporter`, with a method
`createDiagnostic`, that encapsulates the differences in how to handle
the two diagnostic codes.

Change-Id: I6a6a69649e53f6980b5033771b14dbfdaa5895d6
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/467481
Commit-Queue: Paul Berry <paulberry@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
Paul Berry
2025-12-11 10:13:50 -08:00
committed by Commit Queue
parent 2c715e92b6
commit 758d3a8803
9 changed files with 196 additions and 123 deletions
@@ -155,14 +155,19 @@ class AssignmentExpressionResolver {
}
}
_diagnosticReporter.atNode(
right,
diag.invalidAssignment,
arguments: [rightType, writeType],
contextMessages: _resolver.computeWhyNotPromotedMessages(
right,
whyNotPromoted?.call(),
),
_diagnosticReporter.report(
diag.invalidAssignment
.withArguments(
actualStaticType: rightType,
expectedStaticType: writeType,
)
.withContextMessages(
_resolver.computeWhyNotPromotedMessages(
right,
whyNotPromoted?.call(),
),
)
.at(right),
);
}
@@ -83,10 +83,13 @@ class PostfixExpressionResolver {
operandWriteType,
strictCasts: _resolver.analysisOptions.strictCasts,
)) {
_resolver.diagnosticReporter.atNode(
node,
diag.invalidAssignment,
arguments: [type, operandWriteType],
_resolver.diagnosticReporter.report(
diag.invalidAssignment
.withArguments(
actualStaticType: type,
expectedStaticType: operandWriteType,
)
.at(node),
);
}
}
@@ -102,10 +102,13 @@ class PrefixExpressionResolver {
operandWriteType,
strictCasts: _resolver.analysisOptions.strictCasts,
)) {
_resolver.diagnosticReporter.atNode(
node,
diag.invalidAssignment,
arguments: [type, operandWriteType],
_resolver.diagnosticReporter.report(
diag.invalidAssignment
.withArguments(
actualStaticType: type,
expectedStaticType: operandWriteType,
)
.at(node),
);
}
}
@@ -9,6 +9,7 @@ import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/element/element.dart';
import 'package:analyzer/src/dart/element/type_schema.dart';
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
import 'package:analyzer/src/generated/error_detection_helpers.dart';
import 'package:analyzer/src/generated/resolver.dart';
/// Helper for resolving [VariableDeclaration]s.
@@ -84,7 +85,7 @@ class VariableDeclarationResolver {
initializer,
initializerType,
element.type,
diag.invalidAssignment,
const NonAssignabilityReporterForAssignment(),
whyNotPromoted: whyNotPromoted,
);
}
@@ -506,15 +506,15 @@ const DiagnosticWithoutArguments argumentMustBeNative =
);
/// Parameters:
/// Type p0: the name of the actual argument type
/// Type p1: the name of the expected type
/// String p2: additional information, if any, when problem is associated with
/// records
/// Type actualStaticType: the name of the actual argument type
/// Type expectedStaticType: the name of the expected type
/// String additionalInfo: additional information, if any, when problem is
/// associated with records
const DiagnosticWithArguments<
LocatableDiagnostic Function({
required DartType p0,
required DartType p1,
required String p2,
required DartType actualStaticType,
required DartType expectedStaticType,
required String additionalInfo,
})
>
argumentTypeNotAssignable = DiagnosticWithArguments(
@@ -7569,10 +7569,13 @@ invalidAnnotationTarget = DiagnosticWithArguments(
);
/// Parameters:
/// Type p0: the name of the right hand side type
/// Type p1: the name of the left hand side type
/// Type actualStaticType: the name of the right hand side type
/// Type expectedStaticType: the name of the left hand side type
const DiagnosticWithArguments<
LocatableDiagnostic Function({required DartType p0, required DartType p1})
LocatableDiagnostic Function({
required DartType actualStaticType,
required DartType expectedStaticType,
})
>
invalidAssignment = DiagnosticWithArguments(
name: 'invalid_assignment',
@@ -17799,11 +17802,15 @@ LocatableDiagnostic _withArgumentsArgumentMustBeAConstant({
}
LocatableDiagnostic _withArgumentsArgumentTypeNotAssignable({
required DartType p0,
required DartType p1,
required String p2,
required DartType actualStaticType,
required DartType expectedStaticType,
required String additionalInfo,
}) {
return LocatableDiagnosticImpl(diag.argumentTypeNotAssignable, [p0, p1, p2]);
return LocatableDiagnosticImpl(diag.argumentTypeNotAssignable, [
actualStaticType,
expectedStaticType,
additionalInfo,
]);
}
LocatableDiagnostic _withArgumentsArgumentTypeNotAssignableToErrorHandler({
@@ -19242,10 +19249,13 @@ LocatableDiagnostic _withArgumentsInvalidAnnotationTarget({
}
LocatableDiagnostic _withArgumentsInvalidAssignment({
required DartType p0,
required DartType p1,
required DartType actualStaticType,
required DartType expectedStaticType,
}) {
return LocatableDiagnosticImpl(diag.invalidAssignment, [p0, p1]);
return LocatableDiagnosticImpl(diag.invalidAssignment, [
actualStaticType,
expectedStaticType,
]);
}
LocatableDiagnostic _withArgumentsInvalidCastFunction({
@@ -12,7 +12,6 @@ import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/src/dart/ast/ast.dart';
import 'package:analyzer/src/dart/ast/extensions.dart';
import 'package:analyzer/src/dart/element/element.dart';
@@ -20,6 +19,7 @@ import 'package:analyzer/src/dart/element/inheritance_manager3.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/dart/element/type_system.dart';
import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag;
import 'package:analyzer/src/error/codes.dart';
import 'package:analyzer/src/error/listener.dart';
import 'package:analyzer/src/utilities/extensions/object.dart';
@@ -43,7 +43,7 @@ mixin ErrorDetectionHelpers {
Expression expression,
TypeImpl expectedStaticType,
TypeImpl actualStaticType,
DiagnosticCode diagnosticCode, {
NonAssignabilityReporter nonAssignabilityReporter, {
Map<SharedTypeView, NonPromotionReason> Function()? whyNotPromoted,
}) {
if (expectedStaticType is! VoidType &&
@@ -55,7 +55,7 @@ mixin ErrorDetectionHelpers {
expression,
actualStaticType,
expectedStaticType,
diagnosticCode,
nonAssignabilityReporter,
whyNotPromoted: whyNotPromoted,
);
}
@@ -103,7 +103,7 @@ mixin ErrorDetectionHelpers {
Expression expression,
TypeImpl actualStaticType,
TypeImpl expectedStaticType,
DiagnosticCode diagnosticCode, {
NonAssignabilityReporter nonAssignabilityReporter, {
Map<SharedTypeView, NonPromotionReason> Function()? whyNotPromoted,
}) {
if (expectedStaticType is! VoidType &&
@@ -142,64 +142,18 @@ mixin ErrorDetectionHelpers {
return;
}
}
if (diagnosticCode == diag.argumentTypeNotAssignable) {
var additionalInfo = <String>[];
if (expectedStaticType is RecordTypeImpl &&
actualStaticType is RecordTypeImpl) {
var actualPositionalFields = actualStaticType.positionalFields.length;
var expectedPositionalFields =
expectedStaticType.positionalFields.length;
if (expectedPositionalFields != 0 &&
actualPositionalFields != expectedPositionalFields) {
additionalInfo.add(
'Expected $expectedPositionalFields positional arguments, but got $actualPositionalFields instead.',
);
}
var actualNamedFieldsLength = actualStaticType.namedFields.length;
var expectedNamedFieldsLength = expectedStaticType.namedFields.length;
if (expectedNamedFieldsLength != 0 &&
actualNamedFieldsLength != expectedNamedFieldsLength) {
additionalInfo.add(
'Expected $expectedNamedFieldsLength named arguments, but got $actualNamedFieldsLength instead.',
);
}
var namedFields = expectedStaticType.namedFields;
if (namedFields.isNotEmpty) {
for (var field in actualStaticType.namedFields) {
if (!namedFields.any(
(element) =>
element.name == field.name && field.type == element.type,
)) {
additionalInfo.add(
'Unexpected named argument `${field.name}` with type `${field.type.getDisplayString()}`.',
);
}
}
}
}
diagnosticReporter.atNode(
getErrorNode(expression),
diagnosticCode,
arguments: [
actualStaticType,
expectedStaticType,
additionalInfo.join(' '),
],
contextMessages: computeWhyNotPromotedMessages(
expression,
whyNotPromoted?.call(),
),
);
return;
}
diagnosticReporter.atNode(
getErrorNode(expression),
diagnosticCode,
arguments: [actualStaticType, expectedStaticType],
contextMessages: computeWhyNotPromotedMessages(
expression,
whyNotPromoted?.call(),
),
var whyNotPromotedMessages = computeWhyNotPromotedMessages(
expression,
whyNotPromoted?.call(),
);
diagnosticReporter.report(
nonAssignabilityReporter
.createDiagnostic(
expectedStaticType: expectedStaticType,
actualStaticType: actualStaticType,
)
.withContextMessages(whyNotPromotedMessages)
.at(getErrorNode(expression)),
);
}
}
@@ -413,8 +367,89 @@ mixin ErrorDetectionHelpers {
argument,
staticParameterType,
argument.typeOrThrow,
diag.argumentTypeNotAssignable,
const NonAssignabilityReporterForArgument(),
whyNotPromoted: whyNotPromoted,
);
}
}
/// Abstract helper class for reporting an error related to non-assignability.
///
/// Concrete derived classes exist for handling the different varieties of
/// non-assignability errors.
abstract class NonAssignabilityReporter {
const NonAssignabilityReporter();
/// Creates the appropriate [LocatableDiagnostic] to report that the user has
/// tried to assign [actualStaticType] to [expectedStaticType].
LocatableDiagnostic createDiagnostic({
required TypeImpl expectedStaticType,
required TypeImpl actualStaticType,
});
}
/// Helper class for reporting the error [diag.argumentTypeNotAssignable].
class NonAssignabilityReporterForArgument extends NonAssignabilityReporter {
const NonAssignabilityReporterForArgument();
@override
LocatableDiagnostic createDiagnostic({
required TypeImpl expectedStaticType,
required TypeImpl actualStaticType,
}) {
var additionalInfo = <String>[];
if (expectedStaticType is RecordTypeImpl &&
actualStaticType is RecordTypeImpl) {
var actualPositionalFields = actualStaticType.positionalFields.length;
var expectedPositionalFields = expectedStaticType.positionalFields.length;
if (expectedPositionalFields != 0 &&
actualPositionalFields != expectedPositionalFields) {
additionalInfo.add(
'Expected $expectedPositionalFields positional arguments, but got $actualPositionalFields instead.',
);
}
var actualNamedFieldsLength = actualStaticType.namedFields.length;
var expectedNamedFieldsLength = expectedStaticType.namedFields.length;
if (expectedNamedFieldsLength != 0 &&
actualNamedFieldsLength != expectedNamedFieldsLength) {
additionalInfo.add(
'Expected $expectedNamedFieldsLength named arguments, but got $actualNamedFieldsLength instead.',
);
}
var namedFields = expectedStaticType.namedFields;
if (namedFields.isNotEmpty) {
for (var field in actualStaticType.namedFields) {
if (!namedFields.any(
(element) =>
element.name == field.name && field.type == element.type,
)) {
additionalInfo.add(
'Unexpected named argument `${field.name}` with type `${field.type.getDisplayString()}`.',
);
}
}
}
}
return diag.argumentTypeNotAssignable.withArguments(
actualStaticType: actualStaticType,
expectedStaticType: expectedStaticType,
additionalInfo: additionalInfo.join(' '),
);
}
}
/// Helper class for reporting the error [diag.invalidAssignment].
class NonAssignabilityReporterForAssignment extends NonAssignabilityReporter {
const NonAssignabilityReporterForAssignment();
@override
LocatableDiagnostic createDiagnostic({
required TypeImpl expectedStaticType,
required TypeImpl actualStaticType,
}) {
return diag.invalidAssignment.withArguments(
actualStaticType: actualStaticType,
expectedStaticType: expectedStaticType,
);
}
}
@@ -684,7 +684,7 @@ class ErrorVerifier extends RecursiveAstVisitor<void>
defaultValue,
defaultValue.typeOrThrow,
node.declaredFragment!.element.type,
diag.invalidAssignment,
const NonAssignabilityReporterForAssignment(),
);
}
@@ -3754,7 +3754,7 @@ class ErrorVerifier extends RecursiveAstVisitor<void>
argument,
parameterType,
_intType,
diag.argumentTypeNotAssignable,
const NonAssignabilityReporterForArgument(),
);
}
}
+8 -8
View File
@@ -1331,10 +1331,10 @@ CompileTimeErrorCode:
argumentTypeNotAssignable:
type: compileTimeError
parameters:
Type p0: the name of the actual argument type
Type p1: the name of the expected type
String p2: additional information, if any, when problem is associated with records
problemMessage: "The argument type '#p0' can't be assigned to the parameter type '#p1'. #p2"
Type actualStaticType: the name of the actual argument type
Type expectedStaticType: the name of the expected type
String additionalInfo: additional information, if any, when problem is associated with records
problemMessage: "The argument type '#actualStaticType' can't be assigned to the parameter type '#expectedStaticType'. #additionalInfo"
hasPublishedDocs: true
documentation: |-
#### Description
@@ -9287,10 +9287,10 @@ CompileTimeErrorCode:
invalidAssignment:
type: compileTimeError
parameters:
Type p0: the name of the right hand side type
Type p1: the name of the left hand side type
problemMessage: "A value of type '#p0' can't be assigned to a variable of type '#p1'."
correctionMessage: "Try changing the type of the variable, or casting the right-hand type to '#p1'."
Type actualStaticType: the name of the right hand side type
Type expectedStaticType: the name of the left hand side type
problemMessage: "A value of type '#actualStaticType' can't be assigned to a variable of type '#expectedStaticType'."
correctionMessage: "Try changing the type of the variable, or casting the right-hand type to '#expectedStaticType'."
hasPublishedDocs: true
documentation: |-
#### Description
@@ -85,10 +85,14 @@ main() {
firstType.element.firstFragment.libraryFragment.source,
);
reporter.atNode(
findNode.simple('x'),
diag.argumentTypeNotAssignable,
arguments: [firstType, secondType, ''],
reporter.report(
diag.argumentTypeNotAssignable
.withArguments(
actualStaticType: firstType,
expectedStaticType: secondType,
additionalInfo: '',
)
.at(findNode.simple('x')),
);
var diagnostic = listener.diagnostics[0];
@@ -126,10 +130,14 @@ main() {
listener,
firstType.element.firstFragment.libraryFragment.source,
);
reporter.atNode(
findNode.simple('x'),
diag.argumentTypeNotAssignable,
arguments: [firstType, secondType, ''],
reporter.report(
diag.argumentTypeNotAssignable
.withArguments(
actualStaticType: firstType,
expectedStaticType: secondType,
additionalInfo: '',
)
.at(findNode.simple('x')),
);
var diagnostic = listener.diagnostics[0];
@@ -155,10 +163,14 @@ main() {
var source = result.unit.declaredFragment!.source;
var reporter = DiagnosticReporter(listener, source);
reporter.atNode(
findNode.simple('x'),
diag.argumentTypeNotAssignable,
arguments: [fa.variables.type!.type!, fb.variables.type!.type!, ''],
reporter.report(
diag.argumentTypeNotAssignable
.withArguments(
actualStaticType: fa.variables.type!.type!,
expectedStaticType: fb.variables.type!.type!,
additionalInfo: '',
)
.at(findNode.simple('x')),
);
var diagnostic = listener.diagnostics[0];
@@ -186,10 +198,14 @@ main() {
var source = result.unit.declaredFragment!.source;
var reporter = DiagnosticReporter(listener, source);
reporter.atNode(
findNode.simple('x'),
diag.argumentTypeNotAssignable,
arguments: [ba.variables.type!.type!, bb.variables.type!.type!, ''],
reporter.report(
diag.argumentTypeNotAssignable
.withArguments(
actualStaticType: ba.variables.type!.type!,
expectedStaticType: bb.variables.type!.type!,
additionalInfo: '',
)
.at(findNode.simple('x')),
);
var diagnostic = listener.diagnostics[0];