Skip to content

Commit 86082d4

Browse files
Merge pull request #82 from greglittlefield-wf/UIP-2410_ddc/dev
UIP-2410: Basic DDC support
2 parents 7f0bb3c + 88bce6c commit 86082d4

15 files changed

+700
-20
lines changed

dart_test.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
tags:
2+
# DDC-related tags declared here, since you can't target the DDC using platform selectors:
3+
# https://github.com/dart-lang/test/issues/652
4+
5+
# Tests that should only be run in the dev compiler
6+
ddc:
7+
skip: "skipping DDC-specific tests until UIP-2410 is implemented"
8+
# Tests that should NOT be run in the dev compiler
9+
no-ddc:

lib/src/component/dom_components.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ DomProps domProps([Map backingMap]) => new DomProps(null, backingMap);
3535

3636
typedef DomProps DomPropsFactory();
3737

38-
class DomProps extends component_base.UiProps with DomPropsMixin, ReactPropsMixin {
38+
class DomProps extends component_base.UiProps with DomPropsMixin {
3939
// Wrap Map literal in parens to work around https://github.com/dart-lang/sdk/issues/24410
4040
DomProps(this.componentFactory, [Map props]) : this.props = props ?? ({});
4141

@@ -45,7 +45,7 @@ class DomProps extends component_base.UiProps with DomPropsMixin, ReactPropsMixi
4545
final Map props;
4646
}
4747

48-
class SvgProps extends component_base.UiProps with DomPropsMixin, ReactPropsMixin, SvgPropsMixin implements DomProps {
48+
class SvgProps extends component_base.UiProps with DomPropsMixin, SvgPropsMixin implements DomProps {
4949
// Wrap Map literal in parens to work around https://github.com/dart-lang/sdk/issues/24410
5050
SvgProps(this.componentFactory, [Map props]) : this.props = props ?? ({});
5151

lib/src/component_declaration/component_base.dart

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import 'package:over_react/over_react.dart' show
2828
PropError;
2929

3030
import 'package:over_react/src/component_declaration/component_type_checking.dart';
31+
import 'package:over_react/src/util/ddc_emulated_function_name_bug.dart' as ddc_emulated_function_name_bug;
3132
import 'package:react/react.dart' as react;
3233
import 'package:react/react_client.dart';
3334

@@ -257,7 +258,31 @@ abstract class UiStatefulComponent<TProps extends UiProps, TState extends UiStat
257258
///
258259
/// Note: Implements MapViewMixin instead of extending it so that the abstract [State] declarations
259260
/// don't need a constructor. The generated implementations can mix that functionality in.
260-
abstract class UiState extends Object with MapViewMixin, StateMapViewMixin implements Map {}
261+
abstract class UiState extends Object implements StateMapViewMixin, MapViewMixin, Map {
262+
// Manually implement members from `StateMapViewMixin`,
263+
// since mixing that class in doesn't play well with the DDC.
264+
// TODO find out root cause and reduced test case.
265+
@override Map get _map => this.state;
266+
@override String toString() => '$runtimeType: ${prettyPrintMap(_map)}';
267+
268+
// Manually implement members from `MapViewMixin`,
269+
// since mixing that class in doesn't play well with the DDC.
270+
// TODO find out root cause and reduced test case.
271+
@override operator[](Object key) => _map[key];
272+
@override void operator[]=(key, value) { _map[key] = value; }
273+
@override void addAll(other) { _map.addAll(other); }
274+
@override void clear() { _map.clear(); }
275+
@override putIfAbsent(key, ifAbsent()) => _map.putIfAbsent(key, ifAbsent);
276+
@override bool containsKey(Object key) => _map.containsKey(key);
277+
@override bool containsValue(Object value) => _map.containsValue(value);
278+
@override void forEach(void action(key, value)) { _map.forEach(action); }
279+
@override bool get isEmpty => _map.isEmpty;
280+
@override bool get isNotEmpty => _map.isNotEmpty;
281+
@override int get length => _map.length;
282+
@override Iterable get keys => _map.keys;
283+
@override remove(Object key) => _map.remove(key);
284+
@override Iterable get values => _map.values;
285+
}
261286

262287
/// The string used by default for the key of the attribute added by [UiProps.addTestId].
263288
const defaultTestIdKey = 'data-test-id';
@@ -275,9 +300,41 @@ typedef PropsModifier(Map props);
275300
///
276301
/// Note: Implements MapViewMixin instead of extending it so that the abstract [Props] declarations
277302
/// don't need a constructor. The generated implementations can mix that functionality in.
278-
abstract class UiProps
279-
extends Object with MapViewMixin, PropsMapViewMixin, ReactPropsMixin, UbiquitousDomPropsMixin, CssClassPropsMixin
280-
implements Map {
303+
abstract class UiProps extends Object
304+
with ReactPropsMixin, UbiquitousDomPropsMixin, CssClassPropsMixin
305+
implements PropsMapViewMixin, MapViewMixin, Map {
306+
307+
UiProps() {
308+
// Work around https://github.com/dart-lang/sdk/issues/27647 for all UiProps instances
309+
if (ddc_emulated_function_name_bug.isBugPresent) {
310+
ddc_emulated_function_name_bug.patchName(this);
311+
}
312+
}
313+
314+
// Manually implement members from `MapViewMixin`,
315+
// since mixing that class in doesn't play well with the DDC.
316+
// TODO find out root cause and reduced test case.
317+
@override operator[](Object key) => _map[key];
318+
@override void operator[]=(key, value) { _map[key] = value; }
319+
@override void addAll(other) { _map.addAll(other); }
320+
@override void clear() { _map.clear(); }
321+
@override putIfAbsent(key, ifAbsent()) => _map.putIfAbsent(key, ifAbsent);
322+
@override bool containsKey(Object key) => _map.containsKey(key);
323+
@override bool containsValue(Object value) => _map.containsValue(value);
324+
@override void forEach(void action(key, value)) { _map.forEach(action); }
325+
@override bool get isEmpty => _map.isEmpty;
326+
@override bool get isNotEmpty => _map.isNotEmpty;
327+
@override int get length => _map.length;
328+
@override Iterable get keys => _map.keys;
329+
@override remove(Object key) => _map.remove(key);
330+
@override Iterable get values => _map.values;
331+
332+
// Manually implement members from `StateMapViewMixin`,
333+
// since mixing that class in doesn't play well with the DDC.
334+
// TODO find out root cause and reduced test case.
335+
@override Map get _map => this.props;
336+
@override String toString() => '$runtimeType: ${prettyPrintMap(_map)}';
337+
281338
/// Adds an arbitrary prop key-value pair if [shouldAdd] is true, otherwise, does nothing.
282339
void addProp(propKey, value, [bool shouldAdd = true]) {
283340
if (!shouldAdd) return;
@@ -362,10 +419,6 @@ abstract class UiProps
362419
@override
363420
dynamic noSuchMethod(Invocation invocation) {
364421
if (invocation.memberName == #call && invocation.isMethod) {
365-
var parameters = []
366-
..add(props)
367-
..addAll(invocation.positionalArguments);
368-
369422
assert(() {
370423
// These checks are within the assert so they are not done in production.
371424
var children = invocation.positionalArguments;
@@ -377,7 +430,18 @@ abstract class UiProps
377430
return _validateChildren(children);
378431
});
379432

380-
return Function.apply(componentFactory, parameters);
433+
final factory = componentFactory;
434+
if (factory is ReactComponentFactoryProxy) {
435+
// Use `build` instead of using emulated function behavior to work around DDC issue
436+
// https://github.com/dart-lang/sdk/issues/29904
437+
// Should have the benefit of better performance; TODO optimize type check?
438+
return factory.build(props, invocation.positionalArguments);
439+
} else {
440+
var parameters = []
441+
..add(props)
442+
..addAll(invocation.positionalArguments);
443+
return Function.apply(factory, parameters);
444+
}
381445
}
382446

383447
return super.noSuchMethod(invocation);

lib/src/transformer/README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,23 @@ much cleaner and more _grokkable_.
1616
&nbsp;
1717
&nbsp;
1818

19-
19+
## Transformer options:
20+
The following configuration options are available for the `over_react` transformer.
21+
22+
All values shown are the defaults
23+
24+
```yaml
25+
transformers:
26+
- over_react:
27+
# Whether to apply a workaround in transformed props/state classes for a DDC bug
28+
# in which abstract accessors clobber inherited concrete implementations:
29+
# https://github.com/dart-lang/sdk/issues/29914.
30+
#
31+
# Fixes the issue by generating corresponding abstract getters/setters to
32+
# complete the pair, limited to problematic accessors within transformed
33+
# props/state classes that have the `@override` annotation.
34+
fixDdcAbstractAccessors: false
35+
```
2036
2137
## Wiring it all up
2238

lib/src/transformer/impl_generation.dart

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class ImplGenerator {
5353

5454
final TransformLogger logger;
5555
final TransformedSourceFile transformedFile;
56+
bool shouldFixDdcAbstractAccessors = false;
5657

5758
SourceFile get sourceFile => transformedFile.sourceFile;
5859

@@ -392,6 +393,10 @@ class ImplGenerator {
392393
AccessorType type,
393394
NodeWithMeta<ClassDeclaration, annotations.TypedMap> typedMap
394395
) {
396+
if (shouldFixDdcAbstractAccessors) {
397+
fixDdcAbstractAccessors(type, typedMap);
398+
}
399+
395400
String keyNamespace = getAccessorKeyNamespace(typedMap);
396401

397402
final bool isProps = type == AccessorType.props;
@@ -588,6 +593,66 @@ class ImplGenerator {
588593
staticVariablesImpl
589594
);
590595
}
596+
597+
/// Apply a workaround for an issue where, in the DDC, abstract getter or setter overrides declared in a class clobber
598+
/// the inherited concrete implementations. <https://github.com/dart-lang/sdk/issues/29914>
599+
///
600+
/// Fixes the issue by generating corresponding abstract getters/setters to complete the pair
601+
/// for accessors with the `@override` annotation.
602+
void fixDdcAbstractAccessors(
603+
AccessorType accessorType,
604+
NodeWithMeta<ClassDeclaration, annotations.TypedMap> typedMap,
605+
) {
606+
var candidateAccessors = new List<MethodDeclaration>.from(
607+
typedMap.node.members.where((member) =>
608+
member is MethodDeclaration &&
609+
(member.isGetter || member.isSetter) &&
610+
!member.isSynthetic &&
611+
!member.isStatic &&
612+
member.metadata.any((meta) => meta.name.name == 'override')
613+
)
614+
);
615+
616+
for (var accessor in candidateAccessors) {
617+
// Non-abstract accessors don't exhibit this issue.
618+
if (!accessor.isAbstract) return;
619+
620+
var name = accessor.name.name;
621+
622+
// Don't generate for `Map get props;`/`Map get state;` in mixins
623+
if (accessorType == AccessorType.props && name == 'props') continue;
624+
if (accessorType == AccessorType.state && name == 'state') continue;
625+
626+
if (candidateAccessors.any((other) => other != accessor && other.name.name == name)) {
627+
// Don't generate when both the getter and the setter are declared.
628+
continue;
629+
}
630+
631+
/// Warning: tests rely on this comment as a means of determining whether this fix was applied.
632+
///
633+
/// DO NOT modify or remove without updating tests
634+
const String generatedComment = ' /* fixDdcAbstractAccessors workaround: */ ';
635+
636+
if (accessor.isGetter) {
637+
var type = accessor.returnType?.toSource();
638+
var typeString = type == null ? '' : '$type ';
639+
640+
transformedFile.insert(sourceFile.location(accessor.end),
641+
// use `covariant` here to be extra safe in this typing
642+
'${generatedComment}set $name(covariant ${typeString}value);');
643+
} else {
644+
var parameter = accessor.parameters.parameters.single;
645+
var type = parameter is SimpleFormalParameter
646+
? parameter.type?.toSource()
647+
// This `null` case is mainly for [FunctionTypedFormalParameter].
648+
: null;
649+
var typeString = type == null ? '' : '$type ';
650+
651+
transformedFile.insert(sourceFile.location(accessor.end),
652+
'$generatedComment${typeString}get $name;');
653+
}
654+
}
655+
}
591656
}
592657

593658
enum AccessorType {props, state}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/// Provides detection and patching of the bug described in <https://github.com/dart-lang/sdk/issues/27647>,
2+
/// in which getters/setters with the identifier `name` don't work for emulated function classes, like [UiProps].
3+
@JS()
4+
library over_react.src.util.ddc_emulated_function_name_bug;
5+
6+
import 'package:js/js.dart';
7+
import 'package:over_react/over_react.dart';
8+
9+
/// Create a reduced test case of the issue, using an emulated function pattern that is similar to [UiProps].
10+
///
11+
/// We can't use [UiProps] itself, since it uses [isBugPresent], and that would cause a cyclic initialization error.
12+
class _NsmEmulatedFunctionWithNameProperty implements Function {
13+
void call();
14+
15+
@override
16+
noSuchMethod(i) {}
17+
18+
String _name;
19+
20+
// ignore: unnecessary_getters_setters
21+
String get name => _name;
22+
// ignore: unnecessary_getters_setters
23+
set name(String value) => _name = value;
24+
}
25+
26+
/// Whether this bug, <https://github.com/dart-lang/sdk/issues/27647>, is present in the current runtime.
27+
///
28+
/// This performs functional detection of the bug, and will be `true`
29+
/// only in the DDC and only in versions of the DDC where this bug is present.
30+
final bool isBugPresent = (() {
31+
const testValue = 'test value';
32+
33+
var testObject = new _NsmEmulatedFunctionWithNameProperty();
34+
35+
try {
36+
// In the DDC, this throws:
37+
// TypeError: Cannot assign to read only property 'name' of function 'function call(...args) {
38+
// return call.call.apply(call, args);
39+
// }'
40+
testObject.name = testValue;
41+
} catch(_) {
42+
return true;
43+
}
44+
45+
try {
46+
// We don't expect accessing this to throw, but just in case...
47+
return testObject.name != testValue;
48+
} catch(_) {
49+
return true;
50+
}
51+
})();
52+
53+
54+
@JS()
55+
@anonymous
56+
class _PropertyDescriptor {}
57+
58+
@JS('Object.getPrototypeOf')
59+
external dynamic _getPrototypeOf(dynamic object);
60+
61+
@JS('Object.getOwnPropertyDescriptor')
62+
external _PropertyDescriptor _getOwnPropertyDescriptor(dynamic object, String propertyName);
63+
64+
@JS('Object.defineProperty')
65+
external void _defineProperty(dynamic object, String propertyName, _PropertyDescriptor descriptor);
66+
67+
/// Patches the `name` property on the given [object] to have the expected behavior
68+
/// by copying the property descriptor for `name` from the appropriate prototype.
69+
///
70+
/// This is a noop if `name` is not a property on the given object.
71+
///
72+
/// __This functionality is unstable, and should not be used when [isBugPresent] is `false`.__
73+
///
74+
/// This method also had undefined behavior on non-[UiProps] instances.
75+
void patchName(dynamic object) {
76+
var current = object;
77+
while ((current = _getPrototypeOf(current)) != null) {
78+
var nameDescriptor = _getOwnPropertyDescriptor(current, 'name');
79+
80+
if (nameDescriptor != null) {
81+
_defineProperty(object, 'name', nameDescriptor);
82+
return;
83+
}
84+
}
85+
}

lib/src/util/react_wrappers.dart

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,32 @@ Map getJsProps(/* ReactElement|ReactComponent */ instance) {
101101
return props;
102102
}
103103

104-
Expando<UnmodifiableMapView> _elementPropsCache = new Expando('_elementPropsCache');
104+
/// Whether [Expando]s can be used on [ReactElement]s.
105+
///
106+
/// At the time this was written, this should return:
107+
///
108+
/// - `true` for dart2js and Dart VM
109+
/// - `false` for DDC
110+
final bool _canUseExpandoOnReactElement = (() {
111+
var expando = new Expando<bool>('_canUseExpandoOnReactElement test');
112+
var reactElement = react.div({});
113+
114+
try {
115+
expando[reactElement] = true;
116+
} catch(_) {
117+
return false;
118+
}
119+
120+
return true;
121+
})();
122+
123+
/// A cache of props for a given [ReactElement].
124+
///
125+
/// If caching isn't possible due to [_canUseExpandoOnReactElement] being false,
126+
/// then this will be initialized to `null`, and caching will be disabled.
127+
final Expando<UnmodifiableMapView> _elementPropsCache = _canUseExpandoOnReactElement
128+
? new Expando<UnmodifiableMapView>('_elementPropsCache')
129+
: null;
105130

106131
/// Returns an unmodifiable Map view of props for a [ReactElement] or composite [ReactComponent] [instance].
107132
///
@@ -140,15 +165,17 @@ Map getProps(/* ReactElement|ReactComponent */ instance, {bool traverseWrappers:
140165
}
141166
}
142167

143-
if (!isCompositeComponent) {
168+
if (_elementPropsCache != null && !isCompositeComponent) {
144169
var cachedView = _elementPropsCache[instance];
145170
if (cachedView != null) return cachedView;
146171
}
147172

148173
var propsMap = isDartComponent(instance) ? _getExtendedProps(instance) : getJsProps(instance);
149174
var view = new UnmodifiableMapView(propsMap);
150175

151-
if (!isCompositeComponent) _elementPropsCache[instance] = view;
176+
if (_elementPropsCache != null && !isCompositeComponent) {
177+
_elementPropsCache[instance] = view;
178+
}
152179

153180
return view;
154181
}

0 commit comments

Comments
 (0)