Skip to content

Commit dccf720

Browse files
committed
Revamp class modifiers to close loopholes.
Based on Lasse's proposal at: https://gist.github.com/lrhn/981d4528fbb995c23afa0f9436209537 Fix #2755. Fix #2757.
1 parent 3061f25 commit dccf720

File tree

1 file changed

+193
-79
lines changed

1 file changed

+193
-79
lines changed

accepted/future-releases/class-modifiers/feature-specification.md

Lines changed: 193 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Class modifiers
22

3-
Author: Bob Nystrom
3+
Author: Bob Nystrom, Lasse Nielsen
44

55
Status: Accepted
66

7-
Version 1.3
7+
Version 1.4
88

99
Experiment flag: class-modifiers
1010

@@ -24,7 +24,7 @@ Informally, the new syntax is:
2424

2525
* `interface`: As a modifier on a class or mixin, allows the type to be
2626
implemented but not extended or mixed in. In other words, it takes away
27-
being used as a subclass through extension or mixing in.
27+
being able to inherit from the type.
2828

2929
* `final`: As a modifier on a class or mixin, prohibits extending,
3030
implementing, or mixing in.
@@ -420,6 +420,51 @@ This proposal takes the last option where types have exactly the restrictions
420420
they declare but a lint can be turned on for users who want to be reminded if
421421
they re-add a capability in a subtype.
422422
423+
### Inherited restrictions
424+
425+
Allowing you to ignore restrictions on your own types allows some useful
426+
architectural patterns, but it's important that doing so doesn't let you ignore
427+
restrictions on types from *other* libraries because then you could break the
428+
invariants the library expects. In particular, consider:
429+
430+
```dart
431+
// lib_a.dart
432+
base class A {
433+
void _private() {
434+
print('Got it.');
435+
}
436+
}
437+
438+
callPrivateMethod(A a) {
439+
a._private();
440+
}
441+
```
442+
443+
This library declares a class and marks it `base` to ensure that every instance
444+
of `A` in the program must be an `A` or a class that inherits from it. That in
445+
turn ensures that the call to `_private()` in `callPrivateMethod()` is always
446+
safe.
447+
448+
Now consider:
449+
450+
```
451+
// lib_b.dart
452+
import 'lib_a.dart';
453+
454+
base class B extends A {} // OK: Inheriting.
455+
456+
class C implements B {} // OK: Ignoring restriction on own type B.
457+
```
458+
459+
These two class declarations each seem to be fine. But put together, the result
460+
is a class `C` that is a subtype of `A` but doesn't inherit from it and doesn't
461+
have the `_private()` method that lib_a.dart expects.
462+
463+
So we want to allow libraries to ignore restrictions on their own types, but we
464+
need to be careful that doing so doesn't break invariants in *other* libraries.
465+
In practice, this means that when a class opts out of being implemented using
466+
`base` or `final`, then that particularl restriction can't be ignored.
467+
423468
## Mixin classes
424469

425470
In line with Dart's permissive default nature, Dart allows any class declaration
@@ -458,7 +503,8 @@ This proposal builds on the existing sealed types proposal so the grammar
458503
includes those changes. The full set of modifiers that can appear before a class
459504
or mixin declaration are `abstract`, `sealed`, `base`, `interface`, `final`, and `mixin`.
460505

461-
*The modifiers do not apply to other declarations. This includes `enum` declarations.
506+
*The modifiers do not apply to other declarations like `enum`, `typedef`, or
507+
`extension`.*
462508

463509
Many combinations don't make sense:
464510

@@ -530,62 +576,142 @@ mixinModifier ::= 'sealed' | 'base' | 'interface' | 'final'
530576

531577
## Static semantics
532578

579+
A pair of definitions:
580+
581+
* A *pre-feature library* is a library whose language version is lower than
582+
the version this feature is released in.
583+
584+
* A *post-feature library* is a library whose language version is at or above
585+
the version this feature is released in.
586+
587+
### Basic restrictions
588+
533589
It is a compile-time error to:
534590

535-
* Extend a class marked `interface`, `final` or `sealed` outside of the library
536-
where it is declared.
537-
538-
* Implement the interface of a class or mixin marked `base`, `final` or `sealed`
539-
outside of the library where it is declared.
540-
541-
* Mix in a mixin or mixin class marked `interface`, `final` or `sealed` outside
542-
of the library where it is declared.
591+
* Extend a class marked `interface`, `final` or `sealed` outside of the
592+
library where it is declared.
543593

544-
* Extend a class marked `base` outside of the library where it is declared
545-
unless the extending class is marked `base` or `final`. *This ensures that a
546-
subtype can't escape the `base` restriction of its supertype by offering its
547-
_own_ interface that could then be implemented without inheriting the
548-
concrete implementation from the supertype.* <!-- Needs to account for `sealed` -->
594+
* Implement the interface of a class or mixin marked `base`, `final` or
595+
`sealed` outside of the library where it is declared.
549596

550-
* Mix in a mixin or mixin class marked `base` outside of the library where it
551-
is declared unless the class mixing it in is marked `base` or `final`. *As
552-
with the previous rule, ensures you can't get a backdoor interface on a
553-
mixin that doesn't want to expose one.*
554-
555-
* Apply `mixin` to a class whose superclass is not `Object` or that declares a
556-
_non-trivial generative constructor_.
557-
*Such a class can have an `extends` clause of the form `extends Object`,
558-
or no `extends` clause. It cannot have any `with` clause.*
597+
* Mix in a mixin or mixin class marked `interface`, `final` or `sealed`
598+
outside of the library where it is declared.
599+
600+
There is no direct restriction on types allowed in `on` clauses of mixin
601+
declarations.
602+
603+
A typedef can't be used to subvert these restrictions or any of the restrictions
604+
below. When extending, implementing, or mixing in a typedef, we look at the
605+
library where class or mixin the typedef resolves to is defined to determine if
606+
the behavior is allowed. *Note that the library where the _typedef_ is defined
607+
does not come into play. Typedefs cannot be marked with any of the new
608+
modifiers.*
609+
610+
### Disallowing implementation
611+
612+
It is a compile-time error if a subtype of a declaration is marked `base` or
613+
`final` is not marked `base`, `final`, or `sealed`. This restriction applies to
614+
both direct and indirect subtypes and along all paths that introduce subtypes:
615+
`implements` clauses, `extends` clauses, `with` clauses, and `on` clauses. This
616+
restriction applies even to types within the same library.
617+
618+
*Once the ability to use as an interface is removed, it cannot be reintroduced
619+
in a subtype. If a class is marked `base` or `final`, you may still implement
620+
the class's interface inside the same library, but the implementing class must
621+
again be marked `base`, `final`, or `sealed` to avoid it exposing an
622+
implementable interface.*
623+
624+
Further, while you can ignore some restrictions on declarations within the same
625+
library, you can't use that to ignore restrictions inherited from other
626+
libraries.
559627

560-
A _trivial generative constructor_ is a non-redirecting generative constructor
561-
which has
628+
We say a class or mixin declaration `D` *can't be implemented locally* if it
629+
extends, mixes in, implements, or has as on type any declaration `S` where:
562630

563-
* an empty parameter list,
564-
* no initializer list (no `: ...`),
565-
* no constructor body (only `;`),
566-
* and is not marker `external`.
631+
* `S` is from another library than `D`, and `S` has the modifier `base`,
632+
`final` or `sealed`, or
567633

568-
Any other generative constructor is non-trivial.
569-
A trivial generative constructor may be `const` and may be a named constructor.
634+
* `S` is from the same library as `D`, and `S` can't be implemented locally.
570635

571-
*Declaring a trivial generative constructor allows the class to be used as
572-
both a mixin and as a superclass, even if it also declares other factory constructors
573-
which suppress the default constructor.*
636+
Otherwise, `D` can be implemented locally. It is a compile-time error if:
574637

575-
* Mix in a class not marked `mixin` which has a superclass other than `Object`.
638+
* A class or mixin declaration `D` can't be implemented locally, and `D` is
639+
not marked `base`, `final` or `sealed`.
576640

577-
* Mix in a class not marked `mixin` declared in a library with a language version including
578-
this feature, if the mixin application is not in the same library,
579-
or if the class has any non-trivial generative constructor.
641+
* A class or mixin declaration `D` implements the interface of a class or
642+
mixin declaration `S`, declared in the same library as `D`, and `S` can't be
643+
implemented locally.
580644

581-
* Mix in a class not marked `mixin` from a library with a language version older than
582-
the version this feature ships in, if the class declares any generative constructor.
645+
### Mixin restrictions
583646

584-
A typedef can't be used to subvert these restrictions. When extending,
585-
implementing, or mixing in a typedef, we look at the library where class or
586-
mixin the typedef resolves to is defined to determine if the behavior is
587-
allowed. *Note that the library where the _typedef_ is defined does not come
588-
into play. Typedefs cannot be marked with any of the new modifiers.*
647+
There are a few changes around mixins to support `mixin class` and disallow
648+
using normal `class` declarations as mixins while dealing with language
649+
versioning and backwards compatibility.
650+
651+
Currently, a class may only be used as a mixin if it has a default constructor.
652+
This prevents the class from defining a `const` constructor or any factory
653+
constructors. We loosen this somewhat. Define a *trivial generative constructor*
654+
to be a generative constructor that:
655+
656+
* Is not a redirecting constructor,
657+
658+
* declares no parameters,
659+
660+
* has no initializer list (no `: ...` part),
661+
662+
* has no body (only `;`), and
663+
664+
* is not `external`. *An `external` constructor is considered to have an
665+
externally provided initializer list and/or body.*
666+
667+
A trivial constructor may be named or unnamed, and `const` or non-`const`. A
668+
*non-trivial generative constructor* is a generative constructor which is not a
669+
trivial generative constructor.
670+
671+
It's a compile-time error if:
672+
673+
* A `mixin class` declaration has a superclass other than `Object`. *The
674+
declaration is limited to an `extends Object` clause or no `extends` clause,
675+
and no `with` clauses. The class grammar prohibits `on` clauses.*
676+
677+
* A `mixin class` declaration declares any non-trivial generative constructor.
678+
*It may declare no constructors, in which case it gets a default
679+
constructor, or it can declare factory constructors and/or trivial
680+
generative constructors.*
681+
682+
These rules ensure that when you mark a `class` with `mixin` that it *can* be
683+
used as one.
684+
685+
A class not marked `mixin` can still be used as a mixins when the class's
686+
declaration is in a pre-feature library. Post-feature libraries can also mix in
687+
their own classes that aren't marked `mixin` as long as the class supports it.
688+
Specifically:
689+
690+
It's a compile-time error to for a declaration in library `L` to mix in a
691+
non-`mixin` class declaration `D` from library `K` if any of:
692+
693+
* The superclass of `D` is not `Object`,
694+
695+
* `K` is a pre-feature library, and `D` declares any constructors, or
696+
697+
*For pre-feature libraries, we can't tell if the intent of `class` was "just
698+
a class" or "both a class and a mixin". For compatibility, we assume the
699+
latter, even if the class is being used as a mixin in a post-feature library
700+
and where it does happen to be possible to distinguish those two intents.*
701+
702+
* `K` is a post-feature library, and any of:
703+
704+
* `K` is not the same library as `L`,
705+
706+
* `D` is a mixin-application class declaration (has the form `class D =
707+
...;`), or
708+
709+
* `D` declares any non-trivial generative constructors.
710+
711+
*When a class is in a library where it possible to distinguish between
712+
whether the class is intended to use it as a mixin or not, the author is
713+
obligated to document the intent, and that intent applies to all other
714+
libraries regardless of their version.*
589715

590716
### `@reopen` lint
591717

@@ -616,44 +742,26 @@ The changes in this proposal are guarded by a language version. This makes the
616742
restriction on not allowing classes to be used as mixins by default
617743
non-breaking.
618744

619-
Let `n` be the language version this proposal ships in. Then:
620-
621-
* `base`, `interface`, `final`, `sealed` and `mixin` can only be applied to classes and
622-
mixins in libraries whose language version is `>= n`.
745+
* `base`, `interface`, `final`, `sealed` and `mixin` can only be applied to
746+
classes and mixins in post-feature libraries.
623747

624748
* When the `base`, `interface`, `final`, `mixin`, or `sealed` modifiers are
625749
placed on a class or mixin, the resulting restrictions apply to all other
626-
libraries, even libraries whose version is `< n`.
750+
libraries, even pre-feature libraries.
627751

628752
*In other words, we gate being able to _author_ the restrictions to
629-
libraries on version `n`. But once a type has those restrictions, they apply
753+
post-feature libraries. But once a type has those restrictions, they apply
630754
to all other libraries, regardless of the versions of those libraries.
631755
"Ignorance of the law is no defense."*
632756

633-
**TODO:** Decide if we want to carve out an exception to this rule for the
634-
SDK core libraries.
757+
* We would like to add modifiers to some classes in platform (i.e. `dart:`)
758+
libraries when this feature ships. But we would also like to not immediately
759+
break existing code. To avoid forcing users to immediately migrate,
760+
declarations in pre-feature libraries can ignore modifiers on some
761+
declarations in platform libraries. Instead, users will only have to abide
762+
by those restrictions when they upgrade their library's language version.
635763

636-
* A class declaration in a library whose language version is `< n` can be used
637-
as a mixin as long as the class meets the existing mixin restrictions
638-
(superclass is `Object`, declares no generative constructor). This is is
639-
true even if the library where the class is being used as a mixin is `>=
640-
n`.
641-
642-
*For libraries whose version is `< n`, we can't tell if the intent of
643-
`class` was "just a class" or "both a class and a mixin". For compatibility,
644-
we assume the latter, even if the class is being used as a mixin in a
645-
library whose version is `>= n` and where it does happen to be possible to
646-
distinguish those two intents.*
647-
648-
* A class declaration in a library whose version is `>= n` must be explicitly
649-
marked `mixin class` to allow the class to be used as a mixin from another
650-
library. This is true even if the library where the class is being used as
651-
a mixin is `< n`.
652-
653-
*When a class is in a library where it possible to distinguish between
654-
whether the class is intended to use it as a mixin or not, the author is
655-
obliged to document the intent, and that intent applies to all other libraries
656-
regardless of their version.*
764+
This is a special case behavior only available to platform libraries.
657765

658766
### Compatibility
659767

@@ -665,12 +773,18 @@ needed.
665773

666774
## Changelog
667775

776+
1.4
777+
778+
- Update rules to close loopholes on classes that don't want to expose
779+
interfaces.
780+
668781
1.3
669782

670783
- Specify and update restrictions on `mixin class` declarations to allow
671784
trivial generative constructors.
672-
- Specify that "mixin application" class declarations (`class C = S with M`) cannot
673-
be `mixin class` declaration, but can use other modifiers
785+
786+
- Specify that "mixin application" class declarations (`class C = S with M`)
787+
cannot be `mixin class` declaration, but can use other modifiers
674788

675789
1.2
676790

0 commit comments

Comments
 (0)