| // 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. |
| |
| import 'package:analyzer/analysis_rule/rule_context.dart'; |
| import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; |
| import 'package:analyzer/dart/ast/ast.dart'; |
| import 'package:analyzer/dart/ast/token.dart'; |
| import 'package:analyzer/dart/ast/visitor.dart'; |
| import 'package:analyzer/dart/element/type.dart'; |
| import 'package:analyzer/error/error.dart'; |
| |
| import '../analyzer.dart'; |
| |
| const _desc = r'Avoid method calls or property accesses on a `dynamic` target.'; |
| |
| class AvoidDynamicCalls extends LintRule { |
| AvoidDynamicCalls() |
| : super(name: LintNames.avoid_dynamic_calls, description: _desc); |
| |
| @override |
| DiagnosticCode get diagnosticCode => LinterLintCode.avoidDynamicCalls; |
| |
| @override |
| void registerNodeProcessors( |
| RuleVisitorRegistry registry, |
| RuleContext context, |
| ) { |
| var visitor = _Visitor(this); |
| registry |
| ..addAssignmentExpression(this, visitor) |
| ..addBinaryExpression(this, visitor) |
| ..addFunctionExpressionInvocation(this, visitor) |
| ..addIndexExpression(this, visitor) |
| ..addMethodInvocation(this, visitor) |
| ..addPostfixExpression(this, visitor) |
| ..addPrefixExpression(this, visitor) |
| ..addPrefixedIdentifier(this, visitor) |
| ..addPropertyAccess(this, visitor); |
| } |
| } |
| |
| class _Visitor extends SimpleAstVisitor<void> { |
| /// Member names which are allowed to be accessed on `dynamic`- or |
| /// `Function`-typed expressions. |
| static const _allowedMemberAccesses = { |
| 'hashCode', |
| 'noSuchMethod', |
| 'runtimeType', |
| 'toString', |
| }; |
| |
| final LintRule rule; |
| |
| _Visitor(this.rule); |
| |
| @override |
| void visitAssignmentExpression(AssignmentExpression node) { |
| if (node.readType is! DynamicType) { |
| // An assignment expression can only be a dynamic call if it is a |
| // "compound assignment" (i.e. such as `x += 1`); so if `readType` is not |
| // dynamic, we don't need to check further. |
| return; |
| } |
| if (node.operator.type == TokenType.QUESTION_QUESTION_EQ) { |
| // x ??= foo is not a dynamic call. |
| return; |
| } |
| rule.reportAtNode(node); |
| } |
| |
| @override |
| void visitBinaryExpression(BinaryExpression node) { |
| if (!node.operator.isUserDefinableOperator) { |
| // Operators that can never be provided by the user can't be dynamic. |
| return; |
| } |
| switch (node.operator.type) { |
| case TokenType.EQ_EQ: |
| case TokenType.BANG_EQ: |
| // These operators exist on every type, even "Object?". While they are |
| // virtually dispatched, they are not considered dynamic calls by the |
| // CFE. They would also make landing this lint exponentially harder. |
| return; |
| } |
| _reportIfDynamic(node.leftOperand); |
| // We don't check node.rightOperand, because that is an implicit cast, not a |
| // dynamic call (the call itself is based on leftOperand). While it would be |
| // useful to do so, it is better solved by other more specific lints to |
| // disallow implicit casts from dynamic. |
| } |
| |
| @override |
| void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) { |
| _reportIfDynamicOrFunction(node.function); |
| } |
| |
| @override |
| void visitIndexExpression(IndexExpression node) { |
| _reportIfDynamic(node.realTarget); |
| } |
| |
| @override |
| void visitMethodInvocation(MethodInvocation node) { |
| var methodName = node.methodName.name; |
| if (node.target != null) { |
| if (methodName == 'noSuchMethod' && |
| node.argumentList.arguments.length == 1 && |
| node.argumentList.arguments.first is! NamedExpression) { |
| // Allowed as these exist on every object, even those typed `Object?`. |
| return; |
| } |
| if (methodName == 'toString' && node.argumentList.arguments.isEmpty) { |
| // Allowed as these exist on every object, even those typed `Object?`. |
| return; |
| } |
| } |
| if (_isExplicitCast(node.realTarget)) return; |
| var receiverWasDynamic = _reportIfDynamic(node.realTarget); |
| if (!receiverWasDynamic) { |
| var target = node.realTarget; |
| // The `.call` method is special, where `a.call()` is treated ~as `a()`. |
| // |
| // If the method is `call`, and the receiver is a function, we assume then |
| // we are really checking the static type of the receiver, not the static |
| // type of the `call` method itself. |
| DartType? staticType; |
| if (methodName == 'call' && |
| target != null && |
| target.staticType is FunctionType) { |
| staticType = target.staticType; |
| } |
| _reportIfDynamicOrFunction(node.function, staticType: staticType); |
| } |
| } |
| |
| @override |
| void visitPostfixExpression(PostfixExpression node) { |
| if (node.operator.type == TokenType.BANG) { |
| // `x!` is not a dynamic call, even if `x` is `dynamic`. |
| return; |
| } |
| _reportPrefixOrPostfixExpression(node, node.operand); |
| } |
| |
| @override |
| void visitPrefixedIdentifier(PrefixedIdentifier node) { |
| if (_allowedMemberAccesses.contains(node.identifier.name)) { |
| // Allowed as these exist on every object, even those typed `Object?`. |
| return; |
| } |
| _reportIfDynamic(node.prefix); |
| } |
| |
| @override |
| void visitPrefixExpression(PrefixExpression node) { |
| _reportPrefixOrPostfixExpression(node, node.operand); |
| } |
| |
| @override |
| void visitPropertyAccess(PropertyAccess node) { |
| if (_allowedMemberAccesses.contains(node.propertyName.name)) { |
| // Allowed as these exist on every object, even those typed `Object?`. |
| return; |
| } |
| _reportIfDynamic(node.realTarget); |
| } |
| |
| bool _reportIfDynamic(Expression? node) { |
| if (node == null || node.staticType is! DynamicType) return false; |
| if (_isExplicitCast(node)) return false; |
| |
| rule.reportAtNode(node); |
| return true; |
| } |
| |
| void _reportIfDynamicOrFunction(Expression node, {DartType? staticType}) { |
| staticType ??= node.staticType; |
| if (staticType == null) return; |
| if (_isExplicitCast(node)) return; |
| if (staticType is DynamicType || staticType.isDartCoreFunction) { |
| rule.reportAtNode(node); |
| } |
| } |
| |
| void _reportPrefixOrPostfixExpression(Expression root, Expression operand) { |
| if (_reportIfDynamic(operand)) { |
| return; |
| } |
| if (root is CompoundAssignmentExpression) { |
| if (root.readType is DynamicType) { |
| // An assignment expression can only be a dynamic call if it is a |
| // "compound assignment" (i.e. such as `x += 1`); so if `readType` is |
| // `dynamic` we should report. |
| rule.reportAtNode(root); |
| } |
| } |
| } |
| |
| /// Whether an expression matches a pattern to allow a dynamic call. |
| /// |
| /// This should only be used to check expressions which have a static type of |
| /// `dynamic` or `Function`. |
| /// |
| /// Expressions with a static type of `dynamic` can be used only if they are |
| /// expliticly a parenthesized cast (which must be to `dynamic` or `Function` |
| /// to be relevant). |
| static bool _isExplicitCast(Expression? node) => |
| node?.unParenthesized is AsExpression; |
| } |