| // Copyright (c) 2021, 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. | |
| /// @docImport 'dart:async'; | |
| library; | |
| import 'package:analyzer/dart/ast/visitor.dart'; | |
| import 'package:analyzer/dart/element/type.dart'; | |
| import 'package:analyzer/src/dart/ast/ast.dart'; | |
| import 'package:analyzer/src/dart/element/type.dart'; | |
| import 'package:analyzer/src/dart/element/type_provider.dart'; | |
| import 'package:analyzer/src/dart/element/type_system.dart'; | |
| import 'package:analyzer/src/dart/error/lint_codes.dart'; | |
| import 'package:analyzer/src/diagnostic/diagnostic.dart' as diag; | |
| import 'package:analyzer/src/error/listener.dart'; | |
| import 'package:analyzer/src/error/return_type_verifier.dart'; | |
| import 'package:analyzer/src/generated/error_verifier.dart'; | |
| import 'package:collection/collection.dart'; | |
| /// Reports on invalid functions passed as error handlers. | |
| /// | |
| /// Functions must either accept exactly one positional parameter, or exactly | |
| /// two positional parameters. The one parameter (or the first parameter) must | |
| /// have a type of `dynamic`, `Object`, or `Object?`. If a second parameter is | |
| /// accepted, it must have a type of `StackTrace`. | |
| /// | |
| /// A function is checked if it is passed as: | |
| /// * as the first argument to [Future.catchError], | |
| /// * as the 'onError' named argument to [Future.then], | |
| /// * as the first argument to [Stream.handleError], | |
| /// * as the 'onError' argument to [Stream.listen], | |
| /// * as the first argument to [StreamSubscription.onError], | |
| /// | |
| /// Additionally, a function passed as the first argument to | |
| /// `Future<T>.catchError` must return `FutureOr<T>`, and any return statements | |
| /// in a function literal must return a value of type `FutureOr<T>`. | |
| class ErrorHandlerVerifier { | |
| final DiagnosticReporter _diagnosticReporter; | |
| final TypeProviderImpl _typeProvider; | |
| final TypeSystemImpl _typeSystem; | |
| final ReturnTypeVerifier _returnTypeVerifier; | |
| final bool _strictCasts; | |
| ErrorHandlerVerifier( | |
| this._diagnosticReporter, | |
| this._typeProvider, | |
| this._typeSystem, { | |
| required bool strictCasts, | |
| }) : _strictCasts = strictCasts, | |
| _returnTypeVerifier = ReturnTypeVerifier( | |
| typeProvider: _typeProvider, | |
| typeSystem: _typeSystem, | |
| diagnosticReporter: _diagnosticReporter, | |
| strictCasts: strictCasts, | |
| ); | |
| void verifyNamedFunctionInvocation( | |
| NamedFunctionInvocation node, | |
| Expression target, | |
| ) { | |
| _verifyInvocation( | |
| node, | |
| target: target, | |
| methodName: node.name.lexeme, | |
| argumentList: node.argumentList, | |
| ); | |
| } | |
| /// Checks that [expression], a function with static type [expressionType], is | |
| /// a valid error handler. | |
| /// | |
| /// Only checks the first parameter type if [checkFirstParameterType] is true. | |
| /// Certain error handlers are allowed to specify a different type for their | |
| /// first parameter. | |
| void _checkErrorHandlerFunctionType( | |
| AstNode errorNode, | |
| Expression expression, | |
| FunctionTypeImpl expressionType, | |
| DartType expectedFunctionReturnType, { | |
| bool checkFirstParameterType = true, | |
| }) { | |
| void report() { | |
| _diagnosticReporter.report( | |
| diag.argumentTypeNotAssignableToErrorHandler | |
| .withArguments( | |
| actualType: expressionType, | |
| expectedType: expectedFunctionReturnType, | |
| ) | |
| .at(errorNode), | |
| ); | |
| } | |
| var parameters = expressionType.formalParameters; | |
| if (parameters.isEmpty) { | |
| return report(); | |
| } | |
| var firstParameter = parameters.first; | |
| if (firstParameter.isNamed) { | |
| return report(); | |
| } else if (checkFirstParameterType) { | |
| if (!_typeSystem.isSubtypeOf( | |
| _typeProvider.objectType, | |
| firstParameter.type, | |
| )) { | |
| return report(); | |
| } | |
| } | |
| if (parameters.length == 2) { | |
| var secondParameter = parameters[1]; | |
| if (secondParameter.isNamed) { | |
| return report(); | |
| } else { | |
| if (!_typeSystem.isSubtypeOf( | |
| _typeProvider.stackTraceType, | |
| secondParameter.type, | |
| )) { | |
| return report(); | |
| } | |
| } | |
| } else if (parameters.length > 2) { | |
| return report(); | |
| } | |
| } | |
| /// Check the 'onError' argument given to [Future.catchError]. | |
| void _checkFutureCatchErrorOnError(Expression target, Expression callback) { | |
| var targetType = target.staticType as InterfaceTypeImpl; | |
| var targetFutureType = targetType.typeArguments.first; | |
| var expectedReturnType = _typeProvider.futureOrType(targetFutureType); | |
| if (callback is FunctionExpressionImpl) { | |
| var callbackType = callback.staticType as FunctionTypeImpl; | |
| _checkErrorHandlerFunctionType( | |
| callback, | |
| callback, | |
| callbackType, | |
| expectedReturnType, | |
| ); | |
| if (targetFutureType is VoidType) { | |
| return; | |
| } | |
| if (callbackType.returnType is VoidType && | |
| _isVoidOrDynamic(targetFutureType)) { | |
| // Special case for `void`: A function returning `void` is allowed for the | |
| // cases where the expected type is `void`, `dynamic`, or `Null`. | |
| return; | |
| } | |
| var catchErrorOnErrorExecutable = EnclosingExecutableContext( | |
| callback.declaredFragment!.element, | |
| isAsynchronous: true, | |
| isGenerator: false, | |
| catchErrorOnErrorReturnType: expectedReturnType, | |
| ); | |
| var returnStatementVerifier = _ReturnStatementVerifier( | |
| _returnTypeVerifier, | |
| ); | |
| _returnTypeVerifier.enclosingExecutable = catchErrorOnErrorExecutable; | |
| callback.body.accept2(returnStatementVerifier); | |
| } else { | |
| var callbackType = callback.staticType; | |
| if (callbackType is FunctionTypeImpl) { | |
| _checkReturnType( | |
| targetFutureType, | |
| callbackType.returnType, | |
| callback, | |
| diag.returnTypeInvalidForCatchError, | |
| ); | |
| _checkErrorHandlerFunctionType( | |
| callback, | |
| callback, | |
| callbackType, | |
| expectedReturnType, | |
| ); | |
| } else { | |
| // If [callback] is not even a Function, then ErrorVerifier will have | |
| // reported this. | |
| } | |
| } | |
| } | |
| void _checkFutureThenOnError(Expression node, Expression callback) { | |
| var nodeType = node.staticType as InterfaceTypeImpl; | |
| var targetFutureType = nodeType.typeArguments.first; | |
| var expectedReturnType = _typeProvider.futureOrType(targetFutureType); | |
| if (callback is FunctionExpressionImpl) { | |
| var callbackType = callback.staticType as FunctionTypeImpl; | |
| _checkErrorHandlerFunctionType( | |
| callback, | |
| callback, | |
| callbackType, | |
| expectedReturnType, | |
| ); | |
| if (targetFutureType is VoidType) { | |
| return; | |
| } | |
| if (callbackType.returnType is VoidType && | |
| _isVoidOrDynamic(targetFutureType)) { | |
| // Special case for `void`: A function returning `void` is allowed for the | |
| // cases where the expected type is `void`, `dynamic`, or `Null`. | |
| return; | |
| } | |
| var thenOnErrorExecutable = EnclosingExecutableContext( | |
| callback.declaredFragment!.element, | |
| isAsynchronous: true, | |
| isGenerator: false, | |
| thenOnErrorReturnType: expectedReturnType, | |
| ); | |
| var returnStatementVerifier = _ReturnStatementVerifier( | |
| _returnTypeVerifier, | |
| ); | |
| _returnTypeVerifier.enclosingExecutable = thenOnErrorExecutable; | |
| callback.body.accept2(returnStatementVerifier); | |
| } else { | |
| if (callback.staticType case FunctionTypeImpl callbackType) { | |
| _checkReturnType( | |
| targetFutureType, | |
| callbackType.returnType, | |
| callback, | |
| diag.returnTypeInvalidForThen, | |
| ); | |
| _checkErrorHandlerFunctionType( | |
| callback, | |
| callback, | |
| callbackType, | |
| expectedReturnType, | |
| checkFirstParameterType: false, | |
| ); | |
| } | |
| } | |
| } | |
| void _checkReturnType( | |
| TypeImpl targetType, | |
| TypeImpl functionReturnType, | |
| Expression callback, | |
| DiagnosticWithArguments< | |
| LocatableDiagnostic Function({ | |
| required DartType actualType, | |
| required DartType expectedType, | |
| }) | |
| > | |
| diagnostic, | |
| ) { | |
| if (functionReturnType is VoidType) { | |
| if (_isVoidOrDynamic(targetType) || _typeSystem.isNullable(targetType)) { | |
| // Special case for `void`: A function returning `void` is allowed for the | |
| // cases where the expected type is `void`, `dynamic`, or a nullable type. | |
| return; | |
| } | |
| } | |
| var expectedType = _typeProvider.futureOrType(targetType); | |
| if (!_typeSystem.isAssignableTo( | |
| functionReturnType, | |
| expectedType, | |
| strictCasts: _strictCasts, | |
| )) { | |
| _diagnosticReporter.report( | |
| diagnostic | |
| .withArguments( | |
| actualType: functionReturnType, | |
| expectedType: expectedType, | |
| ) | |
| .at(callback), | |
| ); | |
| } | |
| } | |
| /// Returns whether [type] represents the type named [typeName], declared in | |
| /// the 'dart:async' library. | |
| bool _isDartCoreAsyncType(DartType type, String typeName) => | |
| type is InterfaceType && | |
| type.element.name == typeName && | |
| type.element.library.isDartAsync; | |
| void _verifyInvocation( | |
| Expression node, { | |
| required Expression target, | |
| required String methodName, | |
| required ArgumentList argumentList, | |
| }) { | |
| if (argumentList.arguments2.isEmpty) { | |
| return; | |
| } | |
| var targetType = target.staticType; | |
| if (targetType == null) { | |
| return; | |
| } | |
| if (methodName == 'catchError' && targetType.isDartAsyncFuture) { | |
| var callback = argumentList.arguments2.first; | |
| if (callback is NamedArgument) { | |
| // TODO(srawlins): The comment below is wrong, given | |
| // `named-arguments-anywhere`. | |
| // This implies that no positional arguments are passed. | |
| return; | |
| } | |
| _checkFutureCatchErrorOnError(target, callback.argumentExpression2); | |
| return; | |
| } | |
| if (methodName == 'then' && targetType.isDartAsyncFuture) { | |
| var callback = argumentList.arguments2 | |
| .whereType<NamedArgument>() | |
| .firstWhereOrNull((argument) => argument.name.lexeme == 'onError'); | |
| if (callback == null) { | |
| return; | |
| } | |
| _checkFutureThenOnError(node, callback.argumentExpression2); | |
| return; | |
| } | |
| if (methodName == 'handleError' && | |
| _isDartCoreAsyncType(targetType, 'Stream')) { | |
| var callback = argumentList.arguments2.first; | |
| if (callback is NamedArgument) { | |
| // This implies that no positional arguments are passed. | |
| return; | |
| } | |
| var callbackType = callback.argumentExpression2.staticType; | |
| if (callbackType == null) { | |
| return; | |
| } | |
| if (callbackType is FunctionTypeImpl) { | |
| _checkErrorHandlerFunctionType( | |
| callback, | |
| callback.argumentExpression2, | |
| callbackType, | |
| _typeProvider.voidType, | |
| checkFirstParameterType: | |
| callback.argumentExpression2 is FunctionExpression, | |
| ); | |
| return; | |
| } | |
| // [callbackType] might be dart:core's Function, or something not | |
| // assignable to Function, in which case an error is reported elsewhere. | |
| } | |
| if (methodName == 'listen' && _isDartCoreAsyncType(targetType, 'Stream')) { | |
| var callback = argumentList.arguments2 | |
| .whereType<NamedArgument>() | |
| .firstWhereOrNull((argument) => argument.name.lexeme == 'onError'); | |
| if (callback == null) { | |
| return; | |
| } | |
| var callbackType = callback.argumentExpression2.staticType; | |
| if (callbackType == null) { | |
| return; | |
| } | |
| if (callbackType is FunctionTypeImpl) { | |
| _checkErrorHandlerFunctionType( | |
| callback, | |
| callback.argumentExpression2, | |
| callbackType, | |
| _typeProvider.voidType, | |
| checkFirstParameterType: | |
| callback.argumentExpression2 is FunctionExpression, | |
| ); | |
| return; | |
| } | |
| // [callbackType] might be dart:core's Function, or something not | |
| // assignable to Function, in which case an error is reported elsewhere. | |
| } | |
| if (methodName == 'onError' && | |
| _isDartCoreAsyncType(targetType, 'StreamSubscription')) { | |
| var callback = argumentList.arguments2.first; | |
| if (callback is NamedArgument) { | |
| // This implies that no positional arguments are passed. | |
| return; | |
| } | |
| var callbackType = callback.argumentExpression2.staticType; | |
| if (callbackType == null) { | |
| return; | |
| } | |
| if (callbackType is FunctionTypeImpl) { | |
| _checkErrorHandlerFunctionType( | |
| callback, | |
| callback.argumentExpression2, | |
| callbackType, | |
| _typeProvider.voidType, | |
| checkFirstParameterType: | |
| callback.argumentExpression2 is FunctionExpression, | |
| ); | |
| return; | |
| } | |
| // [callbackType] might be dart:core's Function, or something not | |
| // assignable to Function, in which case an error is reported elsewhere. | |
| } | |
| } | |
| static bool _isVoidOrDynamic(DartType type) { | |
| return type is VoidType || | |
| type is DynamicType || | |
| type is InvalidType || | |
| type.isDartCoreNull; | |
| } | |
| } | |
| /// Visits a function body, looking for return statements. | |
| class _ReturnStatementVerifier extends RecursiveAstVisitor2<void> { | |
| final ReturnTypeVerifier _returnTypeVerifier; | |
| _ReturnStatementVerifier(this._returnTypeVerifier); | |
| @override | |
| void visitExpressionFunctionBody(ExpressionFunctionBody node) { | |
| _returnTypeVerifier.verifyExpressionFunctionBody(node); | |
| super.visitExpressionFunctionBody(node); | |
| } | |
| @override | |
| void visitFunctionExpression(FunctionExpression node) { | |
| // Do not visit within [node]. We have no interest in return statements | |
| // within. | |
| } | |
| @override | |
| void visitReturnStatement(ReturnStatement node) { | |
| _returnTypeVerifier.verifyReturnStatement(node); | |
| super.visitReturnStatement(node); | |
| } | |
| } |