Open srawlins opened 3 years ago
Not approved: dynamic is fully supported in the language, and something a developer is free to use if they desire.
@mit-mit How does a lint prevent a developer from using dynamic calls if they desire? :thinking: My understanding is that the lint would be used (optionally) by developers who do not desire to have dynamic calls in their codebase, or at least, in rare situations where they do need one, they want it flagged and worked around using an // ignore: avoid_dynamic_calls
.
I was looking forward to this lint to use in the Flutter Web engine. We don't need dynamic invocations in the engine, but our codebase is big, so we don't know if we have some dynamic calls that sneaked in by accident, potentially hurting performance and/or code size.
Also, my understanding is that the lint doesn't lint against dynamic
. It only lints against dynamic invocations. For example, the following code is totally legal:
dynamic json = parseJson(jsonString);
if (json is Map) {
dynamic someField = json['some_field'];
if (someField is num) {
print(someField.toStringAsFixed(2));
} else if (someField is String) {
print(someField);
} else {
// ... etc ...
}
What would be flagged is this:
dynamic json = parseJson(jsonString);
json['some_field'].toStringAsFixed(3);
// ^ ^
// | | dynamic invocation of toStringAsFixed
// dynamic invocation of operator[]
Those invocations can crash, and they can cause retention of methods on unrelated classes. In codebases, such as the Flutter Engine, flagging such situations would be super useful.
(Edit: minor fixes; clarification; examples)
This issue doesn't track the availability of this lint; it tracks whether the lint is enable by default in package:lints
/ package:flutter_lints
. Thus the triage decision is wrt. what we decide to enforce out of the box, not what you can individually configure in your analysis_options.yaml file. Does that explain our triage decision?
Oh, but maybe the confusion is that @srawlins logged this as a request to have this as a supported lint? Then we can certainly reactivate and transfer to https://github.com/dart-lang/linter/issues...
The rule exists. This was a request to add the rule to the lints:core or lints:recommended rule set.
Ah, cool. If this can be optionally added to analysis options, I have no further concerns.
I hear there were some recent discussion around dynamic
in the dart language team and the notes of that discussion made me wonder whether it would actually be a good thing to push people towards avoiding dynamic calls by adding this lint to "recommended" after all.
/cc @munificent @eernstg you were part of these discussions. Curious to hear what you think.
I guess it is possible to disable a recommended lint in a package and certainly using // ignore_for_file:...
. That might make it a good trade-off to enable this lint for anyone who doesn't actively turn it off. After all, a dynamic invocation which is not seen as such can be rather misleading when it comes to potential run-time errors.
@mit-mit Can you reopen this issue for reconsideration during our next lint triage?
It came up in a recent discussion that a surprisingly high number of the top 200 runtime errors reported by our flutter analytics are related to dynamic. There is some hope that enforcing a lint like this (and/or https://github.com/dart-lang/lints/issues/125 could help in reducing that number).
I guess it is possible to disable a recommended lint in a package and certainly using
From a discussion, I believe another way that was proposed to disable this lint was via an explicit cast; so:
(value as dynamic).fooBar();
cc @leafpetersen
Together with strict-casts
(#125), this would completely remove dynamic
from the language - no implicit down-casts and no dynamic invocations.
If we're not willing to actually remove dynamic from the language, I don't think we should enable these restrictions by default, which is what adding to core
or recommended
would imply.
If we're not willing to actually remove dynamic from the language
A big "if"! Surely not a settled decision, but an open possibility.
If we're not willing to actually remove dynamic from the language
To me, the lints we recommend are about best practices - and from our crash reporting we can see that a lot of people are struggling with dynamic
. Therefore, it would make sense to me to guide people away from this particular language feature. That doesn't necessarily mean that the feature has to be removed from the language (that can be a separate discussion). We enable some other lints as well where we guide people away from certain coding patterns that are technically possible in Dart, but good practice has shown they should be avoided.
and from our crash reporting we can see that a lot of people are struggling with
dynamic
.
When I've written an app that's talking to a JSON API I don't know well, sometimes I just guess at the API by casting stuff and then let the crashes incrementally teach me the API's schema. I wonder if some fraction of these crashes are coming from users that are just doing "let me just try it and see what happens" and don't indicate a real source of pain in the language.
I wonder if some fraction of these crashes are coming from users that are just doing "let me just try it and see what happens"
Are we able to pivot the crash reporting on release mode (debug vs release)? I'd suspect such "let me just try it" development would be all debug mode?
The fundamental question here is: Why does Dart support the type dynamic
?
One answer could be: Because it is an irreplaceable extension of the expressive power of the language in situations where very generic code is assumed type safe based on an ad-hoc proof which is beyond the capabilities of the built-in type system. In other words, it's a specialist's tool, and it is no problem at all if dynamically typed code requires some extra syntax. (Like an explicit as dynamic
here and there, or // ignore: avoid_dynamic_calls
, or whatever we wish to require via lints or other mechanized rules and guidelines.)
A different answer could be: Because dynamically typed code is easier to create for inexperienced developers. Static typing is an option that developers may adopt over time when they are ready to make the extra investment into the correctness of their code. If this is the reason for having dynamic
then all usages of this type should occur easily and without syntactic overhead. For instance, the type dynamic
should be used implicitly when no type is specified and type inference has no other information.
I'd very much prefer to consider dynamically typed coding as a specialist's tool, that is: I prefer the first answer.
I think the second answer is much less convincing today than it might have been years ago. In particular, Dart supports a very substantial level of static analysis (type inference, variable promotion, flow analysis) that enables statically typed code with few explicit type annotations. Admittedly, this kind of coding can appear to be more complex than dynamically typed coding, but that's because the statically typed code, even with type inference etc, does more than the dynamically typed code: It includes a proof of certain kinds of correctness. I believe the Dart community as a whole is willing to do the extra work here, in return for less buggy programs.
Based on this perspective on dynamically typed coding, I'd have no problems recommending that we include avoid_dynamic_calls
in a widely used set of lints.
If we actually want to take dynamic
out of the language as a type, we probably could.
I'd just remove the type, and add an "operator"/special-syntax of .dynamic
that can be applied to any expression, and then that single expression behaves as if it has type dynamic
today - can invoke any method, can assign to any type. The result of the invocation will have type Object?
, you'd need to do .dynamic
again if you want more dynamism.
It might be hard on JSON access, but I hope pattern matching will help with that.
The global migration would add .dynamic
at every existing dynamic invocation or implicit downcast, then change every occurrence of dynamic
to Object?
.
Migration order will be a problem, if something changes to returning Object?
before the downstream code migrates, it can't see where to add the .dynamic
(but it might guess based on the type errors).
Or there could be a phase where we allow you to write dynamic
, but language version X.Y and above treats it as Object?
and requires .dynamic
, those below do not.
Possibly automatable, but not easy.
I strongly agree with everything @eernstg said. With Dart 2.0, we pivoted Dart away from the "gradual typing" user experience where a use uses dynamic because they just don't want to deal with types (or don't want to deal with them yet). Once we made List<dynamic>
no longer a subtype of other list types (and likewise other generics), we basically lost that use case. Dart doesn't support the "don't worry about types" use case when code like this is prohibited:
var a = [];
var b = [1, 2, 3];
b.addAll(a);
I'm happy to give up this use case (though I believe it does have value!) because losing that means much better user experience for users who do want the full benefit of types. I think the trade-off is a very clear net win.
But it does mean that, like Erik suggests, dynamic
is basically just a specialized tool for specialized use cases. Given that, it probably doesn't warrant the level of direct language support that it has. If we had designed Dart without ever having dynamic
, I suspect we wouldn't be considering adding it now.
If we had designed Dart without ever having
dynamic
, I suspect we wouldn't be considering adding it now.
I think we would be considering adding something that allows dynamically invoking a method without knowing the type declaring it. We might not do so, but we would be considering it, because there would be requests for it.
The implicit downcast, probably not.
I do think that using is
checks and promotion is better than just "knowing" the type, and that pattern matching and destructing gives a way to do that which works for recursive dynamically typed structures like JSON, but there will likely be a core of very dynamic patterns that cannot be implemented without dynamic dispatch.
But I don't know all those cases, so I can't say whether the cost of supporting them is worse than the loss of not supporting them at all.
I am completely in favor of considering avoid_dynamic_calls
a lint rule, along with #125.
Even if we don't remove dynamic
entirely form the language (which I am also in favor of), I think we should make dynamic
the more explicit as possible. Thus, requiting an explicit cast and/or explicit lint ignore commentary would be positive and surely avoid a lot of runtime errors, specially from beginners.
What about an avoid_dynamic
lint that combines the behavior of no_implicit_dynamic as well as more strictness around using dynamic
anywhere. As a rough proposal, disallow:
dynamic
return types - use Object?
instead.dynamic
argument types.dynamic
expression - use (expression as dynamic).dynamicCall();
when it is intended.dynamic
.If we come up with what we think are the total set of best practices around dynamic
, it would be nicer to enable that all together than to have to piece together different bits of config.
I do think that what a lot of our customers want is effectively Dart without dynamic
, but a way to make dynamic calls when there is no other choice. I don't think it matters much whether the syntax for that is verbose like (expression as dynamic).dynamicCall()
or if it had sugar like expression!.dynamicCall()
.
We can align the analysis towards removing dynamic
from the language entirely, without actually making the decision about doing that now.
Implicit or explicit
dynamic
argument types.
This one is very hard to comply with today, without making a lot of exceptions. There are gobs and gobs of generic types (with dynamic
as the bound) where it is not typical, and often not useful, to specify type arguments. I've been trying to get the flutter devtools codebase to be compliant with strict-inference
, which requires type arguments to be explicitly written where they are not inferred, and I think it would make the code much harder to read and a pain to write. A few examples:
Future.delayed()
- even though it's obvious to me that this returns a Future<void>
or Future<Null>
, the rule would require you to write Future<void>.delayed()
(unless the context type results in void
being inferred). (Often Future.delayed
is just await
ed, so no context type.) The same with Future.value()
.Iterable.fold
is an example of this). In devtools, they use showDialog a few times, which is generic with a type parameter T
, but that T
does not show up in the parameter list; it only shows up in the return type, Future<T?>
. I think this happens with injection frameworks as well. Anyhow, when devtools just await
s showDialog
, inference does not infer anything for T
, so dynamic
is used. But it seems arduous, and unhelpful, to require await showDialog<void>
everywhere.FooNode<T>
, which has FooNode
children, you end up writing FooNode<Object?>
or something similar, a lot, where it really doesn't look helpful.These examples can be alleviated, but it requires some work:
Future.void
, and Future.delayed
required, and introduce new equivalent static methods that return Future<void>
. See https://github.com/dart-lang/sdk/issues/53364. Other similar cases.dynamic
bounds with Object?
.Object?
.I would 100% support a lint rule that required you to change bounds from dynamic
. I don't think we have anything that enforces this. It also might be an annoying amount of churn if we're going to eventually change the default bound to Object?
. I imagine that, while that would be a breaking change to change the default bound, it would break an extremely small amount of code. (Except the cases of List and Map maybe, because JSON parsing; those bounds could be explicitly made dynamic
.)
Invoking any member on a
dynamic
expression - use(expression as dynamic).dynamicCall();
when it is intended.
Available to day as avoid_dynamic_calls
lint rule.
Downcasts from
dynamic
.
Available today as language: strict-casts
. Painful to implement in JSON-heavy code, but I'm very curious to see if macros can alleviate this pain (or hide it under the rug).
If we come up with what we think are the total set of best practices around dynamic, it would be nicer to enable that all together than to have to piece together different bits of config.
We could release an analysis options file that enables different bits of config.
I'm starting to come around to re-considering if https://dart.dev/tools/linter-rules/avoid_dynamic_calls should indeed be added to the recommended lint set. Do we think this would trigger a bunch of prevalent code being written today?
There aren't that many dynamic
type variable bounds. The SDK contains none.
In Dart 2.12, the default bound of a generic type variable with no specified bound was changed to Object?
.
In hindsight, that phrasing was problematic, because there was no concept of "default bound" before, so we failed to enumerate the precise places where one should now use Object?
instead of what was used before. The most obvious is that invoking members on a T
-typed value was not doing it on Object
but on Object?
(which might matter for extensions).
But it didn't change what instantiate to bounds did for type variables with no bound, which is to use dynamic
. Because that's what raw types have always used, since the dawn of Dart 1.
It really should (IMO) have changed that to instantiating to the (default) bound. That might have been very breaking, but the null safety migration was the right place for such a breaking change, especially since this one was automatable by just inserting <dynamic>
in all the places where instantiate to bound would have inferred it.
I'd love to change instantiate-to-bounds to not insert spurious dynamic
s. Separately from everything else.
(And I'd love to avoid any other way to accidentally get dynamic
, like var x;
or untyped parameters having type dynamic
. Those are all accidental dynamic
s, where using Object?
is just as good for subtyping and promotion, but won't hide typo'ed member access. I'd even consider making var x;
an "always promote on assignment" variable, which should be a relatively cheap feature to introduce, if we think that's better for users.)
TL;DR: The issue with Future.delayed
is not the bound, it's the instantiation-to-bound algorithm used for raw types.
About:
- Invoking any member on a
dynamic
expression - use(expression as dynamic).dynamicCall();
when it is intended.
The problem with taking that literally is that the result will have type dynamic
, and now you have to deal with that.
Can you do a dynamic invocation on something that does have type dynamic
? If not, doing String name = json["context"]["project"]["name"];
would turn into:
var name = (((json as dynamic)["context"] as dynamic)["project"] as dynamic)["name"] as String;
Sure, you can use as Map<String, dynamic>
instead of as dynamic
, and it might be more efficient, but sure won't be shorter.
If you can, then var name = (json as dynamic)["context"]["project"]["name"] as String;
will be enough. But that also means that any API that does return dynamic
will opt you in, and it'll be hard to tell the difference between returnsDynamic().arglebargle
and Map()[42].argleBargele
. The former explicitly returns dynamic. The latter is a Map<dynamic, dynamic>
, by instantiate to bounds today, which just as explicitly returns dynamic
.
(Unless we want to distinguish a verbatim dynamic
from a generic bound to dynamic
, which is going to get complicated.)
(Example valid only until someone creates an extension type wrapper for JSON. I know I will. :wink:)
In Dart 2.12, the default bound of a generic type variable with no specified bound was changed to
Object?
. But it didn't change what instantiate to bounds did for type variables with no bound, which is to usedynamic
. It really should (IMO) have changed that to instantiating to the (default) bound. I'd love to change instantiate-to-bounds to not insert spuriousdynamic
s.
Yes, this is what I meant, sorry for the imprecision. Instead of, "There are gobs and gobs of generic types (with dynamic
as the bound)" I should have said "There are gobs and gobs of generic types (with no specified bound, in which case instantiate-to-bounds uses dynamic
.)
Instead of "Alternatively, change the default bound to Object?
." I should have said "change the default bound used in instantiate-to-bounds to be Object?
."
Can you do a dynamic invocation on something that does have type dynamic?
Nope, that's the goal :D
(Example valid only until someone creates an extension type wrapper for JSON. I know I will. 😉)
Do you mean something like these? https://github.com/flutter/devtools/pulls?q=is%3Apr+extension+is%3Aclosed+author%3Asrawlins Or more generic?
Would it help if the language allowed a type parameter bound to be extends void
which means "in the absence of any other context type, instantiate with void
"?
@munificent wrote:
[allow] a type parameter bound to be
extends void
Interesting! That's actually a kind of "don't use this member when the receiver type is raw" mechanism:
typedef Void = void;
class C<X extends Void> {
final X x;
C(this.x);
void foo() {}
}
void main() {
C c = C(1);
c.foo(); // No problem.
print(c.x); // Error!
}
Yeah, it's basically allowing bounds that let you pick which top type you want if the type parameter is unconstrained. We could also treat the default instantiate-to-bound as Object?
and then allow extends dynamic
for backwards compatibility in places where a user really does want that to be what inference defaults to.
The problem with taking that literally is that the result will have type
dynamic
, and now you have to deal with that. Can you do a dynamic invocation on something that does have typedynamic
? If not, doingString name = json["context"]["project"]["name"];
would turn into:var name = (((json as dynamic)["context"] as dynamic)["project"] as dynamic)["name"] as String;
Sure, you can use
as Map<String, dynamic>
instead ofas dynamic
, and it might be more efficient, but sure won't be shorter.
FWIW I would personally be fine with this verbosity, but I agree that many users would find this annoying.
There are also alternative ways to write this today, both approaches that give more opportunity to custom handle inputs with unexpected shape:
var name = switch(json) {
{'context': {'project': {'name':String name}}} => name,
_ => throw FormatException('Expected a Map with <format>'),
};
Or a terse approach:
var {'context': {'project': {'name': String name}}} = json;
Also folks could just disable the lint for a particular file.
Outside of toy CLI scripts, everyone uses some sort of JSON code generation solution at this point, or a wrapper/helper.
I think
avoid_dynamic_calls
would be a great addition to the lint rule sets. Either in core or in recommended.https://dart-lang.github.io/linter/lints/avoid_dynamic_calls.html
It is currently marked
experimental
. See https://github.com/dart-lang/linter/issues/2666