blob: b7ab14814234f3721f94d0cac1e3dbd2b5bb9f8b [file] [log] [blame]
// 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/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 '../analyzer.dart';
const _desc = r'Avoid method calls or property accesses on a "dynamic" target.';
const _details = r'''
**DO** avoid method calls or accessing properties on an object that is either
explicitly or implicitly statically typed "dynamic". Dynamic calls are treated
slightly different in every runtime environment and compiler, but most
production modes (and even some development modes) have both compile size and
runtime performance penalties associated with dynamic calls.
Additionally, targets typed "dynamic" disables most static analysis, meaning it
is easier to lead to a runtime "NoSuchMethodError" or "NullError" than properly
statically typed Dart code.
There is an exception to methods and properties that exist on "Object?":
- a.hashCode
- a.runtimeType
- a.noSuchMethod(someInvocation)
- a.toString()
... these members are dynamically dispatched in the web-based runtimes, but not
in the VM-based ones. Additionally, they are so common that it would be very
punishing to disallow `any.toString()` or `any == true`, for example.
Note that despite "Function" being a type, the semantics are close to identical
to "dynamic", and calls to an object that is typed "Function" will also trigger
this lint.
**BAD:**
```dart
void explicitDynamicType(dynamic object) {
print(object.foo());
}
void implicitDynamicType(object) {
print(object.foo());
}
abstract class SomeWrapper {
T doSomething<T>();
}
void inferredDynamicType(SomeWrapper wrapper) {
var object = wrapper.doSomething();
print(object.foo());
}
void callDynamic(dynamic function) {
function();
}
void functionType(Function function) {
function();
}
```
**GOOD:**
```dart
void explicitType(Fooable object) {
object.foo();
}
void castedType(dynamic object) {
(object as Fooable).foo();
}
abstract class SomeWrapper {
T doSomething<T>();
}
void inferredType(SomeWrapper wrapper) {
var object = wrapper.doSomething<Fooable>();
object.foo();
}
void functionTypeWithParameters(Function() function) {
function();
}
```
''';
class AvoidDynamicCalls extends LintRule implements NodeLintRule {
AvoidDynamicCalls()
: super(
name: 'avoid_dynamic_calls',
description: _desc,
details: _details,
group: Group.errors,
maturity: Maturity.experimental,
);
@override
void registerNodeProcessors(
NodeLintRegistry registry,
LinterContext 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> {
final LintRule rule;
_Visitor(this.rule);
bool _lintIfDynamic(Expression? node) {
if (node?.staticType?.isDynamic ?? false) {
rule.reportLint(node);
return true;
} else {
return false;
}
}
void _lintIfDynamicOrFunction(Expression node, {DartType? staticType}) {
staticType ??= node.staticType;
if (staticType == null) {
return;
}
if (staticType.isDynamic) {
rule.reportLint(node);
}
if (staticType.isDartCoreFunction) {
rule.reportLint(node);
}
}
@override
void visitAssignmentExpression(AssignmentExpression node) {
if (node.readType?.isDynamic != true) {
// 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.reportLint(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;
}
_lintIfDynamic(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) {
_lintIfDynamicOrFunction(node.function);
}
@override
void visitIndexExpression(IndexExpression node) {
_lintIfDynamic(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) {
// Special-cased; these exist on every object, even those typed "Object?".
return;
}
if (methodName == 'toString' && node.argumentList.arguments.isEmpty) {
// Special-cased; these exist on every object, even those typed "Object?".
return;
}
}
var receiverWasDynamic = _lintIfDynamic(node.realTarget);
if (!receiverWasDynamic) {
var target = node.target;
// 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;
}
_lintIfDynamicOrFunction(node.function, staticType: staticType);
}
}
void _lintPrefixOrPostfixExpression(Expression root, Expression operand) {
if (_lintIfDynamic(operand)) {
return;
}
if (root is CompoundAssignmentExpression) {
// Not promoted by "is" since the type would lose capabilities.
var rootAsAssignment = root as CompoundAssignmentExpression;
if (rootAsAssignment.readType?.isDynamic ?? false) {
// 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 lint.
rule.reportLint(root);
}
}
}
@override
void visitPrefixExpression(PrefixExpression node) {
_lintPrefixOrPostfixExpression(node, node.operand);
}
@override
void visitPrefixedIdentifier(PrefixedIdentifier node) {
var property = node.identifier.name;
if (const {
'hashCode',
'noSuchMethod',
'runtimeType',
'toString',
}.contains(property)) {
// Special-cased; these exist on every object, even those typed "Object?".
return;
}
_lintIfDynamic(node.prefix);
}
@override
void visitPostfixExpression(PostfixExpression node) {
if (node.operator.type == TokenType.BANG) {
// x! is not a dynamic call, even if "x" is dynamic.
return;
}
_lintPrefixOrPostfixExpression(node, node.operand);
}
@override
void visitPropertyAccess(PropertyAccess node) {
_lintIfDynamic(node.realTarget);
}
}