-
Notifications
You must be signed in to change notification settings - Fork 166
Initial commit of avoid_dynamic_calls. #2417
Initial commit of avoid_dynamic_calls. #2417
Conversation
/cc @leafpetersen |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this looks great.
} | ||
|
||
void equalityExpressions(dynamic a, dynamic b) { | ||
a == b; // LINT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What a bummer. If anything, this check might really slow uptake of this rule internally in Google. Ideally though, I'd like to keep this check, because you're right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, members of Object
are a special case. See overall comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a special case, but only for the VM, and not worth special, special-casing :P
I'm passing the review to @pq; implementation looks great; this will be much hailed inside Google. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great!
PTAL, and thanks for all the motivating +1s and great ideas to expand testing and correctness. |
Thanks! @pq, as lint 👑 , I want to give you a chance to do a once-over before merging. Thanks in advance! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dug into the ==
issue. As it turns out, the CFE treats invocations of Object
members as non-dynamic even on receivers of type dynamic
. This means that all of the following are OK:
void foo(dynamic a, dynamic b) {
a == b;
a.hashCode;
a.noSuchMethod;
a.noSuchMethod(Invocation());
a.runtimeType;
a.toString;
a.toString();
}
For the methods (noSuchMethod
and toString
) this only holds when the argument list matches the declaration in Object
. Thus, the following should lint:
void foo(dynamic a, dynamic b) {
a.noSuchMethod();
a.noSuchMethod(Invocation(), "extra");
a.toString(radix: 16);
}
This is also in line with the language specification, which treats Object
members specially w.r.t. the type of the invocation expression.
} | ||
|
||
void equalityExpressions(dynamic a, dynamic b) { | ||
a == b; // LINT |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, members of Object
are a special case. See overall comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
Super awesome!
|
||
**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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
=> differently?
I'm guessing that by "non-dynamic" you mean that they don't incur the cost of "dynamic" calls (because they do still perform a lookup to find the right method to invoke). If that's true, then the question becomes: Do we want to flag all invocations on an expression whose type is |
I think the CFE might have a different definition of "dynamic call" than what this lint is trying to address. Consider:
... this prints |
Yes, what I mean is that they are treated exactly the same as if the receiver had type I would definitely say we should not treat these calls as dynamic for the purpose of the lint. It is not so much a matter of what the current compilers are able to optimize (there are lots of other cases where a dynamic call can be eliminated in practice). It is a situation which specifically called out in the spec as an exception to the rules about member access on The spec does not define what is a dynamic call or not (since that is implementation terminology), but it distinguishes between member access which is statically typed and member access which is not. And in this distinction, accessing an |
I could see dropping the |
That dispatch happens for a receiver of type
Yes, I think that would make the lint both more practically useful and more in line with how the spec describes Another way to see it is that for a |
I loosened checks around members that exist on |
@override | ||
void visitPrefixExpression(PrefixExpression node) { | ||
// Implemented as: node?.staticType?.isDynamic == true | ||
if (_lintIfDynamic(node.operand)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the clarification.
I think we want to to such switch on the operator to explicitly express what we are checking and when. Doing one generic _lintIfDynamic
when for ++x
the type is null
does not make this code as easy to read as it could be.
And for ++
and --
we want to check CompoundAssignmentExpression.readType
directly. Because this is the type on which we will call operator+
or operator-
. Just like we do for assignments.
Similarly for postfix expressions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, that's a solid idea. I've consolidated and de-duplicated the code into _lintPrefixOrPostfixExpression(node, node.operand);
. PTAL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With comments addressed, this looks good.
@@ -193,6 +195,11 @@ class _Visitor extends SimpleAstVisitor<void> { | |||
|
|||
@override | |||
void visitMethodInvocation(MethodInvocation node) { | |||
final methodName = node.methodName.name; | |||
if (methodName == 'noSuchMethod' || methodName == 'toString') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also check the number of arguments. In order to be ignored by the lint, there should be 1 positional parameter for noSuchMethod
and 0 for toString
, and in both cases no named parameters.
@@ -216,6 +235,11 @@ class _Visitor extends SimpleAstVisitor<void> { | |||
|
|||
@override | |||
void visitPrefixedIdentifier(PrefixedIdentifier node) { | |||
final property = node.identifier.name; | |||
if (property == 'hashCode' || property == 'runtimeType') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A tearoff of noSuchMethod
or toString
is also not considered dynamic.
a.hashCode; // OK | ||
a.runtimeType; // OK | ||
a.noSuchMethod(null as Invocation); // OK | ||
a.toString(); // OK |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also test tearoffs of noSuchMethod
and toString
(OK) and calling these with the wrong number of arguments, or with named arguments (LINT).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did the most I could. I think being more comprehensive than this is likely infeasible, as the analyzer issues compile-time errors already for the wrong number of arguments on these methods, including tear-offs, and I'm not able to lint them as a result.
|
||
... 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this description is misleading. AFAIU, dart2js does not distinguish between dynamic and non-dynamic calls at all (except for possibly inserting extra type checks). In DDC, the calls in
String foo1(dynamic x) => x.toString();
String foo2(Object? x) => x.toString();
String foo3(Object x) => x.toString();
are all translated identically, but actual dynamic calls, like
String bar1(dynamic x) => x.toString(color: "fuchsia");
String bar2(dynamic x) => x.nothing();
are translated differently from that (using dart.dsend
, giving the name of the method as a string).
So the categorization of these calls as non-dynamic holds equally well across all of our compilers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That can't possibly be true, because undefined.toString()
andnull.toString()
in JS is a runtime error. So there would need to be special handling/interceptors that is not based entirely on virtual dispatch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In either case I think we can tweak the description later. Merging!
Closes dart-lang/sdk#57703.
This is a pretty difficult lint to get "right". I tried my best with the following principals:
Simple existence of
dynamic
or a dynamic-ly typed expression should not cause a lint.There are many places where a dynamic expression can be used in a way that does not "cause" a dynamic call:
as
,is
,??
, orx!
are non-virtual, and are not dynamic calls.Object?
(==
,hashCode
,runtimeType
,toString
).Implicit downcasts from
dynamic
to non-dynamic
, while bad, is not a dynamic call (it's a runtime cast).There are other lints and analysis modes that can lint these.
While not explicitly typed
dynamic
, a static type ofFunction
being invoked is a dynamic call.Open to any and all suggestions. I'm also happy landing this is an experimental state for others to give more feedback. One thing I'd like to avoid is splitting this into like, half-a-dozen different lints or too many ways to opt-out yet; I think
// ignore: avoid_dynamic_call
isn't great, but we can look at the UX of opting-out independent of landing this lint.