| // Copyright (c) 2019, 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 "dart:math" as math; |
| |
| import 'package:analyzer/dart/analysis/features.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'package:analyzer/diagnostic/diagnostic.dart'; |
| import 'package:analyzer/error/error.dart'; |
| import 'package:analyzer/error/listener.dart'; |
| import 'package:analyzer/src/dart/ast/extensions.dart'; |
| import 'package:analyzer/src/dart/element/type_algebra.dart'; |
| import 'package:analyzer/src/dart/element/type_system.dart'; |
| import 'package:analyzer/src/diagnostic/diagnostic.dart'; |
| import 'package:analyzer/src/error/codes.dart'; |
| import 'package:analyzer/src/generated/engine.dart' show AnalysisOptionsImpl; |
| |
| class TypeArgumentsVerifier { |
| final AnalysisOptionsImpl _options; |
| final LibraryElement _libraryElement; |
| final ErrorReporter _errorReporter; |
| |
| TypeArgumentsVerifier( |
| this._options, |
| this._libraryElement, |
| this._errorReporter, |
| ); |
| |
| TypeSystemImpl get _typeSystem => |
| _libraryElement.typeSystem as TypeSystemImpl; |
| |
| void checkConstructorReference(ConstructorReference node) { |
| var classElement = node.constructorName.type.name.staticElement; |
| List<TypeParameterElement> typeParameters; |
| if (classElement is TypeAliasElement) { |
| typeParameters = classElement.typeParameters; |
| } else if (classElement is ClassElement) { |
| typeParameters = classElement.typeParameters; |
| } else { |
| return; |
| } |
| |
| if (typeParameters.isEmpty) { |
| return; |
| } |
| var typeArgumentList = node.constructorName.type.typeArguments; |
| if (typeArgumentList == null) { |
| return; |
| } |
| var constructorType = node.staticType; |
| if (constructorType is DynamicType) { |
| // An erroneous constructor reference. |
| return; |
| } |
| if (constructorType is! FunctionType) { |
| return; |
| } |
| var typeArguments = [ |
| for (var type in typeArgumentList.arguments) type.type!, |
| ]; |
| if (typeArguments.length != typeParameters.length) { |
| // Wrong number of type arguments to be reported elsewhere. |
| return; |
| } |
| var typeArgumentListLength = typeArgumentList.arguments.length; |
| var substitution = Substitution.fromPairs(typeParameters, typeArguments); |
| for (var i = 0; i < typeArguments.length; i++) { |
| var typeParameter = typeParameters[i]; |
| var typeArgument = typeArguments[i]; |
| |
| var bound = typeParameter.bound; |
| if (bound == null) { |
| continue; |
| } |
| |
| bound = _libraryElement.toLegacyTypeIfOptOut(bound); |
| bound = substitution.substituteType(bound); |
| |
| if (!_typeSystem.isSubtypeOf(typeArgument, bound)) { |
| var errorNode = |
| i < typeArgumentListLength ? typeArgumentList.arguments[i] : node; |
| _errorReporter.reportErrorForNode( |
| CompileTimeErrorCode.TYPE_ARGUMENT_NOT_MATCHING_BOUNDS, |
| errorNode, |
| [typeArgument, typeParameter.name, bound], |
| ); |
| } |
| } |
| } |
| |
| void checkEnumConstantDeclaration(EnumConstantDeclaration node) { |
| var constructorElement = node.constructorElement; |
| if (constructorElement == null) { |
| return; |
| } |
| |
| var enumElement = constructorElement.enclosingElement; |
| var typeParameters = enumElement.typeParameters; |
| |
| var typeArgumentList = node.arguments?.typeArguments; |
| var typeArgumentNodes = typeArgumentList?.arguments; |
| if (typeArgumentList != null && |
| typeArgumentNodes != null && |
| typeArgumentNodes.length != typeParameters.length) { |
| _errorReporter.reportErrorForNode( |
| CompileTimeErrorCode.WRONG_NUMBER_OF_TYPE_ARGUMENTS_ENUM, |
| typeArgumentList, |
| [typeParameters.length, typeArgumentNodes.length], |
| ); |
| } |
| |
| if (typeParameters.isEmpty) { |
| return; |
| } |
| |
| // Check that type arguments are regular-bounded. |
| var typeArguments = constructorElement.returnType.typeArguments; |
| var substitution = Substitution.fromPairs(typeParameters, typeArguments); |
| for (var i = 0; i < typeArguments.length; i++) { |
| var typeParameter = typeParameters[i]; |
| var typeArgument = typeArguments[i]; |
| |
| var bound = typeParameter.bound; |
| if (bound == null) { |
| continue; |
| } |
| |
| bound = substitution.substituteType(bound); |
| |
| if (!_typeSystem.isSubtypeOf(typeArgument, bound)) { |
| _errorReporter.reportErrorForNode( |
| CompileTimeErrorCode.TYPE_ARGUMENT_NOT_MATCHING_BOUNDS, |
| typeArgumentNodes?[i] ?? node.name, |
| [typeArgument, typeParameter.name, bound], |
| ); |
| } |
| } |
| } |
| |
| void checkFunctionExpressionInvocation(FunctionExpressionInvocation node) { |
| _checkInvocationTypeArguments( |
| node.typeArguments?.arguments, |
| node.function.staticType, |
| node.staticInvokeType, |
| ); |
| _checkForImplicitDynamicInvoke(node); |
| } |
| |
| void checkFunctionReference(FunctionReference node) { |
| _checkInvocationTypeArguments( |
| node.typeArguments?.arguments, |
| node.function.staticType, |
| node.staticType, |
| ); |
| } |
| |
| void checkListLiteral(ListLiteral node) { |
| var typeArguments = node.typeArguments; |
| if (typeArguments != null) { |
| if (node.isConst) { |
| _checkTypeArgumentConst( |
| typeArguments.arguments, |
| CompileTimeErrorCode.INVALID_TYPE_ARGUMENT_IN_CONST_LIST, |
| ); |
| } |
| _checkTypeArgumentCount(typeArguments, 1, |
| CompileTimeErrorCode.EXPECTED_ONE_LIST_TYPE_ARGUMENTS); |
| } |
| _checkForImplicitDynamicTypedLiteral(node); |
| } |
| |
| void checkMapLiteral(SetOrMapLiteral node) { |
| var typeArguments = node.typeArguments; |
| if (typeArguments != null) { |
| if (node.isConst) { |
| _checkTypeArgumentConst( |
| typeArguments.arguments, |
| CompileTimeErrorCode.INVALID_TYPE_ARGUMENT_IN_CONST_MAP, |
| ); |
| } |
| _checkTypeArgumentCount(typeArguments, 2, |
| CompileTimeErrorCode.EXPECTED_TWO_MAP_TYPE_ARGUMENTS); |
| } |
| _checkForImplicitDynamicTypedLiteral(node); |
| } |
| |
| void checkMethodInvocation(MethodInvocation node) { |
| _checkInvocationTypeArguments( |
| node.typeArguments?.arguments, |
| node.function.staticType, |
| node.staticInvokeType, |
| ); |
| _checkForImplicitDynamicInvoke(node); |
| } |
| |
| void checkNamedType(NamedType node) { |
| _checkForTypeArgumentNotMatchingBounds(node); |
| var parent = node.parent; |
| if (parent is! ConstructorName || |
| parent.parent is! InstanceCreationExpression) { |
| _checkForRawTypeName(node); |
| } |
| } |
| |
| void checkSetLiteral(SetOrMapLiteral node) { |
| var typeArguments = node.typeArguments; |
| if (typeArguments != null) { |
| if (node.isConst) { |
| _checkTypeArgumentConst( |
| typeArguments.arguments, |
| CompileTimeErrorCode.INVALID_TYPE_ARGUMENT_IN_CONST_SET, |
| ); |
| } |
| _checkTypeArgumentCount(typeArguments, 1, |
| CompileTimeErrorCode.EXPECTED_ONE_SET_TYPE_ARGUMENTS); |
| } |
| _checkForImplicitDynamicTypedLiteral(node); |
| } |
| |
| void _checkForImplicitDynamicInvoke(InvocationExpression node) { |
| if (_options.implicitDynamic || node.typeArguments != null) { |
| return; |
| } |
| var invokeType = node.staticInvokeType; |
| var declaredType = node.function.staticType; |
| if (invokeType is FunctionType && |
| declaredType is FunctionType && |
| declaredType.typeFormals.isNotEmpty) { |
| List<DartType> typeArgs = node.typeArgumentTypes!; |
| if (typeArgs.any((t) => t.isDynamic)) { |
| // Issue an error depending on what we're trying to call. |
| Expression function = node.function; |
| if (function is Identifier) { |
| var element = function.staticElement; |
| if (element is MethodElement) { |
| _errorReporter.reportErrorForNode( |
| LanguageCode.IMPLICIT_DYNAMIC_METHOD, |
| node.function, |
| [element.displayName, element.typeParameters.join(', ')]); |
| return; |
| } |
| |
| if (element is FunctionElement) { |
| _errorReporter.reportErrorForNode( |
| LanguageCode.IMPLICIT_DYNAMIC_FUNCTION, |
| node.function, |
| [element.displayName, element.typeParameters.join(', ')]); |
| return; |
| } |
| } |
| |
| // The catch all case if neither of those matched. |
| // For example, invoking a function expression. |
| _errorReporter.reportErrorForNode(LanguageCode.IMPLICIT_DYNAMIC_INVOKE, |
| node.function, [declaredType]); |
| } |
| } |
| } |
| |
| void _checkForImplicitDynamicTypedLiteral(TypedLiteral node) { |
| if (_options.implicitDynamic || node.typeArguments != null) { |
| return; |
| } |
| DartType type = node.typeOrThrow; |
| // It's an error if either the key or value was inferred as dynamic. |
| if (type is InterfaceType && type.typeArguments.any((t) => t.isDynamic)) { |
| // TODO(brianwilkerson) Add StrongModeCode.IMPLICIT_DYNAMIC_SET_LITERAL |
| ErrorCode errorCode = node is ListLiteral |
| ? LanguageCode.IMPLICIT_DYNAMIC_LIST_LITERAL |
| : LanguageCode.IMPLICIT_DYNAMIC_MAP_LITERAL; |
| _errorReporter.reportErrorForNode(errorCode, node); |
| } |
| } |
| |
| /// Checks a type annotation for a raw generic type, and reports the |
| /// appropriate error if [AnalysisOptionsImpl.strictRawTypes] is set. |
| /// |
| /// This checks if [node] refers to a generic type and does not have explicit |
| /// or inferred type arguments. When that happens, it reports error code |
| /// [HintCode.STRICT_RAW_TYPE]. |
| void _checkForRawTypeName(NamedType node) { |
| AstNode parentEscapingTypeArguments(NamedType node) { |
| var parent = node.parent!; |
| while (parent is TypeArgumentList || parent is NamedType) { |
| if (parent.parent == null) { |
| return parent; |
| } |
| parent = parent.parent!; |
| } |
| return parent; |
| } |
| |
| if (!_options.strictRawTypes) return; |
| if (node.typeArguments != null) { |
| // Type has explicit type arguments. |
| return; |
| } |
| var type = node.typeOrThrow; |
| if (_isMissingTypeArguments(node, type, node.name.staticElement)) { |
| AstNode unwrappedParent = parentEscapingTypeArguments(node); |
| if (unwrappedParent is AsExpression || unwrappedParent is IsExpression) { |
| // Do not report a "Strict raw type" error in this case; too noisy. |
| // See https://github.com/dart-lang/language/blob/master/resources/type-system/strict-raw-types.md#conditions-for-a-raw-type-hint |
| } else { |
| _errorReporter |
| .reportErrorForNode(HintCode.STRICT_RAW_TYPE, node, [type]); |
| } |
| } |
| } |
| |
| /// Verify that the type arguments in the given [namedType] are all within |
| /// their bounds. |
| void _checkForTypeArgumentNotMatchingBounds(NamedType namedType) { |
| final type = namedType.type; |
| if (type == null) { |
| return; |
| } |
| |
| final List<TypeParameterElement> typeParameters; |
| final String elementName; |
| final List<DartType> typeArguments; |
| final alias = type.alias; |
| if (alias != null) { |
| elementName = alias.element.name; |
| typeParameters = alias.element.typeParameters; |
| typeArguments = alias.typeArguments; |
| } else if (type is InterfaceType) { |
| elementName = type.element.name; |
| typeParameters = type.element.typeParameters; |
| typeArguments = type.typeArguments; |
| } else { |
| return; |
| } |
| |
| if (typeParameters.isEmpty) { |
| return; |
| } |
| |
| // Check for regular-bounded. |
| List<_TypeArgumentIssue>? issues; |
| final substitution = Substitution.fromPairs(typeParameters, typeArguments); |
| for (var i = 0; i < typeArguments.length; i++) { |
| var typeParameter = typeParameters[i]; |
| var typeArgument = typeArguments[i]; |
| |
| if (typeArgument is FunctionType && typeArgument.typeFormals.isNotEmpty) { |
| if (!_libraryElement.featureSet.isEnabled(Feature.generic_metadata)) { |
| _errorReporter.reportErrorForNode( |
| CompileTimeErrorCode.GENERIC_FUNCTION_TYPE_CANNOT_BE_TYPE_ARGUMENT, |
| _typeArgumentErrorNode(namedType, i), |
| ); |
| continue; |
| } |
| } |
| |
| var bound = typeParameter.bound; |
| if (bound == null) { |
| continue; |
| } |
| |
| bound = _libraryElement.toLegacyTypeIfOptOut(bound); |
| bound = substitution.substituteType(bound); |
| |
| if (!_typeSystem.isSubtypeOf(typeArgument, bound)) { |
| issues ??= <_TypeArgumentIssue>[]; |
| issues.add( |
| _TypeArgumentIssue(i, typeParameter, bound, typeArgument), |
| ); |
| } |
| } |
| |
| // If regular-bounded, we are done. |
| if (issues == null) { |
| return; |
| } |
| |
| List<DiagnosticMessage>? buildContextMessages({ |
| List<DartType>? invertedTypeArguments, |
| }) { |
| final messages = <DiagnosticMessage>[]; |
| |
| void addMessage(String message) { |
| messages.add( |
| DiagnosticMessageImpl( |
| filePath: _errorReporter.source.fullName, |
| length: namedType.length, |
| message: message, |
| offset: namedType.offset, |
| url: null, |
| ), |
| ); |
| } |
| |
| String typeArgumentsToString(List<DartType> typeArguments) { |
| return typeArguments |
| .map((e) => e.getDisplayString(withNullability: true)) |
| .join(', '); |
| } |
| |
| if (namedType.typeArguments == null) { |
| var typeStr = '$elementName<${typeArgumentsToString(typeArguments)}>'; |
| addMessage( |
| "The raw type was instantiated as '$typeStr', " |
| "and is not regular-bounded.", |
| ); |
| } |
| |
| if (invertedTypeArguments != null) { |
| var invertedTypeStr = |
| '$elementName<${typeArgumentsToString(invertedTypeArguments)}>'; |
| addMessage( |
| "The inverted type '$invertedTypeStr' is also not regular-bounded, " |
| "so the type is not well-bounded.", |
| ); |
| } |
| |
| return messages.isNotEmpty ? messages : null; |
| } |
| |
| // If not allowed to be super-bounded, report issues. |
| if (!_shouldAllowSuperBoundedTypes(namedType)) { |
| for (var issue in issues) { |
| _errorReporter.reportErrorForNode( |
| CompileTimeErrorCode.TYPE_ARGUMENT_NOT_MATCHING_BOUNDS, |
| _typeArgumentErrorNode(namedType, issue.index), |
| [issue.argument, issue.parameter.name, issue.parameterBound], |
| buildContextMessages(), |
| ); |
| } |
| return; |
| } |
| |
| // Prepare type arguments for checking for super-bounded. |
| final invertedType = _typeSystem.replaceTopAndBottom(type); |
| final List<DartType> invertedTypeArguments; |
| final invertedAlias = invertedType.alias; |
| if (invertedAlias != null) { |
| invertedTypeArguments = invertedAlias.typeArguments; |
| } else if (invertedType is InterfaceType) { |
| invertedTypeArguments = invertedType.typeArguments; |
| } else { |
| return; |
| } |
| |
| // Check for super-bounded. |
| final invertedSubstitution = Substitution.fromPairs( |
| typeParameters, |
| invertedTypeArguments, |
| ); |
| for (var i = 0; i < invertedTypeArguments.length; i++) { |
| var typeParameter = typeParameters[i]; |
| var typeArgument = invertedTypeArguments[i]; |
| |
| var bound = typeParameter.bound; |
| if (bound == null) { |
| continue; |
| } |
| |
| bound = _libraryElement.toLegacyTypeIfOptOut(bound); |
| bound = invertedSubstitution.substituteType(bound); |
| |
| if (!_typeSystem.isSubtypeOf(typeArgument, bound)) { |
| _errorReporter.reportErrorForNode( |
| CompileTimeErrorCode.TYPE_ARGUMENT_NOT_MATCHING_BOUNDS, |
| _typeArgumentErrorNode(namedType, i), |
| [typeArgument, typeParameter.name, bound], |
| buildContextMessages( |
| invertedTypeArguments: invertedTypeArguments, |
| ), |
| ); |
| } |
| } |
| } |
| |
| /// Verify that each type argument in [typeArgumentList] is within its bounds, |
| /// as defined by [genericType]. |
| void _checkInvocationTypeArguments( |
| List<TypeAnnotation>? typeArgumentList, |
| DartType? genericType, |
| DartType? instantiatedType, |
| ) { |
| if (typeArgumentList == null) { |
| return; |
| } |
| |
| if (genericType is! FunctionType || instantiatedType is! FunctionType) { |
| return; |
| } |
| |
| var fnTypeParams = genericType.typeFormals; |
| var typeArgs = typeArgumentList.map((t) => t.typeOrThrow).toList(); |
| |
| // If the amount mismatches, clean up the lists to be substitutable. The |
| // mismatch in size is reported elsewhere, but we must successfully |
| // perform substitution to validate bounds on mismatched lists. |
| var providedLength = math.min(typeArgs.length, fnTypeParams.length); |
| fnTypeParams = fnTypeParams.sublist(0, providedLength); |
| typeArgs = typeArgs.sublist(0, providedLength); |
| |
| for (int i = 0; i < providedLength; i++) { |
| // Check the `extends` clause for the type parameter, if any. |
| // |
| // Also substitute to handle cases like this: |
| // |
| // <TFrom, TTo extends TFrom> |
| // <TFrom, TTo extends Iterable<TFrom>> |
| // <T extends Cloneable<T>> |
| // |
| DartType argType = typeArgs[i]; |
| |
| if (argType is FunctionType && argType.typeFormals.isNotEmpty) { |
| if (!_libraryElement.featureSet.isEnabled(Feature.generic_metadata)) { |
| _errorReporter.reportErrorForNode( |
| CompileTimeErrorCode.GENERIC_FUNCTION_TYPE_CANNOT_BE_TYPE_ARGUMENT, |
| typeArgumentList[i], |
| ); |
| continue; |
| } |
| } |
| |
| var fnTypeParam = fnTypeParams[i]; |
| var rawBound = fnTypeParam.bound; |
| if (rawBound == null) { |
| continue; |
| } |
| |
| var substitution = Substitution.fromPairs(fnTypeParams, typeArgs); |
| var bound = substitution.substituteType(rawBound); |
| if (!_typeSystem.isSubtypeOf(argType, bound)) { |
| _errorReporter.reportErrorForNode( |
| CompileTimeErrorCode.TYPE_ARGUMENT_NOT_MATCHING_BOUNDS, |
| typeArgumentList[i], |
| [argType, fnTypeParam.name, bound]); |
| } |
| } |
| } |
| |
| /// Checks to ensure that the given list of type [arguments] does not have a |
| /// type parameter as a type argument. The [errorCode] is either |
| /// [CompileTimeErrorCode.INVALID_TYPE_ARGUMENT_IN_CONST_LIST] or |
| /// [CompileTimeErrorCode.INVALID_TYPE_ARGUMENT_IN_CONST_MAP]. |
| void _checkTypeArgumentConst( |
| NodeList<TypeAnnotation> arguments, ErrorCode errorCode) { |
| for (TypeAnnotation type in arguments) { |
| if (type is NamedType && type.type is TypeParameterType) { |
| _errorReporter |
| .reportErrorForNode(errorCode, type, [type.name.toSource()]); |
| } |
| } |
| } |
| |
| /// Verify that the given list of [typeArguments] contains exactly the |
| /// [expectedCount] of elements, reporting an error with the [errorCode] |
| /// if not. |
| void _checkTypeArgumentCount( |
| TypeArgumentList typeArguments, |
| int expectedCount, |
| ErrorCode errorCode, |
| ) { |
| int actualCount = typeArguments.arguments.length; |
| if (actualCount != expectedCount) { |
| _errorReporter.reportErrorForNode( |
| errorCode, |
| typeArguments, |
| [actualCount], |
| ); |
| } |
| } |
| |
| /// Given a [node] without type arguments that refers to [element], issues |
| /// an error if [type] is a generic type, and the type arguments were not |
| /// supplied from inference or a non-dynamic default instantiation. |
| /// |
| /// This function is used by other node-specific type checking functions, and |
| /// should only be called when [node] has no explicit `typeArguments`. |
| /// |
| /// [inferenceContextNode] is the node that has the downwards context type, |
| /// if any. For example an [InstanceCreationExpression]. |
| /// |
| /// This function will return false if any of the following are true: |
| /// |
| /// - [inferenceContextNode] has an inference context type that does not |
| /// contain `_` |
| /// - [type] does not have any `dynamic` type arguments. |
| /// - the element is marked with `@optionalTypeArgs` from "package:meta". |
| bool _isMissingTypeArguments(AstNode node, DartType type, Element? element) { |
| List<DartType> typeArguments; |
| var alias = type.alias; |
| if (alias != null) { |
| typeArguments = alias.typeArguments; |
| } else if (type is InterfaceType) { |
| typeArguments = type.typeArguments; |
| } else { |
| return false; |
| } |
| |
| // Check if this type has type arguments and at least one is dynamic. |
| // If so, we may need to issue a strict-raw-types error. |
| if (typeArguments.any((t) => t.isDynamic)) { |
| if (element != null && element.hasOptionalTypeArgs) { |
| return false; |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| /// Determines if the given [namedType] occurs in a context where |
| /// super-bounded types are allowed. |
| bool _shouldAllowSuperBoundedTypes(NamedType namedType) { |
| var parent = namedType.parent; |
| if (parent is ExtendsClause) return false; |
| if (parent is OnClause) return false; |
| if (parent is ClassTypeAlias) return false; |
| if (parent is WithClause) return false; |
| if (parent is ConstructorName) return false; |
| if (parent is ImplementsClause) return false; |
| if (parent is GenericTypeAlias) return false; |
| return true; |
| } |
| |
| /// Return the type arguments at [index] from [node], or the [node] itself. |
| static TypeAnnotation _typeArgumentErrorNode(NamedType node, int index) { |
| var typeArguments = node.typeArguments?.arguments; |
| if (typeArguments != null && index < typeArguments.length) { |
| return typeArguments[index]; |
| } |
| return node; |
| } |
| } |
| |
| class _TypeArgumentIssue { |
| /// The index for type argument within the passed type arguments. |
| final int index; |
| |
| /// The type parameter with the bound that was violated. |
| final TypeParameterElement parameter; |
| |
| /// The substituted bound of the [parameter]. |
| final DartType parameterBound; |
| |
| /// The type argument that violated the [parameterBound]. |
| final DartType argument; |
| |
| _TypeArgumentIssue( |
| this.index, |
| this.parameter, |
| this.parameterBound, |
| this.argument, |
| ); |
| |
| @override |
| String toString() { |
| return 'TypeArgumentIssue(index=$index, parameter=$parameter, ' |
| 'parameterBound=$parameterBound, argument=$argument)'; |
| } |
| } |