Skip to content
This repository was archived by the owner on Nov 20, 2024. It is now read-only.

Commit 6a49a01

Browse files
authored
Initial commit of avoid_dynamic_calls. (#2417)
* Initial commit of avoid_dynamic_calls. * A few more tweaks for is/as. * Regenerate example/all.yaml. * Update and expand test cases. * Implement Brian's helpful suggestions. * Spelling is hard. * Address feedback. * Update due to comments. * Reduce code de-duplication, use .readType. * Add expand allowed Object? members.
1 parent 21988ed commit 6a49a01

File tree

4 files changed

+451
-0
lines changed

4 files changed

+451
-0
lines changed

example/all.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ linter:
1616
- avoid_catching_errors
1717
- avoid_classes_with_only_static_members
1818
- avoid_double_and_int_checks
19+
- avoid_dynamic_calls
1920
- avoid_empty_else
2021
- avoid_equals_and_hash_code_on_mutable_classes
2122
- avoid_escaping_inner_quotes

lib/src/rules.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import 'rules/avoid_catches_without_on_clauses.dart';
1717
import 'rules/avoid_catching_errors.dart';
1818
import 'rules/avoid_classes_with_only_static_members.dart';
1919
import 'rules/avoid_double_and_int_checks.dart';
20+
import 'rules/avoid_dynamic_calls.dart';
2021
import 'rules/avoid_empty_else.dart';
2122
import 'rules/avoid_equals_and_hash_code_on_mutable_classes.dart';
2223
import 'rules/avoid_escaping_inner_quotes.dart';
@@ -201,6 +202,7 @@ void registerLintRules() {
201202
..register(AvoidCatchingErrors())
202203
..register(AvoidClassesWithOnlyStaticMembers())
203204
..register(AvoidDoubleAndIntChecks())
205+
..register(AvoidDynamicCalls())
204206
..register(AvoidEmptyElse())
205207
..register(AvoidEscapingInnerQuotes())
206208
..register(AvoidFieldInitializersInConstClasses())
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'package:analyzer/dart/ast/ast.dart';
6+
import 'package:analyzer/dart/ast/token.dart';
7+
import 'package:analyzer/dart/ast/visitor.dart';
8+
import '../analyzer.dart';
9+
10+
const _desc = r'Avoid method calls or property accesses on a "dynamic" target.';
11+
12+
const _details = r'''
13+
14+
**DO** avoid method calls or accessing properties on an object that is either
15+
explicitly or implicitly statically typed "dynamic". Dynamic calls are treated
16+
slightly different in every runtime environment and compiler, but most
17+
production modes (and even some development modes) have both compile size and
18+
runtime performance penalties associated with dynamic calls.
19+
20+
Additionally, targets typed "dynamic" disables most static analysis, meaning it
21+
is easier to lead to a runtime "NoSuchMethodError" or "NullError" than properly
22+
statically typed Dart code.
23+
24+
There is an exception to methods and properties that exist on "Object?":
25+
- a.hashCode
26+
- a.runtimeType
27+
- a.noSuchMethod(someInvocation)
28+
- a.toString()
29+
30+
... these members are dynamically dispatched in the web-based runtimes, but not
31+
in the VM-based ones. Additionally, they are so common that it would be very
32+
punishing to disallow `any.toString()` or `any == true`, for example.
33+
34+
Note that despite "Function" being a type, the semantics are close to identical
35+
to "dynamic", and calls to an object that is typed "Function" will also trigger
36+
this lint.
37+
38+
**BAD:**
39+
```
40+
void explicitDynamicType(dynamic object) {
41+
print(object.foo());
42+
}
43+
44+
void implicitDynamicType(object) {
45+
print(object.foo());
46+
}
47+
48+
abstract class SomeWrapper {
49+
T doSomething<T>();
50+
}
51+
52+
void inferredDynamicType(SomeWrapper wrapper) {
53+
var object = wrapper.doSomething();
54+
print(object.foo());
55+
}
56+
57+
void callDynamic(dynamic function) {
58+
function();
59+
}
60+
61+
void functionType(Function function) {
62+
function();
63+
}
64+
```
65+
66+
**GOOD:**
67+
```
68+
void explicitType(Fooable object) {
69+
object.foo();
70+
}
71+
72+
void castedType(dynamic object) {
73+
(object as Fooable).foo();
74+
}
75+
76+
abstract class SomeWrapper {
77+
T doSomething<T>();
78+
}
79+
80+
void inferredType(SomeWrapper wrapper) {
81+
var object = wrapper.doSomething<Fooable>();
82+
object.foo();
83+
}
84+
85+
void functionTypeWithParameters(Function() function) {
86+
function();
87+
}
88+
```
89+
90+
''';
91+
92+
class AvoidDynamicCalls extends LintRule implements NodeLintRule {
93+
AvoidDynamicCalls()
94+
: super(
95+
name: 'avoid_dynamic_calls',
96+
description: _desc,
97+
details: _details,
98+
group: Group.errors,
99+
maturity: Maturity.experimental,
100+
);
101+
102+
@override
103+
void registerNodeProcessors(
104+
NodeLintRegistry registry,
105+
LinterContext context,
106+
) {
107+
final visitor = _Visitor(this);
108+
registry
109+
..addAssignmentExpression(this, visitor)
110+
..addBinaryExpression(this, visitor)
111+
..addFunctionExpressionInvocation(this, visitor)
112+
..addIndexExpression(this, visitor)
113+
..addMethodInvocation(this, visitor)
114+
..addPostfixExpression(this, visitor)
115+
..addPrefixExpression(this, visitor)
116+
..addPrefixedIdentifier(this, visitor)
117+
..addPropertyAccess(this, visitor);
118+
}
119+
}
120+
121+
class _Visitor extends SimpleAstVisitor<void> {
122+
final LintRule rule;
123+
124+
_Visitor(this.rule);
125+
126+
bool _lintIfDynamic(Expression node) {
127+
if (node?.staticType?.isDynamic == true) {
128+
rule.reportLint(node);
129+
return true;
130+
} else {
131+
return false;
132+
}
133+
}
134+
135+
void _lintIfDynamicOrFunction(Expression node) {
136+
final staticType = node?.staticType;
137+
if (staticType == null) {
138+
return;
139+
}
140+
if (staticType.isDynamic) {
141+
rule.reportLint(node);
142+
}
143+
if (staticType.isDartCoreFunction) {
144+
rule.reportLint(node);
145+
}
146+
}
147+
148+
@override
149+
void visitAssignmentExpression(AssignmentExpression node) {
150+
if (node.readType?.isDynamic != true) {
151+
// An assignment expression can only be a dynamic call if it is a
152+
// "compound assignment" (i.e. such as `x += 1`); so if `readType` is not
153+
// dynamic, we don't need to check further.
154+
return;
155+
}
156+
if (node.operator.type == TokenType.QUESTION_QUESTION_EQ) {
157+
// x ??= foo is not a dynamic call.
158+
return;
159+
}
160+
rule.reportLint(node);
161+
}
162+
163+
@override
164+
void visitBinaryExpression(BinaryExpression node) {
165+
if (!node.operator.isUserDefinableOperator) {
166+
// Operators that can never be provided by the user can't be dynamic.
167+
return;
168+
}
169+
switch (node.operator.type) {
170+
case TokenType.EQ_EQ:
171+
case TokenType.BANG_EQ:
172+
// These operators exist on every type, even "Object?". While they are
173+
// virtually dispatched, they are not considered dynamic calls by the
174+
// CFE. They would also make landing this lint exponentially harder.
175+
return;
176+
}
177+
_lintIfDynamic(node.leftOperand);
178+
// We don't check node.rightOperand, because that is an implicit cast, not a
179+
// dynamic call (the call itself is based on leftOperand). While it would be
180+
// useful to do so, it is better solved by other more specific lints to
181+
// disallow implicit casts from dynamic.
182+
}
183+
184+
@override
185+
void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
186+
if (node.function != null) {
187+
_lintIfDynamicOrFunction(node.function);
188+
}
189+
}
190+
191+
@override
192+
void visitIndexExpression(IndexExpression node) {
193+
_lintIfDynamic(node.realTarget);
194+
}
195+
196+
@override
197+
void visitMethodInvocation(MethodInvocation node) {
198+
final methodName = node.methodName.name;
199+
if (node.target != null) {
200+
if (methodName == 'noSuchMethod' &&
201+
node.argumentList.arguments.length == 1 &&
202+
node.argumentList.arguments.first is! NamedExpression) {
203+
// Special-cased; these exist on every object, even those typed "Object?".
204+
return;
205+
}
206+
if (methodName == 'toString' && node.argumentList.arguments.isEmpty) {
207+
// Special-cased; these exist on every object, even those typed "Object?".
208+
return;
209+
}
210+
}
211+
final receiverWasDynamic = _lintIfDynamic(node.realTarget);
212+
if (!receiverWasDynamic) {
213+
_lintIfDynamicOrFunction(node.function);
214+
}
215+
}
216+
217+
void _lintPrefixOrPostfixExpression(Expression root, Expression operand) {
218+
if (_lintIfDynamic(operand)) {
219+
return;
220+
}
221+
if (root is CompoundAssignmentExpression) {
222+
// Not promoted by "is" since the type would lose capabilities.
223+
final rootAsAssignment = root as CompoundAssignmentExpression;
224+
if (rootAsAssignment.readType?.isDynamic == true) {
225+
// An assignment expression can only be a dynamic call if it is a
226+
// "compound assignment" (i.e. such as `x += 1`); so if `readType` is
227+
// dynamic we should lint.
228+
rule.reportLint(root);
229+
}
230+
}
231+
}
232+
233+
@override
234+
void visitPrefixExpression(PrefixExpression node) {
235+
_lintPrefixOrPostfixExpression(node, node.operand);
236+
}
237+
238+
@override
239+
void visitPrefixedIdentifier(PrefixedIdentifier node) {
240+
final property = node.identifier.name;
241+
if (const {
242+
'hashCode',
243+
'noSuchMethod',
244+
'runtimeType',
245+
'toString',
246+
}.contains(property)) {
247+
// Special-cased; these exist on every object, even those typed "Object?".
248+
return;
249+
}
250+
_lintIfDynamic(node.prefix);
251+
}
252+
253+
@override
254+
void visitPostfixExpression(PostfixExpression node) {
255+
if (node.operator.type == TokenType.BANG) {
256+
// x! is not a dynamic call, even if "x" is dynamic.
257+
return;
258+
}
259+
_lintPrefixOrPostfixExpression(node, node.operand);
260+
}
261+
262+
@override
263+
void visitPropertyAccess(PropertyAccess node) {
264+
_lintIfDynamic(node.realTarget);
265+
}
266+
}

0 commit comments

Comments
 (0)