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

[Extension type] Add a @redeclare annotation #53121

Closed
pq opened this issue Aug 3, 2023 · 14 comments
Closed

[Extension type] Add a @redeclare annotation #53121

pq opened this issue Aug 3, 2023 · 14 comments
Assignees
Labels
analyzer-pkg-meta Issues related to package:meta area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. implementation Track the implementation of a specific feature (use on area-meta issue, not issues for each tool) P3 A lower priority bug or feature request type-enhancement A request for a change that isn't a bug

Comments

@pq
Copy link
Member

pq commented Aug 3, 2023

Extension types don't override but rather redeclare so we should probably discourage/disallow the use of the @override annotation on extension type members and provide an alternative annotation such as @redeclare to communicate when an extension type member is intended to be redeclaring. (Along with that we should consider a lint, like unnecessary_redeclare to flag misplaced annotations (e.g., like unnecesssary_overrides).

/cc @dart-lang/language-team @scheglov @bwilkerson @jacob314

@pq pq added area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. P3 A lower priority bug or feature request type-enhancement A request for a change that isn't a bug implementation Track the implementation of a specific feature (use on area-meta issue, not issues for each tool) analyzer-pkg-meta Issues related to package:meta labels Aug 3, 2023
@pq pq self-assigned this Aug 3, 2023
@mit-mit
Copy link
Member

mit-mit commented Aug 4, 2023

Sorry, what's the difference between overriding and redeclaring?

@lrhn
Copy link
Member

lrhn commented Aug 4, 2023

Overriding is virtual, and replaces the existing method on all instances of the subclass, whether they are known to have the subclass interface or not. An override must have a compatible signature.

A redeclaration doesn't need to have a compatible signature. It shadows the original when declared on an extension type which also implements a superior with a same-named member.

The former is providing a specialized version of the overridden member, the latter is just providing a completely new member with the same name.

The idea behind the warning here would be that the name collision might be accidental, and by making you provide an annotation, you can state that it was deliberate. Then we can warn you if you make an accidental shadowing, which is easier to do than an accidental override, since you don't have to match the signature, and we can warn you if you claim to be shadowing something, but isn't.

I'm worried about the latter. Shadowing is unrelated to the thing it shadows, so if that goes away, it really only needs to be an info that the annotation is unnecessary. It doesn't change the meaning of the used-to-be-shadowing member. Making it a warning, which some will then make an error, is more fragile than necessary. Or make it a lint, so it's opt-in, unlike the unnecessary override warning.

For the unannotated redeclaration, that could be a lint to, maybe the same one endless both. I won't put the redeclaration value in the platform libraries, so it would go in package:meta.

Also consider allowing the use of override, which then requires it to have a compatible member signature to at least one member its shadowing.

@mit-mit
Copy link
Member

mit-mit commented Aug 4, 2023

Got it. @redeclare wasn't a term I'd heard before. Did we consider something like @shadow ?

@lrhn
Copy link
Member

lrhn commented Aug 4, 2023

I'm not sure where the "redeclare" name comes from. I'm not even sure we need the annotation, but if there is an audience for it, it's definitely a lint we can provide.

I think "shadow" is a little too specific, and transitive (I'd expect it to say what it shadows).

(If we allowed new on a declaration, to make it not introduce a virtual override, the opposite of override, it would be fitting here.)

@bwilkerson
Copy link
Member

I'm not sure where the "redeclare" name comes from.

I comes from the feature specification, which says:

Hence, we use a different word to describe the relationship between a member named m of a superinterface, and a member named m which is declared by the subinterface: We say that the latter redeclares the former.

@eernstg
Copy link
Member

eernstg commented Aug 7, 2023

@pq wrote:

we should probably discourage/disallow the use of the @override annotation on extension type members and provide an alternative annotation such as @redeclare

That would be great!

I do think a case can be made for the ability to announce explicitly that a given redeclaration relationship is intended.

In particular, it seems very plausible to me that some extension type hierarchies will be designed such that redeclarations never occur. Indeed, several of the early proposals about extension types (known as views at the time, I think) enforced this property by making every redeclaration a compile-time error.

The motivation for avoiding redeclarations would be that a hierarchy with no redeclarations will behave in a manner which is more like the behavior of a class hierarchy: Assume that e has static type T and S is a supertype of T, assume that T has a method named m and S also has a member named m. The latter is then also a method, and e.m() behaves exactly the same as (e as S).m(). In other words, behaviors are preserved across an upcast; with regular classes this is ensured because of late binding, and with extension types with no redeclarations it is ensured because late binding would be a no-op (the most specific implementation of any member is always the statically known one), so it doesn't matter that extension types don't have late binding.

Developers and organizations who wish to outlaw redeclarations could then enable the lint and check that @redeclare is never used.

Conversely, other developers/organizations might prefer to allow redeclarations. In this case it would make sense to enable the lint and have the @redeclare annotation on every redeclaration. This would serve as a reminder for anyone who looks up the declaration, because they need to remember that e.m() and (e as S).m() (where e has type T and T <: S as above) may run two completely unrelated pieces of code (so it's not a good idea to assume that "I already know what m will do").

All in all, I think it's fair to say that @redeclare is similar to @override, but sufficiently different to justify a firm distinction (that is, using a different annotation). Also, @redeclare is arguably even more justified than @override because there is no guarantee that a redeclaration and the redeclared declaration (that is, the "overriding" respectively "overridden" declaration) are related in any way, and we can't rely on late binding to ensure that "we always get the right one".

@bwilkerson
Copy link
Member

A minor nit, unless someone cares deeply. :-) The approach we've consistently taken in the analyzer is that any diagnostics related to annotations are considered to be warnings (enabled by default) because the user has explicitly told us what their expectations are. If we add a declaration of redeclare, then users will automatically get diagnostics when a redeclaration occurs without the annotation or when the annotation is used where it shouldn't be, such as on a non-member declaration or a member that doesn't redeclare anything from a supertype.

But if we want a diagnostic that prohibits the use of redeclare, then that would be a lint (disabled by default) because we'd want the user to explicitly opt in to that prohibition.

@lrhn
Copy link
Member

lrhn commented Aug 7, 2023

That sounds fine to me: A warning to have @redeclare on something which doesn't actually shadow anything, and a lint to warn if you lack a @redeclare on something which does shadow, or if you shadow at all (whether with or without @redeclare`, it won't save you from "no_redeclaratons").

@eernstg
Copy link
Member

eernstg commented Aug 7, 2023

OK, // ignore: no_redeclarations would still be available for the rare exception where someone wants to be saved. ;-)

@pq
Copy link
Member Author

pq commented Aug 7, 2023

What a great discussion. Thanks for all the added consideration!

I'll take a look at the annotation and diagnostic/lint work.

Adding @MaryaBelanger for context: brace yourself for some doc reviews! 😉

@pq pq changed the title [Extension type] Consider a @redeclare annotation [Extension type] Add a @redeclare annotation Aug 7, 2023
@pq
Copy link
Member Author

pq commented Aug 7, 2023

Developers and organizations who wish to outlaw redeclarations could then enable the lint and check that @redeclare is never used.

Do we anticipate a use case where only some redeclarations are meant to be disallowed? That is, should we allow specific declarations to be non-redeclarable? (Something analogous to @nonVirtual?)

@bwilkerson
Copy link
Member

While that's certainly possible, my guess is that we should wait to add anything similar to nonVirtual until we have clear evidence that users want it. I might say the same for the lint, honestly. But I do think that we want redeclare when we release the feature.

@eernstg
Copy link
Member

eernstg commented Aug 8, 2023

Do we anticipate a use case where only some redeclarations are meant to be disallowed?

I think this would be more like a type hierarchy thing than a member declaration thing: With no_redeclarations, developers would be able to rely on a simpler model of the software ("there's nothing special about extension type instance member invocation"). If redeclarations exist then the model would have to be more elaborate ("extension type instance members are resolved statically, so you need to be careful about the static type of e when making calls like e.foo()").

In particular, with no_redeclarations it is probably feasible for clients to ignore the fact that a given type hierarchy is expressed using extension types rather than classes. In contrast, developers who are using an extension type hierarchy where some members are redeclared will need to understand that those members are resolved statically (so they can't rely on the language semantics to choose the right implementation for the given receiver, they're going to get exactly the one which is known at compile time at the call site).

So it probably doesn't make much sense to avoid this kind of complexity on a per-member basis, it's more like a package level or organizational level decision. A lint which is enabled/disabled on a per-package basis would probably be fine for that purpose, and @nonRedeclared probably won't be very helpful.

Finally, it should be noted that we've had extension declarations for years, and they are statically resolved, and I've only rarely heard any complaints about surprising "overrides":

// This is the one we know about.
extension on num {
  void foo() => print("Good ole foo won't hurt a fly!");
}

// This one slipped under the radar.
extension on int {
  void foo() => print("Hehe! I'll swat that one!");
}

void main() {
  num n = 1;
  n.foo(); // All flies ok.
  if (n is! int) return;
  n.foo(); // Oops!
}

Of course, what I really want here is IDE support for dispatch visualization: Statically resolved invocations have pale blue background color, OO dispatched invocations have pale green background color, and dynamic invocations have blinking red background color. ;-)

copybara-service bot pushed a commit that referenced this issue Aug 9, 2023
See: #53121

Change-Id: I812bc49b58cdf15e9c0c8bcc62a846b51f5dff70
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/319100
Commit-Queue: Phil Quitslund <pquitslund@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
copybara-service bot pushed a commit that referenced this issue Aug 9, 2023
See: #53121

Change-Id: I994aac6c733704ac7adc4a7923ee733cdc77aacb
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/319561
Commit-Queue: Phil Quitslund <pquitslund@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
copybara-service bot pushed a commit that referenced this issue Aug 15, 2023
See: #53121

Change-Id: I39294952a3adf0f037417d6e04811e3294128e6d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/320886
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Phil Quitslund <pquitslund@google.com>
copybara-service bot pushed a commit that referenced this issue Aug 17, 2023
See: #53121

Change-Id: I324f8a14820da2e9e5b714f9f1d88c647f9cfebc
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/321424
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Phil Quitslund <pquitslund@google.com>
@srawlins
Copy link
Member

srawlins commented Oct 9, 2023

This annotation was shipped in meta 1.10.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
analyzer-pkg-meta Issues related to package:meta area-analyzer Use area-analyzer for Dart analyzer issues, including the analysis server and code completion. implementation Track the implementation of a specific feature (use on area-meta issue, not issues for each tool) P3 A lower priority bug or feature request type-enhancement A request for a change that isn't a bug
Projects
None yet
Development

No branches or pull requests

6 participants