Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Analyzer doesn't catch that method argument has wrong signature #30052

Closed
Hixie opened this issue Jun 29, 2017 · 16 comments
Closed

Analyzer doesn't catch that method argument has wrong signature #30052

Hixie opened this issue Jun 29, 2017 · 16 comments
Assignees
Labels
area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. P2 A bug or feature request we're likely to work on type-bug Incorrect behavior (everything from a crash to more subtle misbehavior)
Milestone

Comments

@Hixie
Copy link
Contributor

Hixie commented Jun 29, 2017

The analyzer doesn't catch this static type error:

void foo(bool callback()) { }

class Bar {
  void callFoo() {
    foo(method); // <-- this call is illegal ("method" has the wrong signature)
  }

  void method() { }
}

void main() {
  new Bar().callFoo();
}

Analyzer output:

  lint • Document all public members at test.dart:1:6 • public_member_api_docs
  lint • Document all public members at test.dart:3:7 • public_member_api_docs
  lint • Document all public members at test.dart:4:8 • public_member_api_docs
  lint • Document all public members at test.dart:8:8 • public_member_api_docs

VM output (checked mode):

Unhandled exception:
type '() => void' is not a subtype of type '() => bool' of 'callback'
#0      foo (test.dart:1:15)
#1      Bar.callFoo (test.dart:5:5)
#2      main (test.dart:12:13)
#3      _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:265)
#4      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:151)

The analyzer does catch the problem if I make callFoo and method top-level functions or statics on the Bar class. It's only if they're methods that it's a problem.

@bwilkerson bwilkerson added area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. P2 A bug or feature request we're likely to work on type-bug Incorrect behavior (everything from a crash to more subtle misbehavior) labels Jun 29, 2017
@leafpetersen
Copy link
Member

This is working as intended. Passing method to foo is an implicit downcast. Without additional flags (--no-implicit-cast) strong mode allows implicit downcasts between subtype related types unless it can prove that there is no way for the cast to succeed. If you make method top level, then strong mode knows that the cast will fail and issues an error. But without making closed world assumptions, we cannot know that there is not a subclass of Bar that overrides method at a the type () -> bool, which would allow the cast to succeed.

@Hixie
Copy link
Contributor Author

Hixie commented Jun 29, 2017

Isn't that why we have covariant?

This is extremely confusing (especially that it only applies to methods). I don't see how the code above could ever be correct (it will always throw). Bar isn't abstract. method isn't abstract.

I think we should change our intention here. If someone wants to write code like this they should have to explicitly opt-in the same way we do with accepting subclasses with covariant.

@matanlurey
Copy link
Contributor

I was under the impression it was Flutter that wanted strong-mode-implicit-downcasts disabled by default?

@leafpetersen
Copy link
Member

leafpetersen commented Jun 29, 2017

No this isn't related to covariant. Here's an example program where that code won't ever throw:

void foo(bool callback()) { }

class Bar {
  void callFoo() {
    foo(method); // <-- this call is illegal ("method" has the wrong signature)
  }

  void method() { }
}

class Baz extends Bar {
  bool method() { return true; }
}

void main() {
  new Baz().callFoo();
}

Overriding method with something that returns bool is perfectly valid in Dart (it's ok to return something in a context that is just ignoring whatever gets returned).

An the override here means that the implicit downcast in callFoo works at runtime.

If we have a closed world assumption (that is, we assume that we can see all of the classes that will ever be defined all at once), then we can know whether or not method is ever overridden and could issue a warning that the implicit downcast is guaranteed to fail. We don't want to require this at the language level, since we need to support modular tools that can't make closed world assumptions.

--no-implicit-cast would catch this, and it's still on my plate to add a --assignment-cast flag to allow you to make progress with trying to catch more of these statically.

@Hixie
Copy link
Contributor Author

Hixie commented Jun 29, 2017

That would be fine (well, sort of, it seems really dubious to me still) if Bar was abstract, but it's not, so you should be able to use it, and you can't, because it'll throw.

@matanlurey It's a lot more nuanced than that. :-) I think most of the places where we want downcasts are now annotated with covariant. Most of the remaining places are assignments and on the long run we'll move all of those to using as, I think. We don't use as today because we don't want the type check in release builds and assignment lets us get around that.

What I want personally is for the language to be intuitive, and I feel that the case here is a great example of unintuitive behavior.

@leafpetersen
Copy link
Member

What I want personally is for the language to be intuitive, and I feel that the case here is a great example of unintuitive behavior.

Agreed. My personal preference is to avoid all implicit downcasts, but I'm very open to identifying specific subsets of them that are especially pernicious. Do you have something systematic in mind here? Implicit downcasts of callbacks? Or just making the strong mode "impossible cast" warning fire for tearoffs as well, even though those aren't actually truly impossible casts?

cc @floitschG @lrhn @eernstg @munificent

@leafpetersen leafpetersen reopened this Jun 29, 2017
@leafpetersen leafpetersen self-assigned this Jun 29, 2017
@munificent
Copy link
Member

I'm not totally clear on what behavior you want to have. I see a couple of options for your example:

1. Don't report an error statically, and don't report an error at runtime.

I don't think this is feasible because it violates soundness. Consider:

void foo(bool callback()) {
  bool b = callback(); // <--
}

class Bar {
  void callFoo() {
    foo(method);
  }

  void method() { }
}

class SubBar extends Bar {
  int method() => 123;
}

void main() {
  new SubBar().callFoo();
}

On the marked line, b would end up with value 123. That's not good.

2. Don't report an error statically, but check it at runtime.

This is what we do now. As Leaf noted, there are some ways the code could potentially be valid where the runtime check will succeed. Personally, I don't like that the static checker assumes this will succeed. I'd rather you get an error and require an explicit cast. I think you feel the same way?

3. Report an error statically.

If we didn't do the implicit downcast, then you get an error here:

void foo(bool callback()) { }

class Bar {
  void callFoo() {
    foo(method); // Can't assign a value of type () -> void to a parameter of type () -> bool.
  }

  void method() { }
}

You could fix the error by doing a manual downcast:

void foo(bool callback()) { }

class Bar {
  void callFoo() {
    foo(method as bool Function());
  }

  void method() { }
}

Is that what you have in mind?

That would be fine (well, sort of, it seems really dubious to me still) if Bar was abstract, but it's not, so you should be able to use it, and you can't, because it'll throw.

I don't follow how abstract affects things. Can you walk me through it?

@Hixie
Copy link
Contributor Author

Hixie commented Jun 30, 2017

@munificent #3 is what I had in mind for this particular case. Abstractness matters only insofar as if the class was abstract, then it's possible the contract would be for the subclass to always override it? But even then it'd be pretty weird.

@leafpetersen I would approach the problem the other way. Downcasts bad, except for specific cases. One of those cases is when an argument is marked as covariant. Another is in the as expression. What are other cases?

@lrhn
Copy link
Member

lrhn commented Jun 30, 2017

The problem comes from the fact that bool Function() is assignable to void Function().
Because of that, you can write.

bool foo() => true;
void Function() bar = foo;  // allowed
bool Function() baz = bar;  // implicit down-cast that *will* succeed at runtime.

The potentially unsafe down-cast is where strong mode puts the runtime-check.

Now, an analyzer can be smarter than that. Leaf showed an example where the code works, but if the analyzer has global knowledge, it can know that no subclass of Bar overrides method to something that actually works.
The language allows the implicit down-cast because, well, it has implicit down-casts. They are very convenient in many cases and we like them. They do hide some type-errors, but removing implicit down-cast would make you need casts, which duplicates the type, for cases like:

List<String> list = iterable as List<String>;  

@Hixie
Copy link
Contributor Author

Hixie commented Jun 30, 2017

Removing the ability to assign an Iterable (that doesn't implement List) to a variable of type List would be amazing. I make that mistake multiple times a day.

@leafpetersen
Copy link
Member

@lrhn you misspelled

var list = iterable as List<String>;

I'd be ok with adding an operator to succinctly cast to the context type, though you can already write this as a function:

T cast<S, T extends S>(S x) => x as T;
List<String> list = cast(iterable);

@Hixie

I would approach the problem the other way. Downcasts bad, except for specific cases. One of those cases is when an argument is marked as covariant. Another is in the as expression. What are other cases?

I don't have any in mind, but as I say, I prefer to avoid implicit downcasts. For your covariant example, do you mean that you would like the following to be allowed?

class A {
  void foo(covariant List<int> x) {}
}

void test(Iterable<int> y) {
  new A().foo(y);  // implicit downcast allowed here?
}

@Hixie
Copy link
Contributor Author

Hixie commented Jun 30, 2017

I just meant the current semantics (allowing subclassing to be covariant instead of only contravariant).

@eernstg
Copy link
Member

eernstg commented Jul 3, 2017

There are a couple of things which haven't been mentioned in this discussion so far:

(1) void is currently being generalized, and we expect to have a notion of 'voidness preservation' (which is a static check intended to help developers avoid using void values). With such a check in place, the cast from void Function() to bool Function() would be a voidness preservation violation (in all the variants that we've considered, hence almost certainly also in the one that we end up using), and this would cause a diagnostic message on foo(method), because "the invocation of foo forgets that the returned value from method should be ignored".

So let's consider this discussion to be about the types Object Function() and bool Function() instead of void Function() and bool Function() --- then it's a clean subtyping issue, rather than a mixed subtyping/voidness issue. (Right now we don't have any voidness preservation checks, but it's an obvious source of confusion given the ongoing discussions).

(2) Ian mentioned that since method and Bar in the original example aren't abstract, the code should incur some diagnostic message ("this is guaranteed to fail, so please notify me!"). Leaf touched on the same topic by using the phrase "impossible cast". So this discussion isn't just about "allow/disallow implicit downcasts", it's about special cases where there is additional information about a given downcast which makes it particularly unsafe.

One special case is where an expression of type T is statically known to have exactly that type (e.g., new C() when it's invoking a generative constructor; const expressions; literals). Any non-trivial downcast applied to such an expression is statically guaranteed to fail, so it makes sense to flag such a downcast as an "impossible" cast and thus outlaw it.

Another special case could be the original example where an instance of the Bar class will invoke foo(method) which is guaranteed to fail. However, it's not that statement which is guaranteed to fail, it's that statement when executed by a direct instance of Bar (not a subclass/subtype) --- it might be safe in any given subclass as illustrated by Leaf.

We could introduce this new concept: "there exists a class C0 such that if this is a direct instance of C0 then this cast is guaranteed to fail". In particular, we could consider the situation where C0 is simply the class where the cast occurs. This would work nicely for the foo(method) example.

If we were to do this then we would get diagnostic messages on many downcasts applied to this, because this is considered to have an exact type in all concrete classes. In practice, it may or may not be a good trade-off.

Also note that this is not the same thing as saying that instance method tear-offs are considered to have an exact type (which Leaf mentioned that we could do), which would have a different set of trade-offs.

@Hixie
Copy link
Contributor Author

Hixie commented Jul 3, 2017

Can you give an example of the this issue?

@natebosch
Copy link
Member

Should this be considered as another request for #31410

Is there value in keeping both issues open?

@aadilmaan aadilmaan removed this from the Future milestone Jun 4, 2019
@aadilmaan aadilmaan modified the milestones: Future, D25 Release Jun 4, 2019
@srawlins
Copy link
Member

I think downcasts are being wholly re-addressed with the Null Safety type system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. P2 A bug or feature request we're likely to work on type-bug Incorrect behavior (everything from a crash to more subtle misbehavior)
Projects
None yet
Development

No branches or pull requests