Open leonsenft opened 6 days ago
Summary: Extension type erasure in DartObject
prevents code generators from accurately validating types when working with extension types on dart:js_interop
types. This leads to incorrect type checks and potential bugs in generated code.
Summary: Extension type erasure in
DartObject
prevents code generators from accurately validating types when working with extension types ondart:js_interop
types. This leads to incorrect type checks and potential bugs in generated code.
Not so much potential bugs as it doesn't compile.
Error: Can't access platform private library.
import 'dart:_js_types' as import30;
^
Error: Compilation failed.
Related to https://github.com/dart-lang/sdk/pull/55897, which I'll close in favor of this bug. This is also a blocker for adopting extension types in our ongoing improvements to the Dart protobuf runtime
As noted in elsewhere the problem arises because the element/type model is describing the static types in the code, while DartObject
is describing the runtime-type of objects. The runtime-type of an object will never be an extension type.
We could potentially provide some notion of the "static type" of a runtime object, and it might even make sense in the limited world of compile time constants. I can't really think of any better option.
@scheglov
Seems like annotations are not just values, but also expressions, and people want to inspect on the expression as well. This may become even more relevant with macros, where we want to inspect on annotations too.
Note that erasure also implies that const OpaqueToken<A>()
and const OpaqueToken<B>()
where A
and B
are extension types on C
will also collapse into a single const OpaqueToken<C>()
- and consequently break injection if you try to provide both A
and B
.
This means OpaqueToken
and type literals are not very suitable in their current form for injection purposes when extension types are involved, because today language gives you no ways to see them before erasure eliminates them.
Note that erasure also implies that
const OpaqueToken<A>()
andconst OpaqueToken<B>()
whereA
andB
are extension types onC
will also collapse into a singleconst OpaqueToken<C>()
- and consequently break injection if you try to provide bothA
andB
.This means
OpaqueToken
and type literals are not very suitable in their current form for injection purposes when extension types are involved, because today language gives you no ways to see them before erasure eliminates them.
@mattrberry This is potentially concerning for us. Perhaps if an OpaqueToken is on a JS* type, the compiler should require a unique identifier to differentiate them?
// BAD
const signalToken = OpaqueToken<WritableSignal<int>>();
const computedToken = OpaqueToken<Signal<int>>();
// OKAY
const signalToken = OpaqueToken<WritableSignal<int>>('signal');
const computedToken = OpaqueToken<Signal<int>>('computed');
(For clarification for WritableSignal
and Signal
are extension types over JSFunction
)
I think we should probably require an identifier for OpaqueTokens of any extension type, not just on JS* types. We may want to revisit doing the same for type aliases as well. I think I see that as a blocker, though
Issue
When computing a constant value, the analyzer performs extension type erasure on the resulting
DartObject
andTypeState
.https://github.com/dart-lang/sdk/blob/9f27bdeaf66ac8d84676a074ab0fc026564c3732/pkg/analyzer/lib/src/dart/constant/value.dart#L190
https://github.com/dart-lang/sdk/blob/9f27bdeaf66ac8d84676a074ab0fc026564c3732/pkg/analyzer/lib/src/dart/constant/value.dart#L3123
This is problematic for code generators when the analyzed type is a extension type on a
dart:js_interop
type.Example
In ACX, we support annotating constructor parameters with a constant token instance used as a key for dependency injection:
Since Dart's type system lacks any way for us to associate the token's type with the type of the parameter it annotates, we perform a check inside our code generator that the token's type argument matches the parameter's type:
This check breaks down if
Example
happens to be an extension type on a JS type.Now, because of the aforementioned type erasure, the code generator sees
exampleToken
's type asOpaqueToken<JavaScriptFunction>
, instead ofOpaqueToken<Example>
. However, the type ofthis.example
–accessed through the element model–is stillExample
. This causes theisSubtypeOf()
check to fail, sinceJavaScriptFunction
is indeed not assignable to the narrowerExample
type.One might suggest performing type erasure on the parameter's type as well:
This makes the check pass, however, we've now introduced a bug since we'd accept any extension type on
JSFunction
, not justExample
.Furthermore, the underlying type
JavaScriptFunction
is a private type fromdart:_js_types
, which we can't actually use in our generated code, so at the end of the day we still need to be able to get back to the user authored extension type. This problem isn't specific to JS interop types, as users could use extension types over private types in plain Dart code as well.Solution
I'm not proposing any specific solution, however, some way to access an erased type's original type (extension) would be useful for code generators.
@mattrberry @srujzs @scheglov