analyzer: Write out qualified extension names in error messages

Fixes https://github.com/dart-lang/sdk/issues/56269

Change-Id: I025966fd4aa3d7c5b71175321f21f95e8c41f086
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/388580
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Samuel Rawlins <srawlins@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
Sam Rawlins
2024-10-07 18:01:54 +00:00
committed by Commit Queue
parent 07fd58715c
commit 10c1d883df
10 changed files with 269 additions and 151 deletions
@@ -151,7 +151,9 @@ CompileTimeErrorCode.AMBIGUOUS_EXPORT:
status: needsFix
notes: |-
For each exported name, add a fix to hide the name.
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS:
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_THREE_OR_MORE:
status: hasFix
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO:
status: hasFix
CompileTimeErrorCode.AMBIGUOUS_IMPORT:
status: needsFix
@@ -747,7 +747,10 @@ final _builtInLintProducers = <LintCode, List<ProducerGenerator>>{
};
final _builtInNonLintMultiProducers = {
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS: [
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO: [
AddExtensionOverride.new,
],
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_THREE_OR_MORE: [
AddExtensionOverride.new,
],
CompileTimeErrorCode.ARGUMENT_TYPE_NOT_ASSIGNABLE: [
@@ -44,7 +44,7 @@ f() {
}
''', expectedNumberOfFixesForKind: 1, errorFilter: (error) {
return error.errorCode ==
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS;
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO;
});
}
+138 -105
View File
@@ -11,8 +11,10 @@ import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/source/source.dart';
import 'package:analyzer/src/dart/element/extensions.dart';
import 'package:analyzer/src/dart/element/type.dart';
import 'package:analyzer/src/diagnostic/diagnostic.dart';
import 'package:analyzer/src/utilities/extensions/collection.dart';
import 'package:meta/meta.dart';
import 'package:source_span/source_span.dart';
@@ -168,7 +170,19 @@ class ErrorReporter {
return;
}
_convertElements(arguments);
if (arguments != null) {
var invalid = arguments
.whereNotType<String>()
.whereNotType<DartType>()
.whereNotType<Element>()
.whereNotType<int>()
.whereNotType<Uri>();
if (invalid.isNotEmpty) {
throw ArgumentError('Tried to format an error using '
'${invalid.map((e) => e.runtimeType).join(', ')}');
}
}
contextMessages ??= [];
contextMessages.addAll(_convertTypeNames(arguments));
_errorListener.onError(
@@ -334,93 +348,77 @@ class ErrorReporter {
);
}
/// Convert all [Element]s in the [arguments] into their display strings.
void _convertElements(List<Object>? arguments) {
if (arguments == null) {
return;
}
for (var i = 0; i < arguments.length; i++) {
var argument = arguments[i];
if (argument is Element) {
arguments[i] = argument.getDisplayString();
} else if (!(argument is String ||
argument is DartType ||
argument is int ||
argument is Uri)) {
throw ArgumentError(
'Tried to format an error using ${argument.runtimeType}');
}
}
}
/// Given an array of [arguments] that is expected to contain two or more
/// types, convert the types into strings by using the display names of the
/// types, unless there are two or more types with the same names, in which
/// case the extended display names of the types will be used in order to
/// Given an array of [arguments] that may contain [DartType]s and [Element]s,
/// converts the types and elements into strings by using the display names of
/// each, unless there are two or more types or elements with the same display
/// names, in which case the extended display names will be used in order to
/// clarify the message.
List<DiagnosticMessage> _convertTypeNames(List<Object?>? arguments) {
var messages = <DiagnosticMessage>[];
if (arguments == null) {
return messages;
return const [];
}
Map<String, List<_TypeToConvert>> typeGroups = {};
for (int i = 0; i < arguments.length; i++) {
var typeGroups = <String, List<_ToConvert>>{};
for (var i = 0; i < arguments.length; i++) {
var argument = arguments[i];
if (argument is TypeImpl) {
String displayName = argument.getDisplayString(
preferTypeAlias: true,
);
List<_TypeToConvert> types =
typeGroups.putIfAbsent(displayName, () => <_TypeToConvert>[]);
var displayName = argument.getDisplayString(preferTypeAlias: true);
var types = typeGroups.putIfAbsent(displayName, () => []);
types.add(_TypeToConvert(i, argument, displayName));
} else if (argument is Element) {
var displayName = argument.getDisplayString();
var types = typeGroups.putIfAbsent(displayName, () => []);
types.add(_ElementToConvert(i, argument, displayName));
}
}
for (List<_TypeToConvert> typeGroup in typeGroups.values) {
if (typeGroup.length == 1) {
_TypeToConvert typeToConvert = typeGroup[0];
arguments[typeToConvert.index] = typeToConvert.displayName;
} else {
Map<String, Set<Element>> nameToElementMap = {};
for (_TypeToConvert typeToConvert in typeGroup) {
for (Element element in typeToConvert.allElements()) {
Set<Element> elements =
nameToElementMap.putIfAbsent(element.name!, () => <Element>{});
elements.add(element);
}
}
for (_TypeToConvert typeToConvert in typeGroup) {
// TODO(brianwilkerson): When clients do a better job of displaying
// context messages, remove the extra text added to the buffer.
StringBuffer? buffer;
for (Element element in typeToConvert.allElements()) {
String name = element.name!;
if (nameToElementMap[name]!.length > 1) {
if (buffer == null) {
buffer = StringBuffer();
buffer.write('where ');
} else {
buffer.write(', ');
}
buffer.write('$name is defined in ${element.source!.fullName}');
}
messages.add(DiagnosticMessageImpl(
filePath: element.source!.fullName,
length: element.nameLength,
message: '$name is defined in ${element.source!.fullName}',
offset: element.nameOffset,
url: null));
}
if (buffer != null) {
arguments[typeToConvert.index] =
'${typeToConvert.displayName} ($buffer)';
} else {
arguments[typeToConvert.index] = typeToConvert.displayName;
}
var messages = <DiagnosticMessage>[];
for (var typeGroup in typeGroups.values) {
if (typeGroup.length == 1) {
var typeToConvert = typeGroup[0];
// If the display name of a type is unambiguous, just replace the type
// in the arguments list with its display name.
arguments[typeToConvert.index] = typeToConvert.displayName;
continue;
}
var nameToElementMap = <String, Set<Element>>{};
for (var typeToConvert in typeGroup) {
for (var element in typeToConvert.allElements) {
var elements = nameToElementMap.putIfAbsent(element.name!, () => {});
elements.add(element);
}
}
for (var typeToConvert in typeGroup) {
// TODO(brianwilkerson): When clients do a better job of displaying
// context messages, remove the extra text added to the buffer.
StringBuffer? buffer;
for (var element in typeToConvert.allElements) {
var name = element.name!;
var sourcePath = element.source!.fullName;
if (nameToElementMap[name]!.length > 1) {
if (buffer == null) {
buffer = StringBuffer();
buffer.write('where ');
} else {
buffer.write(', ');
}
buffer.write('$name is defined in $sourcePath');
}
messages.add(DiagnosticMessageImpl(
filePath: element.source!.fullName,
length: element.nameLength,
message: '$name is defined in $sourcePath',
offset: element.nameOffset,
url: null,
));
}
arguments[typeToConvert.index] = buffer != null
? '${typeToConvert.displayName} ($buffer)'
: typeToConvert.displayName;
}
}
return messages;
}
@@ -453,6 +451,22 @@ class RecordingErrorListener implements AnalysisErrorListener {
}
}
/// Used by [ErrorReporter._convertTypeNames] to keep track of an error argument
/// that is an [Element], that is being converted to a display string.
class _ElementToConvert implements _ToConvert {
@override
final int index;
@override
final String displayName;
@override
final Iterable<Element> allElements;
_ElementToConvert(this.index, Element element, this.displayName)
: allElements = [element];
}
/// An [AnalysisErrorListener] that ignores error.
class _NullErrorListener implements AnalysisErrorListener {
@override
@@ -461,42 +475,61 @@ class _NullErrorListener implements AnalysisErrorListener {
}
}
/// Used by `ErrorReporter._convertTypeNames` to keep track of a type that is
/// being converted.
class _TypeToConvert {
/// Used by [ErrorReporter._convertTypeNames] to keep track of an argument that
/// is being converted to a display string.
abstract class _ToConvert {
/// A list of all elements involved in the [DartType] or [Element]'s display
/// string.
Iterable<Element> get allElements;
/// The argument's display string, to replace the argument in the argument
/// list.
String get displayName;
/// The index of the argument in the argument list.
int get index;
}
/// Used by [ErrorReporter._convertTypeNames] to keep track of an error argument
/// that is a [DartType], that is being converted to a display string.
class _TypeToConvert implements _ToConvert {
@override
final int index;
final DartType type;
final DartType _type;
@override
final String displayName;
List<Element>? _allElements;
@override
late final Iterable<Element> allElements = () {
var elements = <Element>{};
_TypeToConvert(this.index, this.type, this.displayName);
List<Element> allElements() {
if (_allElements == null) {
Set<Element> elements = <Element>{};
void addElementsFrom(DartType type) {
if (type is FunctionType) {
addElementsFrom(type.returnType);
for (ParameterElement parameter in type.parameters) {
addElementsFrom(parameter.type);
}
} else if (type is InterfaceType) {
if (elements.add(type.element)) {
for (DartType typeArgument in type.typeArguments) {
addElementsFrom(typeArgument);
}
void addElementsFrom(DartType type) {
if (type is FunctionType) {
addElementsFrom(type.returnType);
for (var parameter in type.parameters) {
addElementsFrom(parameter.type);
}
} else if (type is RecordType) {
for (var parameter in type.fields) {
addElementsFrom(parameter.type);
}
} else if (type is InterfaceType) {
if (elements.add(type.element)) {
for (var typeArgument in type.typeArguments) {
addElementsFrom(typeArgument);
}
}
}
addElementsFrom(type);
_allElements = elements.where((element) {
var name = element.name;
return name != null && name.isNotEmpty;
}).toList();
}
return _allElements!;
}
addElementsFrom(_type);
return elements.where((element) {
var name = element.name;
return name != null && name.isNotEmpty;
});
}();
_TypeToConvert(this.index, this._type, this.displayName);
}
@@ -117,21 +117,33 @@ class ExtensionMemberResolver {
}
// The most specific extension is ambiguous.
_errorReporter.atEntity(
nameEntity,
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS,
arguments: [
name.name,
mostSpecific.map((e) {
var name = e.extension.name;
if (name != null) {
return "extension '$name'";
}
var type = e.extension.extendedType.getDisplayString();
return "unnamed extension on '$type'";
}).commaSeparatedWithAnd,
],
);
if (mostSpecific.length == 2) {
_errorReporter.atEntity(
nameEntity,
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO,
arguments: [
name.name,
mostSpecific[0].extension,
mostSpecific[1].extension,
],
);
} else {
_errorReporter.atEntity(
nameEntity,
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_THREE_OR_MORE,
arguments: [
name.name,
mostSpecific.map((e) {
var name = e.extension.name;
if (name != null) {
return "extension '$name'";
}
var type = e.extension.extendedType.getDisplayString();
return "unnamed extension on '$type'";
}).commaSeparatedWithAnd,
],
);
}
return ResolutionResult.ambiguous;
}
+19 -2
View File
@@ -70,14 +70,31 @@ class CompileTimeErrorCode extends AnalyzerErrorCode {
/// Parameters:
/// 0: the name of the member
/// 1: the names of the declaring extensions
static const CompileTimeErrorCode AMBIGUOUS_EXTENSION_MEMBER_ACCESS =
CompileTimeErrorCode(
static const CompileTimeErrorCode
AMBIGUOUS_EXTENSION_MEMBER_ACCESS_THREE_OR_MORE = CompileTimeErrorCode(
'AMBIGUOUS_EXTENSION_MEMBER_ACCESS',
"A member named '{0}' is defined in {1}, and none are more specific.",
correctionMessage:
"Try using an extension override to specify the extension you want to "
"be chosen.",
hasPublishedDocs: true,
uniqueName: 'AMBIGUOUS_EXTENSION_MEMBER_ACCESS_THREE_OR_MORE',
);
/// Parameters:
/// 0: the name of the member
/// 1: the name of the first declaring extension
/// 2: the names of the second declaring extension
static const CompileTimeErrorCode AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO =
CompileTimeErrorCode(
'AMBIGUOUS_EXTENSION_MEMBER_ACCESS',
"A member named '{0}' is defined in '{1}' and '{2}', and neither is more "
"specific.",
correctionMessage:
"Try using an extension override to specify the extension you want to "
"be chosen.",
hasPublishedDocs: true,
uniqueName: 'AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO',
);
/// Parameters:
@@ -49,7 +49,8 @@ const List<ErrorCode> errorCodeValues = [
CompileTimeErrorCode.ABSTRACT_FIELD_INITIALIZER,
CompileTimeErrorCode.ABSTRACT_SUPER_MEMBER_REFERENCE,
CompileTimeErrorCode.AMBIGUOUS_EXPORT,
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS,
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_THREE_OR_MORE,
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO,
CompileTimeErrorCode.AMBIGUOUS_IMPORT,
CompileTimeErrorCode.AMBIGUOUS_SET_OR_MAP_LITERAL_BOTH,
CompileTimeErrorCode.AMBIGUOUS_SET_OR_MAP_LITERAL_EITHER,
+14 -3
View File
@@ -408,14 +408,16 @@ CompileTimeErrorCode:
export 'a.dart';
export 'b.dart' hide C;
```
AMBIGUOUS_EXTENSION_MEMBER_ACCESS:
problemMessage: "A member named '{0}' is defined in {1}, and none are more specific."
AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO:
sharedName: AMBIGUOUS_EXTENSION_MEMBER_ACCESS
problemMessage: "A member named '{0}' is defined in '{1}' and '{2}', and neither is more specific."
correctionMessage: Try using an extension override to specify the extension you want to be chosen.
hasPublishedDocs: true
comment: |-
Parameters:
0: the name of the member
1: the names of the declaring extensions
1: the name of the first declaring extension
2: the names of the second declaring extension
documentation: |-
#### Description
@@ -471,6 +473,15 @@ CompileTimeErrorCode:
print(E2(s).charCount);
}
```
AMBIGUOUS_EXTENSION_MEMBER_ACCESS_THREE_OR_MORE:
sharedName: AMBIGUOUS_EXTENSION_MEMBER_ACCESS
problemMessage: "A member named '{0}' is defined in {1}, and none are more specific."
correctionMessage: Try using an extension override to specify the extension you want to be chosen.
hasPublishedDocs: true
comment: |-
Parameters:
0: the name of the member
1: the names of the declaring extensions
AMBIGUOUS_IMPORT:
problemMessage: "The name '{0}' is defined in the libraries {1}."
correctionMessage: "Try using 'as prefix' for one of the import directives, or hiding the name from all but one of the imports."
@@ -29,7 +29,7 @@ extension E2 on A {
int f(A a) => a();
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 110, 1),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 110, 1),
]);
}
@@ -47,7 +47,7 @@ f() {
0.a;
}
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 98, 1),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 98, 1),
]);
var node = findNode.propertyAccess('0.a');
@@ -111,7 +111,7 @@ f() {
0.a;
}
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 91, 1),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 91, 1),
]);
var node = findNode.propertyAccess('0.a');
@@ -144,7 +144,7 @@ f() {
0.a;
}
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 96, 1),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 96, 1),
]);
var node = findNode.propertyAccess('0.a');
@@ -172,8 +172,38 @@ void f() {
0.foo();
}
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 129, 3,
messageContains: ["in extension 'E1' and extension 'E2',"]),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 129, 3,
messageContains: [
"in 'extension E1 on int' and 'extension E2 on int',",
]),
]);
}
test_method_conflict_conflict_notSpecific_sameName() async {
var one = newFile('$testPackageLibPath/one.dart', '''
extension E on int { void foo() {} }
''');
var two = newFile('$testPackageLibPath/two.dart', '''
extension E on int { void foo() {} }
''');
await assertErrorsInCode('''
// ignore_for_file: unused_import
import 'one.dart';
import 'two.dart';
void f() {
0.foo();
}
''', [
error(
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO,
87,
3,
messageContains: [
"'extension E on int (where E is defined in ${one.path})' and "
"'extension E on int (where E is defined in ${two.path})',",
],
contextMessages: [message(one, 10, 1), message(two, 10, 1)],
),
]);
}
@@ -197,8 +227,10 @@ void f() {
0.foo();
}
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 129, 3,
messageContains: ["in extension 'E1' and extension 'E2',"]),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 129, 3,
messageContains: [
"in 'extension E1 on int' and 'extension E2 on int',",
]),
]);
}
@@ -227,7 +259,7 @@ f() {
0.a();
}
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 88, 1),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 88, 1),
]);
var node = findNode.methodInvocation('0.a()');
@@ -259,8 +291,10 @@ void f() {
0.foo();
}
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 129, 3,
messageContains: ["in extension 'E1' and extension 'E2',"]),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 129, 3,
messageContains: [
"in 'extension E1 on int' and 'extension E2 on int',"
]),
]);
}
@@ -274,7 +308,10 @@ void f() {
0.foo();
}
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 167, 3,
error(
CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_THREE_OR_MORE,
167,
3,
messageContains: [
"in extension 'E1', extension 'E2', and extension 'E3',"
]),
@@ -313,7 +350,7 @@ f(SubTarget<num> t) {
t.foo;
}
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 396, 3),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 396, 3),
]);
}
@@ -331,7 +368,7 @@ extension E2 on A {
A f(A a) => a + a;
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 122, 5),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 122, 5),
]);
}
@@ -351,7 +388,7 @@ void f(A a) {
a += 0;
}
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 130, 2),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 130, 2),
]);
}
@@ -369,7 +406,7 @@ extension E2 on A {
int f(A a) => a[0];
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 134, 1),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 134, 1),
]);
}
@@ -387,7 +424,7 @@ f() {
0[1] += 2;
}
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 136, 1),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 136, 1),
]);
}
@@ -405,7 +442,7 @@ extension E2 on A {
int f(A a) => -a;
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 123, 1),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 123, 1),
]);
}
@@ -423,7 +460,7 @@ f() {
0.a = 3;
}
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 88, 1),
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 88, 1),
]);
assertResolvedNodeText(findNode.assignment('= 3'), r'''
@@ -476,10 +513,9 @@ int f(List<C> x) => x();
int g(List<A> x) => x();
int h(List<B> x) => x();
''', [
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS, 167, 1,
error(CompileTimeErrorCode.AMBIGUOUS_EXTENSION_MEMBER_ACCESS_TWO, 167, 1,
messageContains: [
"in unnamed extension on 'List<A>' and unnamed extension on "
"'List<B>',"
"'extension on List<A>' and 'extension on List<B>',"
]),
]);
}
@@ -430,6 +430,9 @@ export 'b.dart' hide C;
### ambiguous_extension_member_access
_A member named '{0}' is defined in '{1}' and '{2}', and neither is more
specific._
_A member named '{0}' is defined in {1}, and none are more specific._
#### Description