sdk
sdk copied to clipboard
bug: analyzer does not report "inherits multiple members named with incompatible signatures." error but compilation fails.
Consider the following code:
abstract class A {
Null action({required covariant final bool? latest});
}
mixin C on A {}
class B extends A {
Null action({final bool latest = true}) {
return null;
}
}
class D extends B with C {}
void main() {
}
There is no error by the analyzer, however, on running the code, there is an error:
compileDDC
main.dart:13:7: Error: Class 'B with C' inherits multiple members named 'action' with incompatible signatures.
Try adding a declaration of 'action' to 'B with C'.
class D extends B with C {}
^
main.dart:8:8: Context: This is one of the overridden members.
Null action({final bool latest = true}) {
^^^^^^
main.dart:2:8: Context: This is one of the overridden members.
Null action({required covariant final bool? latest});
^^^^^^
Environment: The above error exists on current main (Dart SDK 3.6.0-149.3.beta) and stable (Dart SDK 3.5.0) channels of dartpad.
Expected behaviour: The analyzer should report error.
Summary: The analyzer fails to detect an error when a class inherits multiple members with incompatible signatures, leading to a compilation error. The issue occurs when a class inherits from a class and a mixin, both defining methods with the same name but different parameter types.
On a second thought, I am not sure if this should even be an error. The method on A is overridden by B while C does not have its own definition of the method but inherits it from the class it is being mixed into. There shouldn't be a conflict unless C has its own definition of the method.
@eernstg
tl;dr The analyzer should report that B with C has an error rd;lt
That's a tricky example! ;-)
Let's consider the nominal type structure (that is, the kind of subtype relations that arise because of clauses like extends and implements, not the ones that are based on the structure of the types):
graph BT;
B["B<br>Null action({final bool latest = true})"] -->|extends| A["A<br>Null action({required covariant final bool? latest})"]
C -->|on| A
BC["B with C"] -->|extends| B
BC -->|implements| C
D -->|extends| BC
The override relation from B to A is OK (required in the overridee only is OK, and covariant enables the change from bool? in the overridee to bool in the overrider).
However, the on relation makes C a subtype of A, so the member signature in A is also in the interface of C. This implies that the two member signatures must have a combined member signature in order to allow B with C to receive both of them from superinterfaces, but no such combined member signature exists. Here is the same situation spelled out:
abstract class LikeB {
Null action({final bool latest = true});
}
abstract class LikeC {
Null action({required covariant final bool? latest});
}
abstract class LikeBwithC implements LikeB, LikeC {} // Error, `action` has no member signature.
We can resolve this conflict with regular classes, for instance:
abstract class LikeBwithC implements LikeB, LikeC { // OK.
Null action({bool latest});
}
However, we do not have that option when the conflicted class is a mixin application like B with C, because we don't get the opportunity to write anything like the abstract declaration LikeBwithC.action above to resolve the conflict.
The specification states that the result of a mixin application is a class, and it is a compile-time error if that class has a compile-time error. So it is correct for the CFE to report that 'B with C' has an error, and the analyzer should report a similar thing.
We could consider changing the rules such that this particular kind of conflict in a mixin application is not considered to be a compile-time error in the case where the class declaration that contains the mixin application resolves the conflict. This means that we might change the rules such that the following is OK:
abstract class A {
Null action({required covariant final bool? latest});
}
mixin C on A {}
class B extends A {
Null action({final bool latest = true}) => null;
}
class D extends B with C { // Could (hypothetically!) eliminate the error.
Null action({bool latest}); // `final` and `covariant` OK, too, but make no difference.
}
void main() {}
However, this might be a non-trivial exercise, so I'll leave that to anyone who wishes to spell out a proper proposal and submit it in the language repository.
As a practical matter, the following modification could be used to avoid creating the conflict in the first place (if it is indeed true that no subtype of C will have an action whose latest parameter is required):
abstract class A {
Null action({required covariant final bool? latest});
}
mixin C on A {
Null action({bool? latest});
}
class B extends A {
Null action({final bool latest = true}) => null;
}
class D1 = B with C;
class D extends B with C {}
void main() {}
If we allow a mixin application class to not have a compile-time error when its interface contains a member with two un-combinable member signatures, as long as the class declaration it's part of introduces a subclass where the member is well-defined, then we are still introducing a class with no single consistent member signature for a member.
We'd have to remember both the signatures, so that we can check that the subclass that declares a member signature can be checked against both. If we apply another mixin on top, ... well, it doesn't use the interface for super-invocations, it looks at the actual concrete member's signature, so no problem there. (But we get to combine both super-signatures with the signatures of that mixin too, if it doesn't declare a member, maybe not finding a solution again).
It seems Dart generally treats the declarations of the mixin as declarations of the mixin application class, even abstract declarations, which is probably the correct choice. That allows a mixin to provide a specific type for its members. It does mean that:
abstract class A {
void foo(covariant num? x);
}
abstract class B extends A {
void foo(int? x);
}
abstract interface class I implements A {
void foo(double x);
}
mixin M implements I {
void foo(double x);
}
abstract class C = B with M;
is an error because B&M.foo is not a valid override of B.foo, even with covariant.
The analyzer accepts the code, and the static type of C.foo is void Function(double). That is a bug, that type is not a valid override of the superinterface method B.foo, and covariant overrides need to be valid overrides of all superinterface members - and the analyzer needs to check them all, even if a later subclass has widened.
(The covariant doesn't mean arbitrarily unsafe, it must be a super or subtype of the superinterfaces.)
That makes no difference for the original example here, because the mixin has no member declaration.
(If you comment out the M.foo declaration, the member signature of M comes from the interface I, and it gives a different error: inherits incompatible member signatures, not declares an invalid override).
The reason to make an exception for a mixin application only is that you don't get to add a body with a correction after a mixin application, because it creates the class immediately. Which is valid reason.
But it feels dangerous to allow a class, anywhere, to have an inconsistent interface.
I guess we can phrase it as, when falling back on a combined member signature for a member signature:
The interface member signature is conflicted if the combined signature has no unique solution. (This is what we already compute for the combined superclass interface of a mixin with multiple
ontypes, where we don't allowsuper.invocations on members that don't have a valid combined member signature. We're using the same concept again, so we should give it a name.) It's a compile-time error if any interface has a conflicted member signature, other than the interface of an anonymous mixin application class. (We could extend that, but this is the safer bet.) Any access to an interface member with a conflicted signature is a compile-time error. When creating a combined member signature from interface members where at least one is conflicted, combine with all the member signatures of the conflicted interface's immediate super-interfaces. (If there are no other declarations to combine with, the result will be the same conflict again. If there are, it may still be a new conflict.) A member signature is a valid override of a conflicted member signature if it is a valid override of all the corresponding member signatures of the immediate superinterfaces of the interface with the conflicted signature.
Since no publicly addressable class is an anonymous mixin application, any referable interface should be without conflicts. We push resolution of incompatible member signatures down to the subclass.