From dd8bb5211eab425ddc73ad60bca8a9af161fd606 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Tue, 7 Jul 2020 10:31:26 -0700 Subject: [PATCH 01/38] Initial spike for other branch --- example/builder/main.dart | 3 + example/builder/src/function_component.dart | 306 ++++++++++++++++++ .../src/function_component.over_react.g.dart | 124 +++++++ 3 files changed, 433 insertions(+) create mode 100644 example/builder/src/function_component.dart create mode 100644 example/builder/src/function_component.over_react.g.dart diff --git a/example/builder/main.dart b/example/builder/main.dart index 11a43f944..fc657bc21 100644 --- a/example/builder/main.dart +++ b/example/builder/main.dart @@ -22,6 +22,7 @@ import './src/basic.dart'; import './src/basic_library.dart'; import './src/generic_inheritance_sub.dart'; import './src/generic_inheritance_super.dart'; +import './src/function_component.dart' as function; main() { setClientConfiguration(); @@ -52,6 +53,8 @@ main() { ..subProp = 'sub prop part of lib' ..superProp = 'super prop part of lib' )(), + Dom.h3()('Function component:'), + function.functionComponentContent(), Dom.h3()('getDefaultProps via component factories'), componentConstructorsByName.keys.map((name) => Dom.div()( 'new $name()', diff --git a/example/builder/src/function_component.dart b/example/builder/src/function_component.dart new file mode 100644 index 000000000..6bdc1ba82 --- /dev/null +++ b/example/builder/src/function_component.dart @@ -0,0 +1,306 @@ +import 'dart:developer'; + +import 'package:js/js_util.dart'; +import 'package:meta/meta.dart'; +import 'package:over_react/over_react.dart'; +import 'package:over_react/src/util/prop_errors.dart'; +import 'package:react/react.dart' as react; +import 'package:react/react_client.dart'; +import 'package:react/react_client/js_backed_map.dart'; +import 'package:react/react_client/react_interop.dart'; + +// ignore_for_file: uri_has_not_been_generated +part 'function_component.over_react.g.dart'; + +mixin BasicPropsMixin on UiProps { + String basicProp; + String basic1; + String basic2; + String basic3; + String basic4; +} + +UiFactory Basic = uiFunctionComponent((props) { + return Dom.div()( + Dom.div()('prop id: ${props.id}'), + Dom.div()('default prop testing: ${props.basicProp}'), + Dom.div()('default prop testing: ${props.basic1}'), + Dom.div()(props.basic3, 'children: ${props.children}'), + ); +}, $BasicPropsConfig); + +UiFactory Basic2 = uiFunctionComponent((props) { + return Dom.div()( + Dom.div()('prop id: ${props.id}'), + Dom.div()('default prop testing: ${props.basicProp}'), + Dom.div()('default prop testing: ${props.basic1}'), + Dom.div()(props.basic3, 'children: ${props.children}'), + ); +}, $BasicPropsConfig); + + +// 1. Memory leak? +// 2. Async control-flow is halted + + + + + + + +final Simple = uiFunctionComponent((props) { + return Dom.div()( + Dom.div()('prop id: ${props.id}'), + Dom.div()('default prop testing: ${props.basicProp}'), + Dom.div()('default prop testing: ${props.basic1}'), + Dom.div()(null, props.basic4, 'children: ${props.children}'), + ); +}, $.$BasicPropsConfig, initStatics: (statics) { + statics.defaultProps = (statics.newProps() + ..basicProp = 'basicProp' + ..basic1 = 'basic1' + ); +}); + + + +final Simple2 = uiFunctionComponent((props) { + return Dom.div()( + Dom.div()('prop id: ${props.id}'), + Dom.div()('default prop testing: ${props.basicProp}'), + Dom.div()('default prop testing: ${props.basic1}'), + Dom.div()(null, props.basic4, 'children: ${props.children}'), + ); +}, $.$BasicPropsConfig, initStatics: (statics) { + statics + ..defaultProps = (statics.newProps() + ..basicProp = 'basicProp' + ..basic1 = 'basic1' + ) + ..propTypes = { + statics.keyFor((p) => p.basicProp): (props, __) { + if (props.id == 'id') return PropError.value(props.id, 'id', 'That isn\'t a very descriptive ID.'); + return null; + } + }; +}); + + + + +//UiFactory Basic2 = (BasicPropsMixin props) { +// return Dom.div()( +// Dom.div()('prop id: ${props.id}'), +// Dom.div()('default prop testing: ${props.basicProp}'), +// Dom.div()('default prop testing: ${props.basic1}'), +// Dom.div()(null, props.basic4, 'children: ${props.children}'), +// ); +//}.withTypedProps($.$Basic.factoryFactory); +// +// +// +//UiFactory Basic5 = (BasicPropsMixin props) { +// return Dom.div()( +// Dom.div()('prop id: ${props.id}'), +// Dom.div()('default prop testing: ${props.basicProp}'), +// Dom.div()('default prop testing: ${props.basic1}'), +// Dom.div()(null, props.basic4, 'children: ${props.children}'), +// ); +//}.asFunctionComponent(); +// +// +//UiFactory Basic3 = functionBuilder().function((props) { +// return Dom.div()( +// Dom.div()('prop id: ${props.id}'), +// Dom.div()('default prop testing: ${props.basicProp}'), +// Dom.div()('default prop testing: ${props.basic1}'), +// Dom.div()(null, props.basic4, 'children: ${props.children}'), +// ); +//}.asFunctionComponent(); +// + + +//extension on ReactDartFunctionComponentFactoryProxy { +// UiFactory withTypedProps(FunctionFactoryFactory factoryFactory) { +// return factoryFactory(this); +// } +// UiFactory withTypedPropsFromOtherFactory(UiFactory otherFactory) { +// T uiFactory([Map backingProps]) => otherFactory(backingProps)..componentFactory = this; +// return uiFactory; +// } +//} + +ReactElement functionComponentContent() { + // FIXME look into naming + final genericFactory = uiFunctionComponent((props) { + debugger(); + return Dom.div()( + Dom.div()('prop id: ${props.id}'), + ); + }, null); + +// +// +// UiFactory otherFactory; +// Object closureVariable; +// +// final hocFactory = uiFunctionComponent((props) { +// return otherFactory()( +// Dom.div()('closureVariable: ${closureVariable}'), +// Dom.div()('prop basic1: ${props.basic1}'), +// ); +// }, $.$basicConfig); + + final basicFactory = uiFunctionComponent((props) { + return Dom.div()( + Dom.div()('prop id: ${props.id}'), + Dom.div()('prop basic1: ${props.basic1}'), + ); + // FIXME should displayName really default to "Basic" in this case? + }, $.$BasicPropsConfig, displayName: 'basicFactory'); + + return Fragment()( + (genericFactory()..id = '1')(), + (basicFactory()..id = '2'..basic1 = 'basic1 value')(), + (Basic()..id = '3'..basicProp = 'basicProp')(), + (Simple()..basicProp = 'basicProp')(), + ); +} + + +UiFactory uiFunctionComponent( + dynamic Function(T props) functionComponent, + // FIXME allow passing in displayName for generic function components + FunctionComponentConfig config, + { + PropsFactory propsFactory, + String displayName, + void Function(UiFunctionComponentStatics) initStatics, + }) { + if (config != null) { + if (propsFactory != null) throw ArgumentError('propsFactory cannot be used along with config'); + propsFactory = config.propsFactory; + displayName ??= config.componentName; + } + + // Get the display name from the inner function if possible so it doesn't become `_uiFunctionComponentWrapper` + // FIXME make this work in DDC and make more robust + displayName ??= getFunctionName(functionComponent); + + dynamic _uiFunctionComponentWrapper(Map props) { + return functionComponent(propsFactory.jsMap(props as JsBackedMap)); + } + + /// FIXME DartFunctionComponent should be JsBackedMap? + final factory = react.registerFunctionComponent(_uiFunctionComponentWrapper, + displayName: displayName); + + if (propsFactory == null) { + // todo allow passing in of custom uiFactory/typedPropsFactory + // TODO make it easier to pass in parts of generatedInfo + if (T != UiProps && T != GenericUiProps) { + throw ArgumentError('config.propsFactory must be provided when using custom props classes'); + } + propsFactory = PropsFactory.fromUiFactory(([backingMap]) => GenericUiProps(factory, backingMap)) as PropsFactory; + } + + if (initStatics != null) { + final statics = UiFunctionComponentStatics._( + newProps: () => propsFactory.jsMap(JsBackedMap()), + keyFor: (accessProps) => getPropKey(accessProps, propsFactory.map) + ); + initStatics(statics); + + if (statics.defaultProps != null) { + // fixme need to move to react-dart + (factory.reactFunction as ReactClass).defaultProps = JsBackedMap.from(statics.defaultProps).jsObject; + } + // fixme need to implement in react-dart +// if (statics.propTypes != null) {} + } + + T _uiFactory([Map backingMap]) { + T builder; + if (backingMap == null) { + builder = propsFactory.jsMap(JsBackedMap()); + } else if (backingMap is JsBackedMap) { + builder = propsFactory.jsMap(backingMap); + } else { + builder = propsFactory.map(backingMap); + } + + return builder..componentFactory = factory; + } + return _uiFactory; +} + +String getFunctionName(Function f) { + if (f == null) throw ArgumentError.notNull('f'); + + // DDC + // todo + + // Dart2js + final constructor = getProperty(f, 'constructor'); + if (constructor != null) { + return getProperty(constructor, 'name'); + } + + return null; +} + +class UiFunctionComponentStatics { + Map defaultProps; + Map> propTypes; + + final String Function(void Function(T) accessProps) keyFor; + final T Function() newProps; + + UiFunctionComponentStatics._({this.keyFor, this.newProps}); +// +// T newProps() => this._newProps(); +// +// String keyFor(void Function(T) accessProps) => this._keyFor(accessProps); + +} + +class GenericUiProps extends UiProps { + @override + final Map props; + + GenericUiProps(ReactComponentFactoryProxy componentFactory, [Map props]) : + this.props = props ?? JsBackedMap() { + this.componentFactory = componentFactory; + } + + @override + String get propKeyNamespace => ''; + + @override + bool get $isClassGenerated => true; +} + +typedef FunctionFactoryFactory = UiFactory Function(ReactDartFunctionComponentFactoryProxy); + + +@protected +class FunctionComponentConfig { + @protected + final PropsFactory propsFactory; + final String componentName; + + @protected + FunctionComponentConfig({this.propsFactory, this.componentName}); +} + +class PropsFactory { + final T Function(Map props) map; + final T Function(JsBackedMap props) jsMap; + + PropsFactory({ + @required this.map, + @required this.jsMap, + }); + + PropsFactory.fromUiFactory(UiFactory factory) : this.map = factory, this.jsMap = factory; +} diff --git a/example/builder/src/function_component.over_react.g.dart b/example/builder/src/function_component.over_react.g.dart new file mode 100644 index 000000000..b88d16b67 --- /dev/null +++ b/example/builder/src/function_component.over_react.g.dart @@ -0,0 +1,124 @@ +// ignore_for_file: unnecessary_null_in_if_null_operators +import 'package:over_react/over_react.dart'; + +import 'function_component.dart'; + +final FunctionComponentConfig<$BasicProps> $BasicPropsConfig = FunctionComponentConfig( + propsFactory: PropsFactory( + map: (map) => $BasicProps(map), + jsMap: (map) => _$$BasicProps$JsMap(map), + ), + componentName: 'Basic', +); + +mixin $BasicPropsMixin implements BasicPropsMixin { + @override + Map get props; + + @override + @deprecated + @requiredProp + String get basicProp => props[_$key__basicProp___$BasicProps] ?? null; + + @override + @deprecated + @requiredProp + set basicProp(String value) => props[_$key__basicProp___$BasicProps] = value; + + @override String get basic1 => props[_$key__basic1___$BasicProps] ?? null; + @override set basic1(String value) => props[_$key__basic1___$BasicProps] = value; + + @override String get basic2 => props[_$key__basic2___$BasicProps] ?? null; + @override set basic2(String value) => props[_$key__basic2___$BasicProps] = value; + + @override String get basic3 => props[_$key__basic3___$BasicProps] ?? null; + @override set basic3(String value) => props[_$key__basic3___$BasicProps] = value; + + @override String get basic4 => props[_$key__basic4___$BasicProps] ?? null; + @override set basic4(String value) => props[_$key__basic4___$BasicProps] = value; + + @override String get basic5 => props[_$key__basic5___$BasicProps] ?? null; + @override set basic5(String value) => props[_$key__basic5___$BasicProps] = value; + + static const _$prop__basicProp___$BasicProps = PropDescriptor(_$key__basicProp___$BasicProps, isRequired: true); + static const _$prop__basic1___$BasicProps = PropDescriptor(_$key__basic1___$BasicProps); + static const _$prop__basic2___$BasicProps = PropDescriptor(_$key__basic2___$BasicProps); + static const _$prop__basic3___$BasicProps = PropDescriptor(_$key__basic3___$BasicProps); + static const _$prop__basic4___$BasicProps = PropDescriptor(_$key__basic4___$BasicProps); + static const _$prop__basic5___$BasicProps = PropDescriptor(_$key__basic5___$BasicProps); + static const _$key__basicProp___$BasicProps = 'BasicProps.basicProp'; + static const _$key__basic1___$BasicProps = 'BasicProps.basic1'; + static const _$key__basic2___$BasicProps = 'BasicProps.basic2'; + static const _$key__basic3___$BasicProps = 'BasicProps.basic3'; + static const _$key__basic4___$BasicProps = 'BasicProps.basic4'; + static const _$key__basic5___$BasicProps = 'BasicProps.basic5'; + + static const List $props = [ + _$prop__basicProp___$BasicProps, + _$prop__basic1___$BasicProps, + _$prop__basic2___$BasicProps, + _$prop__basic3___$BasicProps, + _$prop__basic4___$BasicProps, + _$prop__basic5___$BasicProps + ]; + static const List $propKeys = [ + _$key__basicProp___$BasicProps, + _$key__basic1___$BasicProps, + _$key__basic2___$BasicProps, + _$key__basic3___$BasicProps, + _$key__basic4___$BasicProps, + _$key__basic5___$BasicProps + ]; +} + +const PropsMeta _$metaForBasicProps = PropsMeta( + fields: $BasicPropsMixin.$props, + keys: $BasicPropsMixin.$propKeys, +); + +class $BasicProps extends UiProps with BasicPropsMixin, $BasicPropsMixin { + $BasicProps._(); + + static const PropsMeta meta = _$metaForBasicProps; + + factory $BasicProps(Map backingMap) { + if (backingMap == null || backingMap is JsBackedMap) { + return _$$BasicProps$JsMap(backingMap); + } else { + return _$$BasicProps$PlainMap(backingMap); + } + } + + @override + Map get props; + + @override + bool get $isClassGenerated => true; + + @override + String get propKeyNamespace => 'BasicProps.'; +} + +class _$$BasicProps$JsMap extends $BasicProps { + _$$BasicProps$JsMap(JsBackedMap backingMap) + : this._props = JsBackedMap(), + super._() { + this._props = backingMap ?? JsBackedMap(); + } + + @override + JsBackedMap get props => _props; + JsBackedMap _props; +} + +class _$$BasicProps$PlainMap extends $BasicProps { + _$$BasicProps$PlainMap(Map backingMap) + : this._props = {}, + super._() { + this._props = backingMap ?? {}; + } + + @override + Map get props => _props; + Map _props; +} From a81cdf85a0ab2abd0504f682ed5659b1a69887e1 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Tue, 7 Jul 2020 14:25:47 -0700 Subject: [PATCH 02/38] Add v5_functionComponent to parsing versions --- example/builder/src/function_component.dart | 10 +++---- lib/src/builder/parsing/declarations.dart | 1 + .../builder/parsing/members/component.dart | 1 + lib/src/builder/parsing/members/factory.dart | 1 + lib/src/builder/parsing/members_from_ast.dart | 27 ++++++++++++++++++- lib/src/builder/parsing/version.dart | 7 +++++ pubspec.yaml | 6 +++++ 7 files changed, 47 insertions(+), 6 deletions(-) diff --git a/example/builder/src/function_component.dart b/example/builder/src/function_component.dart index 6bdc1ba82..d2004f06f 100644 --- a/example/builder/src/function_component.dart +++ b/example/builder/src/function_component.dart @@ -55,7 +55,7 @@ final Simple = uiFunctionComponent((props) { Dom.div()('default prop testing: ${props.basic1}'), Dom.div()(null, props.basic4, 'children: ${props.children}'), ); -}, $.$BasicPropsConfig, initStatics: (statics) { +}, $BasicPropsConfig, initStatics: (statics) {// ignore: undefined_identifier statics.defaultProps = (statics.newProps() ..basicProp = 'basicProp' ..basic1 = 'basic1' @@ -71,7 +71,7 @@ final Simple2 = uiFunctionComponent((props) { Dom.div()('default prop testing: ${props.basic1}'), Dom.div()(null, props.basic4, 'children: ${props.children}'), ); -}, $.$BasicPropsConfig, initStatics: (statics) { +}, $BasicPropsConfig, initStatics: (statics) { statics ..defaultProps = (statics.newProps() ..basicProp = 'basicProp' @@ -95,7 +95,7 @@ final Simple2 = uiFunctionComponent((props) { // Dom.div()('default prop testing: ${props.basic1}'), // Dom.div()(null, props.basic4, 'children: ${props.children}'), // ); -//}.withTypedProps($.$Basic.factoryFactory); +//}.withTypedProps($Basic.factoryFactory); // // // @@ -149,7 +149,7 @@ ReactElement functionComponentContent() { // Dom.div()('closureVariable: ${closureVariable}'), // Dom.div()('prop basic1: ${props.basic1}'), // ); -// }, $.$basicConfig); +// }, $basicConfig); final basicFactory = uiFunctionComponent((props) { return Dom.div()( @@ -157,7 +157,7 @@ ReactElement functionComponentContent() { Dom.div()('prop basic1: ${props.basic1}'), ); // FIXME should displayName really default to "Basic" in this case? - }, $.$BasicPropsConfig, displayName: 'basicFactory'); + }, $BasicPropsConfig, displayName: 'basicFactory');// ignore: undefined_identifier return Fragment()( (genericFactory()..id = '1')(), diff --git a/lib/src/builder/parsing/declarations.dart b/lib/src/builder/parsing/declarations.dart index 75432044f..b399f1a8b 100644 --- a/lib/src/builder/parsing/declarations.dart +++ b/lib/src/builder/parsing/declarations.dart @@ -96,6 +96,7 @@ class LegacyClassComponentDeclaration extends BoilerplateDeclaration { @required this.props, this.state, }) : assert(version != Version.v4_mixinBased), + assert(version != Version.v5_functionComponent), super(version); /// Validates that the proper annotations are present. diff --git a/lib/src/builder/parsing/members/component.dart b/lib/src/builder/parsing/members/component.dart index 998ab0e17..e591dc441 100644 --- a/lib/src/builder/parsing/members/component.dart +++ b/lib/src/builder/parsing/members/component.dart @@ -99,6 +99,7 @@ class BoilerplateComponent extends BoilerplateMember { break; case Version.v2_legacyBackwardsCompat: case Version.v3_legacyDart2Only: + case Version.v5_functionComponent: break; } diff --git a/lib/src/builder/parsing/members/factory.dart b/lib/src/builder/parsing/members/factory.dart index 3e29cc036..7571a73a2 100644 --- a/lib/src/builder/parsing/members/factory.dart +++ b/lib/src/builder/parsing/members/factory.dart @@ -54,6 +54,7 @@ class BoilerplateFactory extends BoilerplateMember { @override void validate(Version version, ErrorCollector errorCollector) { switch (version) { + case Version.v5_functionComponent: case Version.v4_mixinBased: break; case Version.v2_legacyBackwardsCompat: diff --git a/lib/src/builder/parsing/members_from_ast.dart b/lib/src/builder/parsing/members_from_ast.dart index 71e4186a3..0f64c92d2 100644 --- a/lib/src/builder/parsing/members_from_ast.dart +++ b/lib/src/builder/parsing/members_from_ast.dart @@ -168,6 +168,20 @@ class _BoilerplateMemberDetector { return; } + final rightHandSide = node.variables.firstInitializer; + + if(rightHandSide is MethodInvocation && rightHandSide.methodName.name == 'uiFunctionComponent') { + onFactory(BoilerplateFactory( + node, + VersionConfidences( + v5_functionComponent: Confidence.likely, + v4_mixinBased: Confidence.none, + v3_legacyDart2Only: Confidence.none, + v2_legacyBackwardsCompat: Confidence.none, + ))); + return; + } + final type = node.variables.type; if (type != null) { if (type?.typeNameWithoutPrefix == 'UiFactory') { @@ -188,6 +202,7 @@ class _BoilerplateMemberDetector { onFactory(BoilerplateFactory( node, VersionConfidences( + v5_functionComponent: Confidence.none, v4_mixinBased: Confidence.likely, v3_legacyDart2Only: Confidence.none, v2_legacyBackwardsCompat: Confidence.none, @@ -197,6 +212,7 @@ class _BoilerplateMemberDetector { onFactory(BoilerplateFactory( node, VersionConfidences( + v5_functionComponent: Confidence.none, v4_mixinBased: Confidence.neutral, v2_legacyBackwardsCompat: Confidence.none, v3_legacyDart2Only: Confidence.none, @@ -300,7 +316,7 @@ class _BoilerplateMemberDetector { 'this function assumes that all nodes passed to this function are annotated'); assert(node is! MixinDeclaration, - 'Mixins should never make it in herel they should be classified as Props/State mixins'); + 'Mixins should never make it in here they should be classified as Props/State mixins'); final hasGeneratedPrefix = node.name.name.startsWith(r'_$'); final hasCompanionClass = companion != null; @@ -310,18 +326,21 @@ class _BoilerplateMemberDetector { v2_legacyBackwardsCompat: Confidence.likely, v3_legacyDart2Only: Confidence.unlikely, v4_mixinBased: Confidence.unlikely, + v5_functionComponent: Confidence.none, ); } else if (hasGeneratedPrefix) { return VersionConfidences( v2_legacyBackwardsCompat: Confidence.unlikely, v3_legacyDart2Only: Confidence.likely, v4_mixinBased: Confidence.unlikely, + v5_functionComponent: Confidence.none, ); } else { return VersionConfidences( v2_legacyBackwardsCompat: Confidence.unlikely, v3_legacyDart2Only: Confidence.unlikely, v4_mixinBased: Confidence.likely, + v5_functionComponent: Confidence.none, ); } } @@ -340,6 +359,7 @@ class _BoilerplateMemberDetector { v3_legacyDart2Only: Confidence.unlikely, // Annotated abstract props/state don't exist to the new boilerplate v4_mixinBased: Confidence.none, + v5_functionComponent: Confidence.none, ); } else { return VersionConfidences( @@ -347,6 +367,7 @@ class _BoilerplateMemberDetector { v3_legacyDart2Only: Confidence.likely, // Annotated abstract props/state don't exist to the new boilerplate v4_mixinBased: Confidence.none, + v5_functionComponent: Confidence.none, ); } } @@ -370,6 +391,7 @@ class _BoilerplateMemberDetector { ? Confidence.none : (hasGeneratedPrefix ? Confidence.likely : Confidence.unlikely), v4_mixinBased: isMixin ? Confidence.likely : Confidence.unlikely, + v5_functionComponent: Confidence.none, ); } @@ -396,6 +418,7 @@ class _BoilerplateMemberDetector { v2_legacyBackwardsCompat: Confidence.none, v3_legacyDart2Only: Confidence.none, v4_mixinBased: Confidence.likely, + v5_functionComponent: Confidence.none, ); } @@ -448,6 +471,8 @@ class _BoilerplateMemberDetector { 'FluxUiStatefulComponent2' }; final confidences = VersionConfidences( + // Component classes don't exist in function components + v5_functionComponent: Confidence.none, // If the component extends from a base class known to be supported by the new boilerplate, // has no annotation, is not abstract, and does not have $isClassGenerated, then it's // most likely intended to be part of a new boilerplate class component declaration. diff --git a/lib/src/builder/parsing/version.dart b/lib/src/builder/parsing/version.dart index 387ccbcc4..073a722df 100644 --- a/lib/src/builder/parsing/version.dart +++ b/lib/src/builder/parsing/version.dart @@ -88,6 +88,7 @@ enum Version { v2_legacyBackwardsCompat, v3_legacyDart2Only, v4_mixinBased, + v5_functionComponent, } /// Priority of the possible boilerplate versions. @@ -166,6 +167,7 @@ class VersionConfidences { final int v2_legacyBackwardsCompat; final int v3_legacyDart2Only; final int v4_mixinBased; + final int v5_functionComponent; /// The highest confidence within the class. /// @@ -177,6 +179,7 @@ class VersionConfidences { @required this.v2_legacyBackwardsCompat, @required this.v3_legacyDart2Only, @required this.v4_mixinBased, + @required this.v5_functionComponent, }); /// Constructor to set all confidence values to the same thing @@ -185,6 +188,7 @@ class VersionConfidences { v2_legacyBackwardsCompat: value, v3_legacyDart2Only: value, v4_mixinBased: value, + v5_functionComponent: value, ); /// Constructor to initialize the class with no data. @@ -198,6 +202,7 @@ class VersionConfidences { VersionConfidencePair(Version.v2_legacyBackwardsCompat, v2_legacyBackwardsCompat), VersionConfidencePair(Version.v3_legacyDart2Only, v3_legacyDart2Only), VersionConfidencePair(Version.v4_mixinBased, v4_mixinBased), + VersionConfidencePair(Version.v5_functionComponent, v5_functionComponent), ]; /// Adds all versions in the class to another set of versions from a different [VersionConfidences] @@ -207,6 +212,7 @@ class VersionConfidences { v2_legacyBackwardsCompat: v2_legacyBackwardsCompat + other.v2_legacyBackwardsCompat, v3_legacyDart2Only: v3_legacyDart2Only + other.v3_legacyDart2Only, v4_mixinBased: v4_mixinBased + other.v4_mixinBased, + v5_functionComponent: v5_functionComponent + other.v5_functionComponent, ); } @@ -233,6 +239,7 @@ class VersionConfidences { 'v2': Confidence.description(v2_legacyBackwardsCompat), 'v3': Confidence.description(v3_legacyDart2Only), 'v4': Confidence.description(v4_mixinBased), + 'v5': Confidence.description(v5_functionComponent), }; return '$runtimeType $confidenceMap'; diff --git a/pubspec.yaml b/pubspec.yaml index ae499b72b..44af2d79c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,3 +46,9 @@ dev_dependencies: pedantic: ^1.8.0 test: ^1.9.1 yaml: ^2.2.1 + +dependency_overrides: + react: + git: + url: https://github.com/cleandart/react-dart.git + ref: 5.4.0-wip From 8e2d970de0b14b55a248c3e5892383919e370ec8 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Tue, 7 Jul 2020 15:08:16 -0700 Subject: [PATCH 03/38] Update generated file and error handling --- .../src/function_component.over_react.g.dart | 178 +++++++----------- .../parsing/declarations_from_members.dart | 10 +- 2 files changed, 77 insertions(+), 111 deletions(-) diff --git a/example/builder/src/function_component.over_react.g.dart b/example/builder/src/function_component.over_react.g.dart index b88d16b67..04787d539 100644 --- a/example/builder/src/function_component.over_react.g.dart +++ b/example/builder/src/function_component.over_react.g.dart @@ -1,124 +1,86 @@ -// ignore_for_file: unnecessary_null_in_if_null_operators -import 'package:over_react/over_react.dart'; +// GENERATED CODE - DO NOT MODIFY BY HAND -import 'function_component.dart'; +// ignore_for_file: deprecated_member_use_from_same_package, unnecessary_null_in_if_null_operators, prefer_null_aware_operators +part of 'function_component.dart'; -final FunctionComponentConfig<$BasicProps> $BasicPropsConfig = FunctionComponentConfig( - propsFactory: PropsFactory( - map: (map) => $BasicProps(map), - jsMap: (map) => _$$BasicProps$JsMap(map), - ), - componentName: 'Basic', -); +// ************************************************************************** +// OverReactBuilder (package:over_react/src/builder.dart) +// ************************************************************************** -mixin $BasicPropsMixin implements BasicPropsMixin { +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.' + ' EXCEPTION: this may be used in legacy boilerplate until' + ' it is transitioned to the new mixin-based boilerplate.') +mixin $BasicPropsMixin on BasicPropsMixin { + static const PropsMeta meta = _$metaForBasicPropsMixin; @override - Map get props; - + String get basicProp => + props[_$key__basicProp__BasicPropsMixin] ?? + null; // Add ` ?? null` to workaround DDC bug: ; @override - @deprecated - @requiredProp - String get basicProp => props[_$key__basicProp___$BasicProps] ?? null; - + set basicProp(String value) => + props[_$key__basicProp__BasicPropsMixin] = value; @override - @deprecated - @requiredProp - set basicProp(String value) => props[_$key__basicProp___$BasicProps] = value; - - @override String get basic1 => props[_$key__basic1___$BasicProps] ?? null; - @override set basic1(String value) => props[_$key__basic1___$BasicProps] = value; - - @override String get basic2 => props[_$key__basic2___$BasicProps] ?? null; - @override set basic2(String value) => props[_$key__basic2___$BasicProps] = value; - - @override String get basic3 => props[_$key__basic3___$BasicProps] ?? null; - @override set basic3(String value) => props[_$key__basic3___$BasicProps] = value; - - @override String get basic4 => props[_$key__basic4___$BasicProps] ?? null; - @override set basic4(String value) => props[_$key__basic4___$BasicProps] = value; - - @override String get basic5 => props[_$key__basic5___$BasicProps] ?? null; - @override set basic5(String value) => props[_$key__basic5___$BasicProps] = value; - - static const _$prop__basicProp___$BasicProps = PropDescriptor(_$key__basicProp___$BasicProps, isRequired: true); - static const _$prop__basic1___$BasicProps = PropDescriptor(_$key__basic1___$BasicProps); - static const _$prop__basic2___$BasicProps = PropDescriptor(_$key__basic2___$BasicProps); - static const _$prop__basic3___$BasicProps = PropDescriptor(_$key__basic3___$BasicProps); - static const _$prop__basic4___$BasicProps = PropDescriptor(_$key__basic4___$BasicProps); - static const _$prop__basic5___$BasicProps = PropDescriptor(_$key__basic5___$BasicProps); - static const _$key__basicProp___$BasicProps = 'BasicProps.basicProp'; - static const _$key__basic1___$BasicProps = 'BasicProps.basic1'; - static const _$key__basic2___$BasicProps = 'BasicProps.basic2'; - static const _$key__basic3___$BasicProps = 'BasicProps.basic3'; - static const _$key__basic4___$BasicProps = 'BasicProps.basic4'; - static const _$key__basic5___$BasicProps = 'BasicProps.basic5'; + String get basic1 => + props[_$key__basic1__BasicPropsMixin] ?? + null; // Add ` ?? null` to workaround DDC bug: ; + @override + set basic1(String value) => props[_$key__basic1__BasicPropsMixin] = value; + @override + String get basic2 => + props[_$key__basic2__BasicPropsMixin] ?? + null; // Add ` ?? null` to workaround DDC bug: ; + @override + set basic2(String value) => props[_$key__basic2__BasicPropsMixin] = value; + @override + String get basic3 => + props[_$key__basic3__BasicPropsMixin] ?? + null; // Add ` ?? null` to workaround DDC bug: ; + @override + set basic3(String value) => props[_$key__basic3__BasicPropsMixin] = value; + @override + String get basic4 => + props[_$key__basic4__BasicPropsMixin] ?? + null; // Add ` ?? null` to workaround DDC bug: ; + @override + set basic4(String value) => props[_$key__basic4__BasicPropsMixin] = value; + /* GENERATED CONSTANTS */ + static const PropDescriptor _$prop__basicProp__BasicPropsMixin = + PropDescriptor(_$key__basicProp__BasicPropsMixin); + static const PropDescriptor _$prop__basic1__BasicPropsMixin = + PropDescriptor(_$key__basic1__BasicPropsMixin); + static const PropDescriptor _$prop__basic2__BasicPropsMixin = + PropDescriptor(_$key__basic2__BasicPropsMixin); + static const PropDescriptor _$prop__basic3__BasicPropsMixin = + PropDescriptor(_$key__basic3__BasicPropsMixin); + static const PropDescriptor _$prop__basic4__BasicPropsMixin = + PropDescriptor(_$key__basic4__BasicPropsMixin); + static const String _$key__basicProp__BasicPropsMixin = + 'BasicPropsMixin.basicProp'; + static const String _$key__basic1__BasicPropsMixin = 'BasicPropsMixin.basic1'; + static const String _$key__basic2__BasicPropsMixin = 'BasicPropsMixin.basic2'; + static const String _$key__basic3__BasicPropsMixin = 'BasicPropsMixin.basic3'; + static const String _$key__basic4__BasicPropsMixin = 'BasicPropsMixin.basic4'; static const List $props = [ - _$prop__basicProp___$BasicProps, - _$prop__basic1___$BasicProps, - _$prop__basic2___$BasicProps, - _$prop__basic3___$BasicProps, - _$prop__basic4___$BasicProps, - _$prop__basic5___$BasicProps + _$prop__basicProp__BasicPropsMixin, + _$prop__basic1__BasicPropsMixin, + _$prop__basic2__BasicPropsMixin, + _$prop__basic3__BasicPropsMixin, + _$prop__basic4__BasicPropsMixin ]; static const List $propKeys = [ - _$key__basicProp___$BasicProps, - _$key__basic1___$BasicProps, - _$key__basic2___$BasicProps, - _$key__basic3___$BasicProps, - _$key__basic4___$BasicProps, - _$key__basic5___$BasicProps + _$key__basicProp__BasicPropsMixin, + _$key__basic1__BasicPropsMixin, + _$key__basic2__BasicPropsMixin, + _$key__basic3__BasicPropsMixin, + _$key__basic4__BasicPropsMixin ]; } -const PropsMeta _$metaForBasicProps = PropsMeta( +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +const PropsMeta _$metaForBasicPropsMixin = PropsMeta( fields: $BasicPropsMixin.$props, keys: $BasicPropsMixin.$propKeys, ); - -class $BasicProps extends UiProps with BasicPropsMixin, $BasicPropsMixin { - $BasicProps._(); - - static const PropsMeta meta = _$metaForBasicProps; - - factory $BasicProps(Map backingMap) { - if (backingMap == null || backingMap is JsBackedMap) { - return _$$BasicProps$JsMap(backingMap); - } else { - return _$$BasicProps$PlainMap(backingMap); - } - } - - @override - Map get props; - - @override - bool get $isClassGenerated => true; - - @override - String get propKeyNamespace => 'BasicProps.'; -} - -class _$$BasicProps$JsMap extends $BasicProps { - _$$BasicProps$JsMap(JsBackedMap backingMap) - : this._props = JsBackedMap(), - super._() { - this._props = backingMap ?? JsBackedMap(); - } - - @override - JsBackedMap get props => _props; - JsBackedMap _props; -} - -class _$$BasicProps$PlainMap extends $BasicProps { - _$$BasicProps$PlainMap(Map backingMap) - : this._props = {}, - super._() { - this._props = backingMap ?? {}; - } - - @override - Map get props => _props; - Map _props; -} diff --git a/lib/src/builder/parsing/declarations_from_members.dart b/lib/src/builder/parsing/declarations_from_members.dart index a8957f110..58a888180 100644 --- a/lib/src/builder/parsing/declarations_from_members.dart +++ b/lib/src/builder/parsing/declarations_from_members.dart @@ -332,7 +332,9 @@ Iterable getBoilerplateDeclarations( final single = nonNullFactoryPropsOrComponents.single; final span = errorCollector.spanFor(single.node); if (single == factory) { - errorCollector.addError(errorFactoryOnly, span); + if(factory.versionConfidences.maxConfidence.version != Version.v5_functionComponent) { + errorCollector.addError(errorFactoryOnly, span); + } } else if (single == propsClass) { errorCollector.addError(errorPropsClassOnly, span); } else if (single == componentClass) { @@ -346,7 +348,9 @@ Iterable getBoilerplateDeclarations( } else if (propsClass == null) { errorCollector.addError(errorNoProps, span); } else if (componentClass == null) { - errorCollector.addError(errorNoComponent, span); + if(factory.versionConfidences.maxConfidence.version != Version.v5_functionComponent) { + errorCollector.addError(errorNoComponent, span); + } } continue; } @@ -365,7 +369,7 @@ const errorStateOnly = ' these are required to use UiState.'; const errorFactoryOnly = 'Could not find matching props class in this file;' - ' this is required to declare a props map view or function component,' + ' this is required to declare a props map view,' ' and a component class is also required to declare a class-based component.'; const errorPropsClassOnly = 'Could not find matching factory in this file;' From d773544537bdec0547d45bdcc994388f22ac6285 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Thu, 9 Jul 2020 11:50:06 -0700 Subject: [PATCH 04/38] Parse ComponentFactoryDeclarations --- lib/over_react.dart | 1 + lib/src/builder/parsing/declarations.dart | 41 +++++ .../parsing/declarations_from_members.dart | 30 ++++ .../builder/parsing/member_association.dart | 22 +++ .../parsing/members/props_and_state.dart | 2 + .../members/props_and_state_mixins.dart | 2 + lib/src/builder/parsing/version.dart | 1 + .../function_component.dart | 169 ++++++++++++++++++ 8 files changed, 268 insertions(+) create mode 100644 lib/src/component_declaration/function_component.dart diff --git a/lib/over_react.dart b/lib/over_react.dart index ba78a063f..b90e348d7 100644 --- a/lib/over_react.dart +++ b/lib/over_react.dart @@ -60,6 +60,7 @@ export 'src/component_declaration/component_base_2.dart' show UiStatefulMixin2; export 'src/component_declaration/built_redux_component.dart'; export 'src/component_declaration/flux_component.dart'; +export 'src/component_declaration/function_component.dart'; export 'src/util/character_constants.dart'; export 'src/util/class_names.dart'; export 'src/util/constants_base.dart'; diff --git a/lib/src/builder/parsing/declarations.dart b/lib/src/builder/parsing/declarations.dart index b399f1a8b..a83c804be 100644 --- a/lib/src/builder/parsing/declarations.dart +++ b/lib/src/builder/parsing/declarations.dart @@ -24,6 +24,7 @@ import 'version.dart'; /// The possible declaration types that the builder will look for. enum DeclarationType { propsMapViewOrFunctionComponentDeclaration, + functionComponentDeclaration, classComponentDeclaration, legacyClassComponentDeclaration, legacyAbstractPropsDeclaration, @@ -138,6 +139,7 @@ class LegacyAbstractPropsDeclaration extends BoilerplateDeclaration { @required Version version, @required this.props, }) : assert(version != Version.v4_mixinBased), + assert(version != Version.v5_functionComponent), super(version); /// Validates that if there are props that the props class has the correct annotation. @@ -166,6 +168,7 @@ class LegacyAbstractStateDeclaration extends BoilerplateDeclaration { @required Version version, @required this.state, }) : assert(version != Version.v4_mixinBased), + assert(version != Version.v5_functionComponent), super(version); /// Validates that if there are state fields that the class has the correct annotation. @@ -299,6 +302,44 @@ class PropsMapViewOrFunctionComponentDeclaration extends BoilerplateDeclaration }) : super(Version.v4_mixinBased); } +/// UPDATE THIS DOC COMMENT +/// A boilerplate declaration for either a props map view or function component declared using +/// the new mixin-based boilerplate, which have almost identical syntax and code generation. +/// +/// See [BoilerplateDeclaration] for more info. +class FunctionComponentDeclaration extends BoilerplateDeclaration + with _TypedMapMixinShorthandDeclaration { + /// The related factory instance. + final List factories; + + /// The related props instance. + /// + /// Can be either [BoilerplateProps] or [BoilerplatePropsMixin], but not both. + final Union props; + + @override + get _members => [...factories, props.either]; + + @override + get type => DeclarationType.functionComponentDeclaration; + + @override + void validate(ErrorCollector errorCollector) { + super.validate(errorCollector); + + for (final factory in factories) { + _validateShorthand(errorCollector, PropsStateStringHelpers.props(), + propsOrState: props, + shorthandUsage: factory.propsGenericArg ?? factory.node); + } + } + + FunctionComponentDeclaration({ + @required this.factories, + @required this.props, + }) : super(Version.v5_functionComponent); +} + /// Common interface for a boilerplate declaration for either a props or state mixin of any /// boilerplate version. /// diff --git a/lib/src/builder/parsing/declarations_from_members.dart b/lib/src/builder/parsing/declarations_from_members.dart index 58a888180..dfdb95ea9 100644 --- a/lib/src/builder/parsing/declarations_from_members.dart +++ b/lib/src/builder/parsing/declarations_from_members.dart @@ -69,6 +69,7 @@ Iterable getBoilerplateDeclarations( if (members.isEmpty) return; final _consumedMembers = {}; + final _functionComponentFactories = {}; /// Indicate that [member] has been grouped into a declaration, /// so that it is not grouped into another declaration. @@ -209,6 +210,12 @@ Iterable getBoilerplateDeclarations( ..sort((a, b) => b.versionConfidences.maxConfidence.confidence .compareTo(a.versionConfidences.maxConfidence.confidence)); for (final factory in factoriesMostToLeastConfidence) { + if(factory.versionConfidences.maxConfidence.version == Version.v5_functionComponent) { + _functionComponentFactories.add(factory); + // will be validated below the for-loop. + continue; + } + final propsClassOrMixin = getPropsFor(factory, members.props, members.propsMixins); final stateClassOrMixin = getStateFor(factory, members.states, members.stateMixins); if (propsClassOrMixin == null) { @@ -271,6 +278,8 @@ Iterable getBoilerplateDeclarations( props: propsClassOrMixin, state: stateClassOrMixin); break; + case Version.v5_functionComponent: + break; } } } else { @@ -287,6 +296,27 @@ Iterable getBoilerplateDeclarations( } } + if(_functionComponentFactories.isNotEmpty) { + final allUnusedProps = [ + members.props, + members.propsMixins, + ].expand((i) => i).whereNot(hasBeenConsumed); + + for (final propsClassOrMixin in allUnusedProps) { + final associatedFactories = _functionComponentFactories.where((factory) => + getPropsNameFromConfig(factory) == propsClassOrMixin.name.name); + if (associatedFactories.isNotEmpty) { + associatedFactories.forEach(consume); + consume(propsClassOrMixin); + yield FunctionComponentDeclaration( + factories: associatedFactories.toList(), + props: getUnion( + propsClassOrMixin), + ); + } + } + } + // ----------------------------------------------------------------------------------------------- // // Handle leftover members that didn't get grouped into a declaration diff --git a/lib/src/builder/parsing/member_association.dart b/lib/src/builder/parsing/member_association.dart index fa07e659a..26b061e12 100644 --- a/lib/src/builder/parsing/member_association.dart +++ b/lib/src/builder/parsing/member_association.dart @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'package:analyzer/dart/ast/ast.dart'; + import 'ast_util.dart'; import 'members.dart'; import 'util.dart'; @@ -75,6 +77,14 @@ Union _getNameMatchUnion getUnion(BoilerplateMember member) { + if(member is A) return Union.a(member); + + if (member is B) return Union.b(member); + + return null; +} + /// Retrieves the component for a given [member] if it is found in [components]. /// /// This first tries to normalize the names of the entities to find a matching name, @@ -114,6 +124,18 @@ Union getPropsFor( getRelatedName(member).mapIfNotNull((name) => _getNameMatchUnion(props, propsMixins, name)); } +/// ADD DOC COMMENT IF KEPT +String getPropsNameFromConfig(BoilerplateFactory factory) { + final rightHandSide = factory.node.variables.firstInitializer; + if(rightHandSide == null || rightHandSide is! MethodInvocation) return null; + final args = (rightHandSide as MethodInvocation).argumentList.arguments; + if(args.length < 2) return null; + final config = args[1].toSource(); + final startIndex = config.indexOf(RegExp(r'\$')) + 1; + final configIndex = config.lastIndexOf(RegExp(r'Config')); + return config.substring(startIndex, configIndex == -1 ? config.length : configIndex); +} + /// Retrieves the props for a given [member] if it is found in [states] or [stateMixins]. /// /// This is done purely off of matching the name of the member class against the props diff --git a/lib/src/builder/parsing/members/props_and_state.dart b/lib/src/builder/parsing/members/props_and_state.dart index 7a1585038..c61fbed16 100644 --- a/lib/src/builder/parsing/members/props_and_state.dart +++ b/lib/src/builder/parsing/members/props_and_state.dart @@ -62,6 +62,8 @@ abstract class BoilerplatePropsOrState extends BoilerplateTypedMapMember @override void validate(Version version, ErrorCollector errorCollector) { switch (version) { + case Version.v5_functionComponent: + break; case Version.v4_mixinBased: final node = this.node; if (node is MixinDeclaration) { diff --git a/lib/src/builder/parsing/members/props_and_state_mixins.dart b/lib/src/builder/parsing/members/props_and_state_mixins.dart index 7c3eaeaa9..f6eee0483 100644 --- a/lib/src/builder/parsing/members/props_and_state_mixins.dart +++ b/lib/src/builder/parsing/members/props_and_state_mixins.dart @@ -76,6 +76,8 @@ abstract class BoilerplatePropsOrStateMixin extends BoilerplateTypedMapMember } switch (version) { + case Version.v5_functionComponent: + break; case Version.v4_mixinBased: final node = this.node; if (node is MixinDeclaration) { diff --git a/lib/src/builder/parsing/version.dart b/lib/src/builder/parsing/version.dart index 073a722df..8e7c96851 100644 --- a/lib/src/builder/parsing/version.dart +++ b/lib/src/builder/parsing/version.dart @@ -98,6 +98,7 @@ enum Version { /// /// The length of this iterable should match the length of values in [Version]. const _versionsInPriorityOrder = [ + Version.v5_functionComponent, Version.v4_mixinBased, Version.v2_legacyBackwardsCompat, Version.v3_legacyDart2Only, diff --git a/lib/src/component_declaration/function_component.dart b/lib/src/component_declaration/function_component.dart new file mode 100644 index 000000000..5f71f0512 --- /dev/null +++ b/lib/src/component_declaration/function_component.dart @@ -0,0 +1,169 @@ +// Copyright 2016 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ignore_for_file: prefer_generic_function_type_aliases +library over_react.component_declaration.function_component; + +import 'dart:developer'; + +import 'package:js/js_util.dart'; +import 'package:meta/meta.dart'; +import 'package:over_react/over_react.dart'; +import 'package:over_react/src/util/prop_errors.dart'; +import 'package:react/react.dart' as react; +import 'package:react/react_client.dart'; +import 'package:react/react_client/js_backed_map.dart'; +import 'package:react/react_client/react_interop.dart'; + +export 'component_type_checking.dart' show isComponentOfType, isValidElementOfType; + + + + +UiFactory uiFunctionComponent( + dynamic Function(T props) functionComponent, + // FIXME allow passing in displayName for generic function components + FunctionComponentConfig config, + { + PropsFactory propsFactory, + String displayName, + void Function(UiFunctionComponentStatics) initStatics, + }) { + if (config != null) { + if (propsFactory != null) throw ArgumentError('propsFactory cannot be used along with config'); + propsFactory = config.propsFactory; + displayName ??= config.componentName; + } + + // Get the display name from the inner function if possible so it doesn't become `_uiFunctionComponentWrapper` + // FIXME make this work in DDC and make more robust + displayName ??= getFunctionName(functionComponent); + + dynamic _uiFunctionComponentWrapper(Map props) { + return functionComponent(propsFactory.jsMap(props as JsBackedMap)); + } + + /// FIXME DartFunctionComponent should be JsBackedMap? + final factory = react.registerFunctionComponent(_uiFunctionComponentWrapper, + displayName: displayName); + + if (propsFactory == null) { + // todo allow passing in of custom uiFactory/typedPropsFactory + // TODO make it easier to pass in parts of generatedInfo + if (T != UiProps && T != GenericUiProps) { + throw ArgumentError('config.propsFactory must be provided when using custom props classes'); + } + propsFactory = PropsFactory.fromUiFactory(([backingMap]) => GenericUiProps(factory, backingMap)) as PropsFactory; + } + + if (initStatics != null) { + final statics = UiFunctionComponentStatics._( + newProps: () => propsFactory.jsMap(JsBackedMap()), + keyFor: (accessProps) => getPropKey(accessProps, propsFactory.map) + ); + initStatics(statics); + + if (statics.defaultProps != null) { + // fixme need to move to react-dart + (factory.reactFunction as ReactClass).defaultProps = JsBackedMap.from(statics.defaultProps).jsObject; + } + // fixme need to implement in react-dart +// if (statics.propTypes != null) {} + } + + T _uiFactory([Map backingMap]) { + T builder; + if (backingMap == null) { + builder = propsFactory.jsMap(JsBackedMap()); + } else if (backingMap is JsBackedMap) { + builder = propsFactory.jsMap(backingMap); + } else { + builder = propsFactory.map(backingMap); + } + + return builder..componentFactory = factory; + } + return _uiFactory; +} + +String getFunctionName(Function f) { + if (f == null) throw ArgumentError.notNull('f'); + + // DDC + // todo + + // Dart2js + final constructor = getProperty(f, 'constructor'); + if (constructor != null) { + return getProperty(constructor, 'name'); + } + + return null; +} + +class UiFunctionComponentStatics { + Map defaultProps; + Map> propTypes; + + final String Function(void Function(T) accessProps) keyFor; + final T Function() newProps; + + UiFunctionComponentStatics._({this.keyFor, this.newProps}); +// +// T newProps() => this._newProps(); +// +// String keyFor(void Function(T) accessProps) => this._keyFor(accessProps); + +} + +class GenericUiProps extends UiProps { + @override + final Map props; + + GenericUiProps(ReactComponentFactoryProxy componentFactory, [Map props]) : + this.props = props ?? JsBackedMap() { + this.componentFactory = componentFactory; + } + + @override + String get propKeyNamespace => ''; + + @override + bool get $isClassGenerated => true; +} + +typedef FunctionFactoryFactory = UiFactory Function(ReactDartFunctionComponentFactoryProxy); + + +@protected +class FunctionComponentConfig { + @protected + final PropsFactory propsFactory; + final String componentName; + + @protected + FunctionComponentConfig({this.propsFactory, this.componentName}); +} + +class PropsFactory { + final T Function(Map props) map; + final T Function(JsBackedMap props) jsMap; + + PropsFactory({ + @required this.map, + @required this.jsMap, + }); + + PropsFactory.fromUiFactory(UiFactory factory) : this.map = factory, this.jsMap = factory; +} From 86bc3277431dae9ea31913afd5f153181aec427f Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Thu, 9 Jul 2020 16:45:40 -0700 Subject: [PATCH 05/38] Remove v5 and update PropsMapViewOrFunctionComponentDeclaration --- .../codegen/typed_map_impl_generator.dart | 3 +- lib/src/builder/parsing/declarations.dart | 47 ++----------------- .../parsing/declarations_from_members.dart | 29 +++++------- .../builder/parsing/member_association.dart | 9 ++-- .../builder/parsing/members/component.dart | 2 - lib/src/builder/parsing/members/factory.dart | 8 +++- .../parsing/members/props_and_state.dart | 2 - .../members/props_and_state_mixins.dart | 2 - lib/src/builder/parsing/members_from_ast.dart | 18 ++----- lib/src/builder/parsing/version.dart | 8 ---- 10 files changed, 32 insertions(+), 96 deletions(-) diff --git a/lib/src/builder/codegen/typed_map_impl_generator.dart b/lib/src/builder/codegen/typed_map_impl_generator.dart index a4d56575d..d30af1dac 100644 --- a/lib/src/builder/codegen/typed_map_impl_generator.dart +++ b/lib/src/builder/codegen/typed_map_impl_generator.dart @@ -361,7 +361,8 @@ class _TypedMapImplGenerator extends TypedMapImplGenerator { _TypedMapImplGenerator.propsMapViewOrFunctionComponent( PropsMapViewOrFunctionComponentDeclaration declaration) : names = TypedMapNames(declaration.props.either.name.name), - factoryNames = FactoryNames(declaration.factory.name.name), + assert(declaration.factories.length == 1), + factoryNames = FactoryNames(declaration.factories.first.name.name), member = declaration.props.either, isProps = true, componentFactoryName = 'null', diff --git a/lib/src/builder/parsing/declarations.dart b/lib/src/builder/parsing/declarations.dart index a83c804be..4545625ec 100644 --- a/lib/src/builder/parsing/declarations.dart +++ b/lib/src/builder/parsing/declarations.dart @@ -97,7 +97,6 @@ class LegacyClassComponentDeclaration extends BoilerplateDeclaration { @required this.props, this.state, }) : assert(version != Version.v4_mixinBased), - assert(version != Version.v5_functionComponent), super(version); /// Validates that the proper annotations are present. @@ -139,7 +138,6 @@ class LegacyAbstractPropsDeclaration extends BoilerplateDeclaration { @required Version version, @required this.props, }) : assert(version != Version.v4_mixinBased), - assert(version != Version.v5_functionComponent), super(version); /// Validates that if there are props that the props class has the correct annotation. @@ -168,7 +166,6 @@ class LegacyAbstractStateDeclaration extends BoilerplateDeclaration { @required Version version, @required this.state, }) : assert(version != Version.v4_mixinBased), - assert(version != Version.v5_functionComponent), super(version); /// Validates that if there are state fields that the class has the correct annotation. @@ -275,41 +272,6 @@ class ClassComponentDeclaration extends BoilerplateDeclaration class PropsMapViewOrFunctionComponentDeclaration extends BoilerplateDeclaration with _TypedMapMixinShorthandDeclaration { /// The related factory instance. - final BoilerplateFactory factory; - - /// The related props instance. - /// - /// Can be either [BoilerplateProps] or [BoilerplatePropsMixin], but not both. - final Union props; - - @override - get _members => [factory, props.either]; - - @override - get type => DeclarationType.propsMapViewOrFunctionComponentDeclaration; - - @override - void validate(ErrorCollector errorCollector) { - super.validate(errorCollector); - - _validateShorthand(errorCollector, PropsStateStringHelpers.props(), - propsOrState: props, shorthandUsage: factory.propsGenericArg ?? factory.node); - } - - PropsMapViewOrFunctionComponentDeclaration({ - @required this.factory, - @required this.props, - }) : super(Version.v4_mixinBased); -} - -/// UPDATE THIS DOC COMMENT -/// A boilerplate declaration for either a props map view or function component declared using -/// the new mixin-based boilerplate, which have almost identical syntax and code generation. -/// -/// See [BoilerplateDeclaration] for more info. -class FunctionComponentDeclaration extends BoilerplateDeclaration - with _TypedMapMixinShorthandDeclaration { - /// The related factory instance. final List factories; /// The related props instance. @@ -321,7 +283,7 @@ class FunctionComponentDeclaration extends BoilerplateDeclaration get _members => [...factories, props.either]; @override - get type => DeclarationType.functionComponentDeclaration; + get type => DeclarationType.propsMapViewOrFunctionComponentDeclaration; @override void validate(ErrorCollector errorCollector) { @@ -329,15 +291,14 @@ class FunctionComponentDeclaration extends BoilerplateDeclaration for (final factory in factories) { _validateShorthand(errorCollector, PropsStateStringHelpers.props(), - propsOrState: props, - shorthandUsage: factory.propsGenericArg ?? factory.node); + propsOrState: props, shorthandUsage: factory.propsGenericArg ?? factory.node); } } - FunctionComponentDeclaration({ + PropsMapViewOrFunctionComponentDeclaration({ @required this.factories, @required this.props, - }) : super(Version.v5_functionComponent); + }) : super(Version.v4_mixinBased); } /// Common interface for a boilerplate declaration for either a props or state mixin of any diff --git a/lib/src/builder/parsing/declarations_from_members.dart b/lib/src/builder/parsing/declarations_from_members.dart index dfdb95ea9..28edae5fa 100644 --- a/lib/src/builder/parsing/declarations_from_members.dart +++ b/lib/src/builder/parsing/declarations_from_members.dart @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'package:analyzer/dart/ast/ast.dart'; + import 'ast_util.dart'; import 'declarations.dart'; import 'member_association.dart'; @@ -210,7 +212,7 @@ Iterable getBoilerplateDeclarations( ..sort((a, b) => b.versionConfidences.maxConfidence.confidence .compareTo(a.versionConfidences.maxConfidence.confidence)); for (final factory in factoriesMostToLeastConfidence) { - if(factory.versionConfidences.maxConfidence.version == Version.v5_functionComponent) { + if (factory.isFunctionComponentFactory) { _functionComponentFactories.add(factory); // will be validated below the for-loop. continue; @@ -278,8 +280,6 @@ Iterable getBoilerplateDeclarations( props: propsClassOrMixin, state: stateClassOrMixin); break; - case Version.v5_functionComponent: - break; } } } else { @@ -289,29 +289,28 @@ Iterable getBoilerplateDeclarations( consume(factory); consumePropsAndState(); yield PropsMapViewOrFunctionComponentDeclaration( - factory: factory, + factories: [factory], props: propsClassOrMixin, ); } } } - if(_functionComponentFactories.isNotEmpty) { + if (_functionComponentFactories.isNotEmpty) { final allUnusedProps = [ members.props, members.propsMixins, ].expand((i) => i).whereNot(hasBeenConsumed); for (final propsClassOrMixin in allUnusedProps) { - final associatedFactories = _functionComponentFactories.where((factory) => - getPropsNameFromConfig(factory) == propsClassOrMixin.name.name); + final associatedFactories = _functionComponentFactories + .where((factory) => getPropsNameFromConfig(factory) == propsClassOrMixin.name.name); if (associatedFactories.isNotEmpty) { associatedFactories.forEach(consume); consume(propsClassOrMixin); - yield FunctionComponentDeclaration( + yield PropsMapViewOrFunctionComponentDeclaration( factories: associatedFactories.toList(), - props: getUnion( - propsClassOrMixin), + props: getUnion(propsClassOrMixin), ); } } @@ -362,9 +361,7 @@ Iterable getBoilerplateDeclarations( final single = nonNullFactoryPropsOrComponents.single; final span = errorCollector.spanFor(single.node); if (single == factory) { - if(factory.versionConfidences.maxConfidence.version != Version.v5_functionComponent) { - errorCollector.addError(errorFactoryOnly, span); - } + errorCollector.addError(errorFactoryOnly, span); } else if (single == propsClass) { errorCollector.addError(errorPropsClassOnly, span); } else if (single == componentClass) { @@ -378,9 +375,7 @@ Iterable getBoilerplateDeclarations( } else if (propsClass == null) { errorCollector.addError(errorNoProps, span); } else if (componentClass == null) { - if(factory.versionConfidences.maxConfidence.version != Version.v5_functionComponent) { - errorCollector.addError(errorNoComponent, span); - } + errorCollector.addError(errorNoComponent, span); } continue; } @@ -399,7 +394,7 @@ const errorStateOnly = ' these are required to use UiState.'; const errorFactoryOnly = 'Could not find matching props class in this file;' - ' this is required to declare a props map view,' + ' this is required to declare a props map view or function component,' ' and a component class is also required to declare a class-based component.'; const errorPropsClassOnly = 'Could not find matching factory in this file;' diff --git a/lib/src/builder/parsing/member_association.dart b/lib/src/builder/parsing/member_association.dart index 26b061e12..1937207e2 100644 --- a/lib/src/builder/parsing/member_association.dart +++ b/lib/src/builder/parsing/member_association.dart @@ -77,8 +77,9 @@ Union _getNameMatchUnion getUnion(BoilerplateMember member) { - if(member is A) return Union.a(member); +Union getUnion( + BoilerplateMember member) { + if (member is A) return Union.a(member); if (member is B) return Union.b(member); @@ -127,9 +128,9 @@ Union getPropsFor( /// ADD DOC COMMENT IF KEPT String getPropsNameFromConfig(BoilerplateFactory factory) { final rightHandSide = factory.node.variables.firstInitializer; - if(rightHandSide == null || rightHandSide is! MethodInvocation) return null; + if (rightHandSide == null || rightHandSide is! MethodInvocation) return null; final args = (rightHandSide as MethodInvocation).argumentList.arguments; - if(args.length < 2) return null; + if (args.length < 2) return null; final config = args[1].toSource(); final startIndex = config.indexOf(RegExp(r'\$')) + 1; final configIndex = config.lastIndexOf(RegExp(r'Config')); diff --git a/lib/src/builder/parsing/members/component.dart b/lib/src/builder/parsing/members/component.dart index e591dc441..85b542bd9 100644 --- a/lib/src/builder/parsing/members/component.dart +++ b/lib/src/builder/parsing/members/component.dart @@ -99,8 +99,6 @@ class BoilerplateComponent extends BoilerplateMember { break; case Version.v2_legacyBackwardsCompat: case Version.v3_legacyDart2Only: - case Version.v5_functionComponent: - break; } const reservedMembers = { diff --git a/lib/src/builder/parsing/members/factory.dart b/lib/src/builder/parsing/members/factory.dart index 7571a73a2..c754707d7 100644 --- a/lib/src/builder/parsing/members/factory.dart +++ b/lib/src/builder/parsing/members/factory.dart @@ -45,6 +45,11 @@ class BoilerplateFactory extends BoilerplateMember { bool get hasFactoryAnnotation => node.hasAnnotationWithName('Factory'); + bool get isFunctionComponentFactory => + node.variables.firstInitializer is MethodInvocation && + (node.variables.firstInitializer as MethodInvocation).methodName.name == + 'uiFunctionComponent'; + /// Verifies the correct implementation of a boilerplate factory /// /// Major checks included are: @@ -54,7 +59,6 @@ class BoilerplateFactory extends BoilerplateMember { @override void validate(Version version, ErrorCollector errorCollector) { switch (version) { - case Version.v5_functionComponent: case Version.v4_mixinBased: break; case Version.v2_legacyBackwardsCompat: @@ -82,7 +86,7 @@ class BoilerplateFactory extends BoilerplateMember { anyDescendantIdentifiers( initializer, (identifier) => identifier.name == generatedFactoryName); - if (!referencesGeneratedFactory) { + if (!referencesGeneratedFactory && !isFunctionComponentFactory) { errorCollector.addError( 'Factory variables are stubs for the generated factories, and must ' 'be initialized with or otherwise reference the generated factory. Should be: `$factoryName = $generatedFactoryName`', diff --git a/lib/src/builder/parsing/members/props_and_state.dart b/lib/src/builder/parsing/members/props_and_state.dart index c61fbed16..7a1585038 100644 --- a/lib/src/builder/parsing/members/props_and_state.dart +++ b/lib/src/builder/parsing/members/props_and_state.dart @@ -62,8 +62,6 @@ abstract class BoilerplatePropsOrState extends BoilerplateTypedMapMember @override void validate(Version version, ErrorCollector errorCollector) { switch (version) { - case Version.v5_functionComponent: - break; case Version.v4_mixinBased: final node = this.node; if (node is MixinDeclaration) { diff --git a/lib/src/builder/parsing/members/props_and_state_mixins.dart b/lib/src/builder/parsing/members/props_and_state_mixins.dart index f6eee0483..7c3eaeaa9 100644 --- a/lib/src/builder/parsing/members/props_and_state_mixins.dart +++ b/lib/src/builder/parsing/members/props_and_state_mixins.dart @@ -76,8 +76,6 @@ abstract class BoilerplatePropsOrStateMixin extends BoilerplateTypedMapMember } switch (version) { - case Version.v5_functionComponent: - break; case Version.v4_mixinBased: final node = this.node; if (node is MixinDeclaration) { diff --git a/lib/src/builder/parsing/members_from_ast.dart b/lib/src/builder/parsing/members_from_ast.dart index 0f64c92d2..e7b33a0bd 100644 --- a/lib/src/builder/parsing/members_from_ast.dart +++ b/lib/src/builder/parsing/members_from_ast.dart @@ -169,13 +169,12 @@ class _BoilerplateMemberDetector { } final rightHandSide = node.variables.firstInitializer; - - if(rightHandSide is MethodInvocation && rightHandSide.methodName.name == 'uiFunctionComponent') { + if (rightHandSide is MethodInvocation && + rightHandSide.methodName.name == 'uiFunctionComponent') { onFactory(BoilerplateFactory( node, VersionConfidences( - v5_functionComponent: Confidence.likely, - v4_mixinBased: Confidence.none, + v4_mixinBased: Confidence.likely, v3_legacyDart2Only: Confidence.none, v2_legacyBackwardsCompat: Confidence.none, ))); @@ -202,7 +201,6 @@ class _BoilerplateMemberDetector { onFactory(BoilerplateFactory( node, VersionConfidences( - v5_functionComponent: Confidence.none, v4_mixinBased: Confidence.likely, v3_legacyDart2Only: Confidence.none, v2_legacyBackwardsCompat: Confidence.none, @@ -212,7 +210,6 @@ class _BoilerplateMemberDetector { onFactory(BoilerplateFactory( node, VersionConfidences( - v5_functionComponent: Confidence.none, v4_mixinBased: Confidence.neutral, v2_legacyBackwardsCompat: Confidence.none, v3_legacyDart2Only: Confidence.none, @@ -326,21 +323,18 @@ class _BoilerplateMemberDetector { v2_legacyBackwardsCompat: Confidence.likely, v3_legacyDart2Only: Confidence.unlikely, v4_mixinBased: Confidence.unlikely, - v5_functionComponent: Confidence.none, ); } else if (hasGeneratedPrefix) { return VersionConfidences( v2_legacyBackwardsCompat: Confidence.unlikely, v3_legacyDart2Only: Confidence.likely, v4_mixinBased: Confidence.unlikely, - v5_functionComponent: Confidence.none, ); } else { return VersionConfidences( v2_legacyBackwardsCompat: Confidence.unlikely, v3_legacyDart2Only: Confidence.unlikely, v4_mixinBased: Confidence.likely, - v5_functionComponent: Confidence.none, ); } } @@ -359,7 +353,6 @@ class _BoilerplateMemberDetector { v3_legacyDart2Only: Confidence.unlikely, // Annotated abstract props/state don't exist to the new boilerplate v4_mixinBased: Confidence.none, - v5_functionComponent: Confidence.none, ); } else { return VersionConfidences( @@ -367,7 +360,6 @@ class _BoilerplateMemberDetector { v3_legacyDart2Only: Confidence.likely, // Annotated abstract props/state don't exist to the new boilerplate v4_mixinBased: Confidence.none, - v5_functionComponent: Confidence.none, ); } } @@ -391,7 +383,6 @@ class _BoilerplateMemberDetector { ? Confidence.none : (hasGeneratedPrefix ? Confidence.likely : Confidence.unlikely), v4_mixinBased: isMixin ? Confidence.likely : Confidence.unlikely, - v5_functionComponent: Confidence.none, ); } @@ -418,7 +409,6 @@ class _BoilerplateMemberDetector { v2_legacyBackwardsCompat: Confidence.none, v3_legacyDart2Only: Confidence.none, v4_mixinBased: Confidence.likely, - v5_functionComponent: Confidence.none, ); } @@ -471,8 +461,6 @@ class _BoilerplateMemberDetector { 'FluxUiStatefulComponent2' }; final confidences = VersionConfidences( - // Component classes don't exist in function components - v5_functionComponent: Confidence.none, // If the component extends from a base class known to be supported by the new boilerplate, // has no annotation, is not abstract, and does not have $isClassGenerated, then it's // most likely intended to be part of a new boilerplate class component declaration. diff --git a/lib/src/builder/parsing/version.dart b/lib/src/builder/parsing/version.dart index 8e7c96851..387ccbcc4 100644 --- a/lib/src/builder/parsing/version.dart +++ b/lib/src/builder/parsing/version.dart @@ -88,7 +88,6 @@ enum Version { v2_legacyBackwardsCompat, v3_legacyDart2Only, v4_mixinBased, - v5_functionComponent, } /// Priority of the possible boilerplate versions. @@ -98,7 +97,6 @@ enum Version { /// /// The length of this iterable should match the length of values in [Version]. const _versionsInPriorityOrder = [ - Version.v5_functionComponent, Version.v4_mixinBased, Version.v2_legacyBackwardsCompat, Version.v3_legacyDart2Only, @@ -168,7 +166,6 @@ class VersionConfidences { final int v2_legacyBackwardsCompat; final int v3_legacyDart2Only; final int v4_mixinBased; - final int v5_functionComponent; /// The highest confidence within the class. /// @@ -180,7 +177,6 @@ class VersionConfidences { @required this.v2_legacyBackwardsCompat, @required this.v3_legacyDart2Only, @required this.v4_mixinBased, - @required this.v5_functionComponent, }); /// Constructor to set all confidence values to the same thing @@ -189,7 +185,6 @@ class VersionConfidences { v2_legacyBackwardsCompat: value, v3_legacyDart2Only: value, v4_mixinBased: value, - v5_functionComponent: value, ); /// Constructor to initialize the class with no data. @@ -203,7 +198,6 @@ class VersionConfidences { VersionConfidencePair(Version.v2_legacyBackwardsCompat, v2_legacyBackwardsCompat), VersionConfidencePair(Version.v3_legacyDart2Only, v3_legacyDart2Only), VersionConfidencePair(Version.v4_mixinBased, v4_mixinBased), - VersionConfidencePair(Version.v5_functionComponent, v5_functionComponent), ]; /// Adds all versions in the class to another set of versions from a different [VersionConfidences] @@ -213,7 +207,6 @@ class VersionConfidences { v2_legacyBackwardsCompat: v2_legacyBackwardsCompat + other.v2_legacyBackwardsCompat, v3_legacyDart2Only: v3_legacyDart2Only + other.v3_legacyDart2Only, v4_mixinBased: v4_mixinBased + other.v4_mixinBased, - v5_functionComponent: v5_functionComponent + other.v5_functionComponent, ); } @@ -240,7 +233,6 @@ class VersionConfidences { 'v2': Confidence.description(v2_legacyBackwardsCompat), 'v3': Confidence.description(v3_legacyDart2Only), 'v4': Confidence.description(v4_mixinBased), - 'v5': Confidence.description(v5_functionComponent), }; return '$runtimeType $confidenceMap'; From 6958e221cbf169eb7ee12796a3b5286a68029bf9 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Thu, 9 Jul 2020 17:11:56 -0700 Subject: [PATCH 06/38] Update example --- example/builder/src/function_component.dart | 159 +----------------- .../src/function_component.over_react.g.dart | 70 ++++++++ 2 files changed, 77 insertions(+), 152 deletions(-) diff --git a/example/builder/src/function_component.dart b/example/builder/src/function_component.dart index d2004f06f..c9442e48f 100644 --- a/example/builder/src/function_component.dart +++ b/example/builder/src/function_component.dart @@ -1,10 +1,7 @@ import 'dart:developer'; -import 'package:js/js_util.dart'; -import 'package:meta/meta.dart'; import 'package:over_react/over_react.dart'; import 'package:over_react/src/util/prop_errors.dart'; -import 'package:react/react.dart' as react; import 'package:react/react_client.dart'; import 'package:react/react_client/js_backed_map.dart'; import 'package:react/react_client/react_interop.dart'; @@ -21,13 +18,8 @@ mixin BasicPropsMixin on UiProps { } UiFactory Basic = uiFunctionComponent((props) { - return Dom.div()( - Dom.div()('prop id: ${props.id}'), - Dom.div()('default prop testing: ${props.basicProp}'), - Dom.div()('default prop testing: ${props.basic1}'), - Dom.div()(props.basic3, 'children: ${props.children}'), - ); -}, $BasicPropsConfig); + return Dom.div()(); +}, $BasicPropsMixinConfig); UiFactory Basic2 = uiFunctionComponent((props) { return Dom.div()( @@ -36,7 +28,7 @@ UiFactory Basic2 = uiFunctionComponent((props) { Dom.div()('default prop testing: ${props.basic1}'), Dom.div()(props.basic3, 'children: ${props.children}'), ); -}, $BasicPropsConfig); +}, $BasicPropsMixinConfig); // 1. Memory leak? @@ -55,7 +47,7 @@ final Simple = uiFunctionComponent((props) { Dom.div()('default prop testing: ${props.basic1}'), Dom.div()(null, props.basic4, 'children: ${props.children}'), ); -}, $BasicPropsConfig, initStatics: (statics) {// ignore: undefined_identifier +}, $BasicPropsMixinConfig, initStatics: (statics) {// ignore: undefined_identifier statics.defaultProps = (statics.newProps() ..basicProp = 'basicProp' ..basic1 = 'basic1' @@ -71,7 +63,7 @@ final Simple2 = uiFunctionComponent((props) { Dom.div()('default prop testing: ${props.basic1}'), Dom.div()(null, props.basic4, 'children: ${props.children}'), ); -}, $BasicPropsConfig, initStatics: (statics) { +}, $BasicPropsMixinConfig, initStatics: (statics) { statics ..defaultProps = (statics.newProps() ..basicProp = 'basicProp' @@ -130,6 +122,7 @@ final Simple2 = uiFunctionComponent((props) { // } //} + ReactElement functionComponentContent() { // FIXME look into naming final genericFactory = uiFunctionComponent((props) { @@ -157,7 +150,7 @@ ReactElement functionComponentContent() { Dom.div()('prop basic1: ${props.basic1}'), ); // FIXME should displayName really default to "Basic" in this case? - }, $BasicPropsConfig, displayName: 'basicFactory');// ignore: undefined_identifier + }, $BasicPropsMixinConfig, displayName: 'basicFactory');// ignore: undefined_identifier return Fragment()( (genericFactory()..id = '1')(), @@ -166,141 +159,3 @@ ReactElement functionComponentContent() { (Simple()..basicProp = 'basicProp')(), ); } - - -UiFactory uiFunctionComponent( - dynamic Function(T props) functionComponent, - // FIXME allow passing in displayName for generic function components - FunctionComponentConfig config, - { - PropsFactory propsFactory, - String displayName, - void Function(UiFunctionComponentStatics) initStatics, - }) { - if (config != null) { - if (propsFactory != null) throw ArgumentError('propsFactory cannot be used along with config'); - propsFactory = config.propsFactory; - displayName ??= config.componentName; - } - - // Get the display name from the inner function if possible so it doesn't become `_uiFunctionComponentWrapper` - // FIXME make this work in DDC and make more robust - displayName ??= getFunctionName(functionComponent); - - dynamic _uiFunctionComponentWrapper(Map props) { - return functionComponent(propsFactory.jsMap(props as JsBackedMap)); - } - - /// FIXME DartFunctionComponent should be JsBackedMap? - final factory = react.registerFunctionComponent(_uiFunctionComponentWrapper, - displayName: displayName); - - if (propsFactory == null) { - // todo allow passing in of custom uiFactory/typedPropsFactory - // TODO make it easier to pass in parts of generatedInfo - if (T != UiProps && T != GenericUiProps) { - throw ArgumentError('config.propsFactory must be provided when using custom props classes'); - } - propsFactory = PropsFactory.fromUiFactory(([backingMap]) => GenericUiProps(factory, backingMap)) as PropsFactory; - } - - if (initStatics != null) { - final statics = UiFunctionComponentStatics._( - newProps: () => propsFactory.jsMap(JsBackedMap()), - keyFor: (accessProps) => getPropKey(accessProps, propsFactory.map) - ); - initStatics(statics); - - if (statics.defaultProps != null) { - // fixme need to move to react-dart - (factory.reactFunction as ReactClass).defaultProps = JsBackedMap.from(statics.defaultProps).jsObject; - } - // fixme need to implement in react-dart -// if (statics.propTypes != null) {} - } - - T _uiFactory([Map backingMap]) { - T builder; - if (backingMap == null) { - builder = propsFactory.jsMap(JsBackedMap()); - } else if (backingMap is JsBackedMap) { - builder = propsFactory.jsMap(backingMap); - } else { - builder = propsFactory.map(backingMap); - } - - return builder..componentFactory = factory; - } - return _uiFactory; -} - -String getFunctionName(Function f) { - if (f == null) throw ArgumentError.notNull('f'); - - // DDC - // todo - - // Dart2js - final constructor = getProperty(f, 'constructor'); - if (constructor != null) { - return getProperty(constructor, 'name'); - } - - return null; -} - -class UiFunctionComponentStatics { - Map defaultProps; - Map> propTypes; - - final String Function(void Function(T) accessProps) keyFor; - final T Function() newProps; - - UiFunctionComponentStatics._({this.keyFor, this.newProps}); -// -// T newProps() => this._newProps(); -// -// String keyFor(void Function(T) accessProps) => this._keyFor(accessProps); - -} - -class GenericUiProps extends UiProps { - @override - final Map props; - - GenericUiProps(ReactComponentFactoryProxy componentFactory, [Map props]) : - this.props = props ?? JsBackedMap() { - this.componentFactory = componentFactory; - } - - @override - String get propKeyNamespace => ''; - - @override - bool get $isClassGenerated => true; -} - -typedef FunctionFactoryFactory = UiFactory Function(ReactDartFunctionComponentFactoryProxy); - - -@protected -class FunctionComponentConfig { - @protected - final PropsFactory propsFactory; - final String componentName; - - @protected - FunctionComponentConfig({this.propsFactory, this.componentName}); -} - -class PropsFactory { - final T Function(Map props) map; - final T Function(JsBackedMap props) jsMap; - - PropsFactory({ - @required this.map, - @required this.jsMap, - }); - - PropsFactory.fromUiFactory(UiFactory factory) : this.map = factory, this.jsMap = factory; -} diff --git a/example/builder/src/function_component.over_react.g.dart b/example/builder/src/function_component.over_react.g.dart index 04787d539..d5372eda9 100644 --- a/example/builder/src/function_component.over_react.g.dart +++ b/example/builder/src/function_component.over_react.g.dart @@ -84,3 +84,73 @@ const PropsMeta _$metaForBasicPropsMixin = PropsMeta( fields: $BasicPropsMixin.$props, keys: $BasicPropsMixin.$propKeys, ); + +_$$BasicPropsMixin _$Basic([Map backingProps]) => backingProps == null + ? _$$BasicPropsMixin$JsMap(JsBackedMap()) + : _$$BasicPropsMixin(backingProps); + +// Concrete props implementation. +// +// Implements constructor and backing map, and links up to generated component factory. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +abstract class _$$BasicPropsMixin extends UiProps + with + BasicPropsMixin, + $BasicPropsMixin // If this generated mixin is undefined, it's likely because BasicPropsMixin is not a valid `mixin`-based props mixin, or because it is but the generated mixin was not exported. Check the declaration of BasicPropsMixin. +{ + _$$BasicPropsMixin._(); + + factory _$$BasicPropsMixin(Map backingMap) { + if (backingMap == null || backingMap is JsBackedMap) { + return _$$BasicPropsMixin$JsMap(backingMap); + } else { + return _$$BasicPropsMixin$PlainMap(backingMap); + } + } + + /// Let `UiProps` internals know that this class has been generated. + @override + bool get $isClassGenerated => true; + + /// The default namespace for the prop getters/setters generated for this class. + @override + String get propKeyNamespace => ''; +} + +// Concrete props implementation that can be backed by any [Map]. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$BasicPropsMixin$PlainMap extends _$$BasicPropsMixin { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$BasicPropsMixin$PlainMap(Map backingMap) + : this._props = {}, + super._() { + this._props = backingMap ?? {}; + } + + /// The backing props map proxied by this class. + @override + Map get props => _props; + Map _props; +} + +// Concrete props implementation that can only be backed by [JsMap], +// allowing dart2js to compile more optimal code for key-value pair reads/writes. +@Deprecated('This API is for use only within generated code.' + ' Do not reference it in your code, as it may change at any time.') +class _$$BasicPropsMixin$JsMap extends _$$BasicPropsMixin { + // This initializer of `_props` to an empty map, as well as the reassignment + // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217 + _$$BasicPropsMixin$JsMap(JsBackedMap backingMap) + : this._props = JsBackedMap(), + super._() { + this._props = backingMap ?? JsBackedMap(); + } + + /// The backing props map proxied by this class. + @override + JsBackedMap get props => _props; + JsBackedMap _props; +} From 7dc77f675c38ee9bb774d1b3db9e4895fc48eef9 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Fri, 10 Jul 2020 11:20:27 -0700 Subject: [PATCH 07/38] Add codegen for FunctionComponentConfig --- example/builder/src/function_component.dart | 12 ++++++--- .../src/function_component.over_react.g.dart | 10 +++++--- .../codegen/typed_map_impl_generator.dart | 25 +++++++++++++++++++ .../builder/parsing/members/component.dart | 1 + .../function_component.dart | 8 +----- 5 files changed, 42 insertions(+), 14 deletions(-) diff --git a/example/builder/src/function_component.dart b/example/builder/src/function_component.dart index c9442e48f..b49afb28d 100644 --- a/example/builder/src/function_component.dart +++ b/example/builder/src/function_component.dart @@ -18,7 +18,12 @@ mixin BasicPropsMixin on UiProps { } UiFactory Basic = uiFunctionComponent((props) { - return Dom.div()(); + return Dom.div()( + Dom.div()('prop id: ${props.id}'), + Dom.div()('default prop testing: ${props.basicProp}'), + Dom.div()('default prop testing: ${props.basic1}'), + Dom.div()(props.basic3, 'children: ${props.children}'), + ); }, $BasicPropsMixinConfig); UiFactory Basic2 = uiFunctionComponent((props) { @@ -47,7 +52,7 @@ final Simple = uiFunctionComponent((props) { Dom.div()('default prop testing: ${props.basic1}'), Dom.div()(null, props.basic4, 'children: ${props.children}'), ); -}, $BasicPropsMixinConfig, initStatics: (statics) {// ignore: undefined_identifier +}, $BasicPropsMixinConfig, initStatics: (statics) { statics.defaultProps = (statics.newProps() ..basicProp = 'basicProp' ..basic1 = 'basic1' @@ -122,7 +127,6 @@ final Simple2 = uiFunctionComponent((props) { // } //} - ReactElement functionComponentContent() { // FIXME look into naming final genericFactory = uiFunctionComponent((props) { @@ -150,7 +154,7 @@ ReactElement functionComponentContent() { Dom.div()('prop basic1: ${props.basic1}'), ); // FIXME should displayName really default to "Basic" in this case? - }, $BasicPropsMixinConfig, displayName: 'basicFactory');// ignore: undefined_identifier + }, $BasicPropsMixinConfig, displayName: 'basicFactory'); return Fragment()( (genericFactory()..id = '1')(), diff --git a/example/builder/src/function_component.over_react.g.dart b/example/builder/src/function_component.over_react.g.dart index d5372eda9..7384c1242 100644 --- a/example/builder/src/function_component.over_react.g.dart +++ b/example/builder/src/function_component.over_react.g.dart @@ -85,9 +85,13 @@ const PropsMeta _$metaForBasicPropsMixin = PropsMeta( keys: $BasicPropsMixin.$propKeys, ); -_$$BasicPropsMixin _$Basic([Map backingProps]) => backingProps == null - ? _$$BasicPropsMixin$JsMap(JsBackedMap()) - : _$$BasicPropsMixin(backingProps); +final FunctionComponentConfig<_$$BasicPropsMixin> $BasicPropsMixinConfig = + FunctionComponentConfig( + propsFactory: PropsFactory( + map: (map) => _$$BasicPropsMixin(map), + jsMap: (map) => _$$BasicPropsMixin$JsMap(map), + ), + componentName: 'Basic'); // Concrete props implementation. // diff --git a/lib/src/builder/codegen/typed_map_impl_generator.dart b/lib/src/builder/codegen/typed_map_impl_generator.dart index d30af1dac..8f1d5df7d 100644 --- a/lib/src/builder/codegen/typed_map_impl_generator.dart +++ b/lib/src/builder/codegen/typed_map_impl_generator.dart @@ -339,6 +339,8 @@ class _TypedMapImplGenerator extends TypedMapImplGenerator { final String componentFactoryName; + final bool isFunctionComponentDeclaration; + @override final Version version; @@ -348,6 +350,7 @@ class _TypedMapImplGenerator extends TypedMapImplGenerator { member = declaration.props.either, isProps = true, componentFactoryName = ComponentNames(declaration.component.name.name).componentFactoryName, + isFunctionComponentDeclaration = false, version = declaration.version; _TypedMapImplGenerator.state(ClassComponentDeclaration declaration) @@ -356,6 +359,7 @@ class _TypedMapImplGenerator extends TypedMapImplGenerator { member = declaration.state.either, isProps = false, componentFactoryName = ComponentNames(declaration.component.name.name).componentFactoryName, + isFunctionComponentDeclaration = false, version = declaration.version; _TypedMapImplGenerator.propsMapViewOrFunctionComponent( @@ -366,11 +370,32 @@ class _TypedMapImplGenerator extends TypedMapImplGenerator { member = declaration.props.either, isProps = true, componentFactoryName = 'null', + isFunctionComponentDeclaration = declaration.factories.first.isFunctionComponentFactory, version = declaration.version; @override bool get isComponent2 => true; + String _generateFunctionComponentConfig() { + return 'final FunctionComponentConfig<${names.implName}> ' + '${names.generatedMixinName}Config = FunctionComponentConfig(\n' + 'propsFactory: PropsFactory(\n' + 'map: (map) => ${names.implName}(map),\n' + 'jsMap: (map) => ${names.jsMapImplName}(map),),\n' + // TODO: figure out how to connect this with factory + 'componentName: \'${factoryNames.consumerName}\');\n\n'; + } + + + @override + void _generateFactory() { + if(isFunctionComponentDeclaration) { + outputContentsBuffer.write(_generateFunctionComponentConfig()); + } else { + super._generateFactory(); + } + } + @override void _generatePropsImpl() { outputContentsBuffer.write(_generateConcretePropsOrStateImpl( diff --git a/lib/src/builder/parsing/members/component.dart b/lib/src/builder/parsing/members/component.dart index 85b542bd9..998ab0e17 100644 --- a/lib/src/builder/parsing/members/component.dart +++ b/lib/src/builder/parsing/members/component.dart @@ -99,6 +99,7 @@ class BoilerplateComponent extends BoilerplateMember { break; case Version.v2_legacyBackwardsCompat: case Version.v3_legacyDart2Only: + break; } const reservedMembers = { diff --git a/lib/src/component_declaration/function_component.dart b/lib/src/component_declaration/function_component.dart index 5f71f0512..9849c3a24 100644 --- a/lib/src/component_declaration/function_component.dart +++ b/lib/src/component_declaration/function_component.dart @@ -1,4 +1,4 @@ -// Copyright 2016 Workiva Inc. +// Copyright 2020 Workiva Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,12 +15,9 @@ // ignore_for_file: prefer_generic_function_type_aliases library over_react.component_declaration.function_component; -import 'dart:developer'; - import 'package:js/js_util.dart'; import 'package:meta/meta.dart'; import 'package:over_react/over_react.dart'; -import 'package:over_react/src/util/prop_errors.dart'; import 'package:react/react.dart' as react; import 'package:react/react_client.dart'; import 'package:react/react_client/js_backed_map.dart'; @@ -28,9 +25,6 @@ import 'package:react/react_client/react_interop.dart'; export 'component_type_checking.dart' show isComponentOfType, isValidElementOfType; - - - UiFactory uiFunctionComponent( dynamic Function(T props) functionComponent, // FIXME allow passing in displayName for generic function components From 027cd66e81146ff84b47a7d35fb3cb949caf81a2 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Fri, 10 Jul 2020 11:35:46 -0700 Subject: [PATCH 08/38] Fix test failures and format --- lib/src/builder/codegen/typed_map_impl_generator.dart | 5 ++--- test/vm_tests/builder/declaration_parsing_test.dart | 8 ++++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/src/builder/codegen/typed_map_impl_generator.dart b/lib/src/builder/codegen/typed_map_impl_generator.dart index 8f1d5df7d..f6a6ef24d 100644 --- a/lib/src/builder/codegen/typed_map_impl_generator.dart +++ b/lib/src/builder/codegen/typed_map_impl_generator.dart @@ -382,14 +382,13 @@ class _TypedMapImplGenerator extends TypedMapImplGenerator { 'propsFactory: PropsFactory(\n' 'map: (map) => ${names.implName}(map),\n' 'jsMap: (map) => ${names.jsMapImplName}(map),),\n' - // TODO: figure out how to connect this with factory + // TODO: figure out how to connect this with factory 'componentName: \'${factoryNames.consumerName}\');\n\n'; } - @override void _generateFactory() { - if(isFunctionComponentDeclaration) { + if (isFunctionComponentDeclaration) { outputContentsBuffer.write(_generateFunctionComponentConfig()); } else { super._generateFactory(); diff --git a/test/vm_tests/builder/declaration_parsing_test.dart b/test/vm_tests/builder/declaration_parsing_test.dart index f8e736802..59662dc41 100644 --- a/test/vm_tests/builder/declaration_parsing_test.dart +++ b/test/vm_tests/builder/declaration_parsing_test.dart @@ -1538,7 +1538,9 @@ main() { ])); final decl = declarations.firstWhereType(); - expect(decl.factory?.name?.name, name); + expect(decl.factories, isNotNull); + expect(decl.factories.length, 1); + expect(decl.factories.first.name.name, name); expect(decl.props.b?.name?.name, propsName); expect(decl.version, Version.v4_mixinBased); }); @@ -1550,7 +1552,9 @@ main() { '''); final decl = expectSingleOfType(declarations); - expect(decl.factory?.name?.name, name); + expect(decl.factories, isNotNull); + expect(decl.factories.length, 1); + expect(decl.factories.first.name.name, name); expect(decl.props.a?.name?.name, propsName); expect(decl.version, Version.v4_mixinBased); }); From 3e62161fe38254979837e01b232fae332a485c69 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Fri, 10 Jul 2020 12:35:28 -0700 Subject: [PATCH 09/38] Update getPropsNameFromConfig --- lib/src/builder/parsing/member_association.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/src/builder/parsing/member_association.dart b/lib/src/builder/parsing/member_association.dart index 1937207e2..40caecc30 100644 --- a/lib/src/builder/parsing/member_association.dart +++ b/lib/src/builder/parsing/member_association.dart @@ -127,14 +127,13 @@ Union getPropsFor( /// ADD DOC COMMENT IF KEPT String getPropsNameFromConfig(BoilerplateFactory factory) { + if(factory.propsGenericArg != null) { + return factory.propsGenericArg.typeNameWithoutPrefix; + } final rightHandSide = factory.node.variables.firstInitializer; if (rightHandSide == null || rightHandSide is! MethodInvocation) return null; - final args = (rightHandSide as MethodInvocation).argumentList.arguments; - if (args.length < 2) return null; - final config = args[1].toSource(); - final startIndex = config.indexOf(RegExp(r'\$')) + 1; - final configIndex = config.lastIndexOf(RegExp(r'Config')); - return config.substring(startIndex, configIndex == -1 ? config.length : configIndex); + final args = (rightHandSide as MethodInvocation).typeArguments.arguments.firstOrNull; + return args?.typeNameWithoutPrefix; } /// Retrieves the props for a given [member] if it is found in [states] or [stateMixins]. From e2da213f52de96244cd265c955d5cdca8e865622 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Fri, 10 Jul 2020 17:09:42 -0700 Subject: [PATCH 10/38] Add initial parsing tests --- example/builder/src/function_component.dart | 2 +- .../builder/parsing/member_association.dart | 2 +- .../builder/declaration_parsing_test.dart | 111 ++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/example/builder/src/function_component.dart b/example/builder/src/function_component.dart index b49afb28d..e14d787a2 100644 --- a/example/builder/src/function_component.dart +++ b/example/builder/src/function_component.dart @@ -147,7 +147,7 @@ ReactElement functionComponentContent() { // Dom.div()('prop basic1: ${props.basic1}'), // ); // }, $basicConfig); - + // FIXME right now this only works because a top level factory declaration made the config generate final basicFactory = uiFunctionComponent((props) { return Dom.div()( Dom.div()('prop id: ${props.id}'), diff --git a/lib/src/builder/parsing/member_association.dart b/lib/src/builder/parsing/member_association.dart index 40caecc30..7c384502a 100644 --- a/lib/src/builder/parsing/member_association.dart +++ b/lib/src/builder/parsing/member_association.dart @@ -127,7 +127,7 @@ Union getPropsFor( /// ADD DOC COMMENT IF KEPT String getPropsNameFromConfig(BoilerplateFactory factory) { - if(factory.propsGenericArg != null) { + if (factory.propsGenericArg != null) { return factory.propsGenericArg.typeNameWithoutPrefix; } final rightHandSide = factory.node.variables.firstInitializer; diff --git a/test/vm_tests/builder/declaration_parsing_test.dart b/test/vm_tests/builder/declaration_parsing_test.dart index 59662dc41..ce6eeeeac 100644 --- a/test/vm_tests/builder/declaration_parsing_test.dart +++ b/test/vm_tests/builder/declaration_parsing_test.dart @@ -1566,6 +1566,117 @@ main() { sharedMapViewTests(hasMapViewSuffix: true); }); }); + + group('function component', () { + void sharedFunctionComponentTests({bool hasLeftHandType}) { + test('(shorthand)', () { + final leftHandType = hasLeftHandType ? 'UiFactory' : 'final'; + final typeParams = hasLeftHandType ? '' : ''; + setUpAndParse(''' + $leftHandType Foo = uiFunctionComponent$typeParams((props) { + return Dom.div()(); + }, \$FooPropsMixinConfig); + + $leftHandType Bar = uiFunctionComponent$typeParams((props) { + return Dom.div()(); + }, \$FooPropsMixinConfig); + + $leftHandType Baz = uiFunctionComponent$typeParams((props) { + return Dom.div()(); + }, \$FooPropsMixinConfig); + + mixin FooPropsMixin on UiProps {} + '''); + + expect(declarations, unorderedEquals([ + isA(), + isA(), + ])); + final decl = declarations.firstWhereType(); + + expect(decl.factories, isNotNull); + expect(decl.factories.length, 3); + expect(decl.factories.map((factory) => factory.name.name), unorderedEquals([ + 'Foo', + 'Bar', + 'Baz', + ])); + expect(decl.props.b?.name?.name, 'FooPropsMixin'); + expect(decl.version, Version.v4_mixinBased); + }); + + test('(verbose)', () { + final leftHandType = hasLeftHandType ? 'UiFactory' : 'final'; + final typeParams = hasLeftHandType ? '' : ''; + setUpAndParse(''' + $leftHandType Foo = uiFunctionComponent$typeParams((props) { + return Dom.div()(); + }, \$FooPropsConfig); + class FooProps = UiProps with FooPropsMixin; + '''); + final decl = expectSingleOfType(declarations); + + expect(decl.factories, isNotNull); + expect(decl.factories.length, 1); + expect(decl.factories.first.name.name, 'Foo'); + expect(decl.props.a?.name?.name, 'FooProps'); + expect(decl.version, Version.v4_mixinBased); + }); + + test('with multiple mixins in the same file', () { + final leftHandType = hasLeftHandType ? 'UiFactory' : 'final'; + final leftHandType2 = hasLeftHandType ? 'UiFactory' : 'final'; + final typeParams = hasLeftHandType ? '' : ''; + final typeParams2 = hasLeftHandType ? '' : ''; + setUpAndParse(''' + $leftHandType Foo = uiFunctionComponent$typeParams((props) { + return Dom.div()(); + }, \$FooPropsMixinConfig); + + $leftHandType Bar = uiFunctionComponent$typeParams((props) { + return Dom.div()(); + }, \$FooPropsMixinConfig); + + $leftHandType2 Baz = uiFunctionComponent$typeParams2((props) { + return Dom.div()(); + }, \$BarPropsMixinConfig); + + mixin FooPropsMixin on UiProps {} + mixin BarPropsMixin on UiProps {} + '''); + + expect(declarations, unorderedEquals([ + isA(), + isA(), + isA(), + isA(), + ])); + final decl = declarations.whereType().toList(); + + expect(decl.length, 2); + expect(decl.first.factories, isNotNull); + expect(decl.first.factories.length, 2); + expect(decl.first.factories.map((factory) => factory.name.name), unorderedEquals([ + 'Foo', + 'Bar', + ])); + expect(decl.first.props.b?.name?.name, 'FooPropsMixin'); + expect(decl.first.version, Version.v4_mixinBased); + + expect(decl[1].factories, isNotNull); + expect(decl[1].factories.length, 1); + expect(decl[1].factories.first.name.name, 'Baz'); + expect(decl[1].props.b?.name?.name, 'BarPropsMixin'); + expect(decl[1].version, Version.v4_mixinBased); + }); + } + + sharedFunctionComponentTests(hasLeftHandType: true); + + group('without left hand typing', () { + sharedFunctionComponentTests(hasLeftHandType: false); + }); + }); }); }); From 42005e237e72cf10a32a42c98c4d7028b4cee3c3 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Mon, 13 Jul 2020 10:44:33 -0700 Subject: [PATCH 11/38] Generate config for each factory to keep correct displayName --- example/builder/src/function_component.dart | 18 +++++++----- .../src/function_component.over_react.g.dart | 26 ++++++++++++++++- .../codegen/typed_map_impl_generator.dart | 29 ++++++++++--------- 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/example/builder/src/function_component.dart b/example/builder/src/function_component.dart index e14d787a2..424083383 100644 --- a/example/builder/src/function_component.dart +++ b/example/builder/src/function_component.dart @@ -24,7 +24,7 @@ UiFactory Basic = uiFunctionComponent((props) { Dom.div()('default prop testing: ${props.basic1}'), Dom.div()(props.basic3, 'children: ${props.children}'), ); -}, $BasicPropsMixinConfig); +}, $BasicPropsConfig); UiFactory Basic2 = uiFunctionComponent((props) { return Dom.div()( @@ -33,7 +33,7 @@ UiFactory Basic2 = uiFunctionComponent((props) { Dom.div()('default prop testing: ${props.basic1}'), Dom.div()(props.basic3, 'children: ${props.children}'), ); -}, $BasicPropsMixinConfig); +}, $Basic2PropsConfig); // 1. Memory leak? @@ -52,7 +52,7 @@ final Simple = uiFunctionComponent((props) { Dom.div()('default prop testing: ${props.basic1}'), Dom.div()(null, props.basic4, 'children: ${props.children}'), ); -}, $BasicPropsMixinConfig, initStatics: (statics) { +}, $SimplePropsConfig, initStatics: (statics) { statics.defaultProps = (statics.newProps() ..basicProp = 'basicProp' ..basic1 = 'basic1' @@ -68,7 +68,7 @@ final Simple2 = uiFunctionComponent((props) { Dom.div()('default prop testing: ${props.basic1}'), Dom.div()(null, props.basic4, 'children: ${props.children}'), ); -}, $BasicPropsMixinConfig, initStatics: (statics) { +}, $Simple2PropsConfig, initStatics: (statics) { statics ..defaultProps = (statics.newProps() ..basicProp = 'basicProp' @@ -128,13 +128,15 @@ final Simple2 = uiFunctionComponent((props) { //} ReactElement functionComponentContent() { - // FIXME look into naming - final genericFactory = uiFunctionComponent((props) { + GenericFactory(props) { debugger(); return Dom.div()( Dom.div()('prop id: ${props.id}'), ); - }, null); + } + + // FIXME look into naming + final genericFactory = uiFunctionComponent(GenericFactory, null); // // @@ -154,7 +156,7 @@ ReactElement functionComponentContent() { Dom.div()('prop basic1: ${props.basic1}'), ); // FIXME should displayName really default to "Basic" in this case? - }, $BasicPropsMixinConfig, displayName: 'basicFactory'); + }, $BasicPropsConfig, displayName: 'basicFactory'); return Fragment()( (genericFactory()..id = '1')(), diff --git a/example/builder/src/function_component.over_react.g.dart b/example/builder/src/function_component.over_react.g.dart index 7384c1242..b892c7229 100644 --- a/example/builder/src/function_component.over_react.g.dart +++ b/example/builder/src/function_component.over_react.g.dart @@ -85,7 +85,7 @@ const PropsMeta _$metaForBasicPropsMixin = PropsMeta( keys: $BasicPropsMixin.$propKeys, ); -final FunctionComponentConfig<_$$BasicPropsMixin> $BasicPropsMixinConfig = +final FunctionComponentConfig<_$$BasicPropsMixin> $BasicPropsConfig = FunctionComponentConfig( propsFactory: PropsFactory( map: (map) => _$$BasicPropsMixin(map), @@ -93,6 +93,30 @@ final FunctionComponentConfig<_$$BasicPropsMixin> $BasicPropsMixinConfig = ), componentName: 'Basic'); +final FunctionComponentConfig<_$$BasicPropsMixin> $Basic2PropsConfig = + FunctionComponentConfig( + propsFactory: PropsFactory( + map: (map) => _$$BasicPropsMixin(map), + jsMap: (map) => _$$BasicPropsMixin$JsMap(map), + ), + componentName: 'Basic2'); + +final FunctionComponentConfig<_$$BasicPropsMixin> $SimplePropsConfig = + FunctionComponentConfig( + propsFactory: PropsFactory( + map: (map) => _$$BasicPropsMixin(map), + jsMap: (map) => _$$BasicPropsMixin$JsMap(map), + ), + componentName: 'Simple'); + +final FunctionComponentConfig<_$$BasicPropsMixin> $Simple2PropsConfig = + FunctionComponentConfig( + propsFactory: PropsFactory( + map: (map) => _$$BasicPropsMixin(map), + jsMap: (map) => _$$BasicPropsMixin$JsMap(map), + ), + componentName: 'Simple2'); + // Concrete props implementation. // // Implements constructor and backing map, and links up to generated component factory. diff --git a/lib/src/builder/codegen/typed_map_impl_generator.dart b/lib/src/builder/codegen/typed_map_impl_generator.dart index f6a6ef24d..4ac84987e 100644 --- a/lib/src/builder/codegen/typed_map_impl_generator.dart +++ b/lib/src/builder/codegen/typed_map_impl_generator.dart @@ -46,7 +46,7 @@ abstract class TypedMapImplGenerator extends BoilerplateDeclarationGenerator { TypedMapNames get names; bool get isComponent2; - FactoryNames get factoryNames; + List get factoryNames; bool get isProps; BoilerplateTypedMapMember get member; @@ -73,7 +73,7 @@ abstract class TypedMapImplGenerator extends BoilerplateDeclarationGenerator { if (factoryNames == null) throw StateError('factoryNames must not be null'); outputContentsBuffer - .write('${names.implName} ${factoryNames.implName}([Map backingProps]) => '); + .write('${names.implName} ${factoryNames.first.implName}([Map backingProps]) => '); if (!isComponent2) { /// _$$FooProps _$Foo([Map backingProps]) => _$$FooProps(backingProps); @@ -266,7 +266,7 @@ class _LegacyTypedMapImplGenerator extends TypedMapImplGenerator { final TypedMapNames names; @override - final FactoryNames factoryNames; + final List factoryNames; @override final bool isProps; @@ -278,13 +278,13 @@ class _LegacyTypedMapImplGenerator extends TypedMapImplGenerator { _LegacyTypedMapImplGenerator.props(this.declaration) : names = TypedMapNames(declaration.props.name.name), - factoryNames = FactoryNames(declaration.factory.name.name), + factoryNames = [FactoryNames(declaration.factory.name.name)], member = declaration.props, isProps = true; _LegacyTypedMapImplGenerator.state(this.declaration) : names = TypedMapNames(declaration.state.name.name), - factoryNames = FactoryNames(declaration.factory.name.name), + factoryNames = [FactoryNames(declaration.factory.name.name)], member = declaration.state, isProps = false; @@ -329,7 +329,7 @@ class _TypedMapImplGenerator extends TypedMapImplGenerator { final TypedMapNames names; @override - final FactoryNames factoryNames; + final List factoryNames; @override final bool isProps; @@ -346,7 +346,7 @@ class _TypedMapImplGenerator extends TypedMapImplGenerator { _TypedMapImplGenerator.props(ClassComponentDeclaration declaration) : names = TypedMapNames(declaration.props.either.name.name), - factoryNames = FactoryNames(declaration.factory.name.name), + factoryNames = [FactoryNames(declaration.factory.name.name)], member = declaration.props.either, isProps = true, componentFactoryName = ComponentNames(declaration.component.name.name).componentFactoryName, @@ -355,7 +355,7 @@ class _TypedMapImplGenerator extends TypedMapImplGenerator { _TypedMapImplGenerator.state(ClassComponentDeclaration declaration) : names = TypedMapNames(declaration.state.either.name.name), - factoryNames = FactoryNames(declaration.factory.name.name), + factoryNames = [FactoryNames(declaration.factory.name.name)], member = declaration.state.either, isProps = false, componentFactoryName = ComponentNames(declaration.component.name.name).componentFactoryName, @@ -366,7 +366,8 @@ class _TypedMapImplGenerator extends TypedMapImplGenerator { PropsMapViewOrFunctionComponentDeclaration declaration) : names = TypedMapNames(declaration.props.either.name.name), assert(declaration.factories.length == 1), - factoryNames = FactoryNames(declaration.factories.first.name.name), + factoryNames = + declaration.factories.map((factory) => FactoryNames(factory.name.name)).toList(), member = declaration.props.either, isProps = true, componentFactoryName = 'null', @@ -376,20 +377,20 @@ class _TypedMapImplGenerator extends TypedMapImplGenerator { @override bool get isComponent2 => true; - String _generateFunctionComponentConfig() { + String _generateFunctionComponentConfig(FactoryNames factoryName) { return 'final FunctionComponentConfig<${names.implName}> ' - '${names.generatedMixinName}Config = FunctionComponentConfig(\n' + '\$${factoryName.consumerName}PropsConfig = FunctionComponentConfig(\n' 'propsFactory: PropsFactory(\n' 'map: (map) => ${names.implName}(map),\n' 'jsMap: (map) => ${names.jsMapImplName}(map),),\n' - // TODO: figure out how to connect this with factory - 'componentName: \'${factoryNames.consumerName}\');\n\n'; + 'componentName: \'${factoryName.consumerName}\');\n\n'; } @override void _generateFactory() { if (isFunctionComponentDeclaration) { - outputContentsBuffer.write(_generateFunctionComponentConfig()); + factoryNames.forEach( + (factory) => outputContentsBuffer.write(_generateFunctionComponentConfig(factory))); } else { super._generateFactory(); } From 685e9d7c778d875b4d9e96d8647f218122c715ea Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Tue, 14 Jul 2020 10:54:42 -0700 Subject: [PATCH 12/38] Add parsing for generic function components --- lib/src/builder/codegen.dart | 2 ++ lib/src/builder/parsing/declarations.dart | 24 +++++++++++++++++++ .../parsing/declarations_from_members.dart | 7 ++++++ .../builder/declaration_parsing_test.dart | 14 +++++++++++ 4 files changed, 47 insertions(+) diff --git a/lib/src/builder/codegen.dart b/lib/src/builder/codegen.dart index 6cf460e03..ec2884b8e 100644 --- a/lib/src/builder/codegen.dart +++ b/lib/src/builder/codegen.dart @@ -37,6 +37,8 @@ class ImplGenerator { case DeclarationType.propsMapViewOrFunctionComponentDeclaration: _generatePropsMapViewOrFunctionComponent(declaration); break; + case DeclarationType.genericFunctionComponentDeclaration: + break; case DeclarationType.classComponentDeclaration: _generateClassComponent(declaration); break; diff --git a/lib/src/builder/parsing/declarations.dart b/lib/src/builder/parsing/declarations.dart index 4545625ec..b35201d30 100644 --- a/lib/src/builder/parsing/declarations.dart +++ b/lib/src/builder/parsing/declarations.dart @@ -23,6 +23,7 @@ import 'version.dart'; /// The possible declaration types that the builder will look for. enum DeclarationType { + genericFunctionComponentDeclaration, propsMapViewOrFunctionComponentDeclaration, functionComponentDeclaration, classComponentDeclaration, @@ -301,6 +302,29 @@ class PropsMapViewOrFunctionComponentDeclaration extends BoilerplateDeclaration }) : super(Version.v4_mixinBased); } +/// A boilerplate declaration for a generic function component declared using +/// the new mixin-based boilerplate. +/// +/// This is similar to [PropsMapViewOrFunctionComponentDeclaration], but it does +/// not need a corresponding props mixin because it uses UiProps and no +/// code is generated. +/// +/// See [BoilerplateDeclaration] for more info. +class GenericFunctionComponentDeclaration extends BoilerplateDeclaration + with _TypedMapMixinShorthandDeclaration { + final BoilerplateFactory factory; + + @override + get _members => [factory]; + + @override + get type => DeclarationType.genericFunctionComponentDeclaration; + + GenericFunctionComponentDeclaration({ + @required this.factory, + }) : super(Version.v4_mixinBased); +} + /// Common interface for a boilerplate declaration for either a props or state mixin of any /// boilerplate version. /// diff --git a/lib/src/builder/parsing/declarations_from_members.dart b/lib/src/builder/parsing/declarations_from_members.dart index 28edae5fa..45c930815 100644 --- a/lib/src/builder/parsing/declarations_from_members.dart +++ b/lib/src/builder/parsing/declarations_from_members.dart @@ -302,6 +302,13 @@ Iterable getBoilerplateDeclarations( members.propsMixins, ].expand((i) => i).whereNot(hasBeenConsumed); + final genericFactories = _functionComponentFactories + .where((factory) => getPropsNameFromConfig(factory) == 'UiProps'); + for (final factory in genericFactories) { + consume(factory); + yield GenericFunctionComponentDeclaration(factory: factory); + } + for (final propsClassOrMixin in allUnusedProps) { final associatedFactories = _functionComponentFactories .where((factory) => getPropsNameFromConfig(factory) == propsClassOrMixin.name.name); diff --git a/test/vm_tests/builder/declaration_parsing_test.dart b/test/vm_tests/builder/declaration_parsing_test.dart index ce6eeeeac..3754e58a8 100644 --- a/test/vm_tests/builder/declaration_parsing_test.dart +++ b/test/vm_tests/builder/declaration_parsing_test.dart @@ -1623,6 +1623,20 @@ main() { expect(decl.version, Version.v4_mixinBased); }); + test('generic UiProps', () { + final leftHandType = hasLeftHandType ? 'UiFactory' : 'final'; + final typeParams = hasLeftHandType ? '' : ''; + setUpAndParse(''' + $leftHandType Foo = uiFunctionComponent$typeParams((props) { + return Dom.div()(); + }, null); + '''); + final decl = expectSingleOfType(declarations); + + expect(decl.factory.name.name, 'Foo'); + expect(decl.version, Version.v4_mixinBased); + }); + test('with multiple mixins in the same file', () { final leftHandType = hasLeftHandType ? 'UiFactory' : 'final'; final leftHandType2 = hasLeftHandType ? 'UiFactory' : 'final'; From 659692e6dffa320380449cd204046c6e324735cf Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Tue, 14 Jul 2020 11:58:29 -0700 Subject: [PATCH 13/38] Move defaultProps to react-dart --- .../function_component.dart | 61 ++++++++----------- 1 file changed, 25 insertions(+), 36 deletions(-) diff --git a/lib/src/component_declaration/function_component.dart b/lib/src/component_declaration/function_component.dart index 9849c3a24..4e48579cc 100644 --- a/lib/src/component_declaration/function_component.dart +++ b/lib/src/component_declaration/function_component.dart @@ -21,15 +21,12 @@ import 'package:over_react/over_react.dart'; import 'package:react/react.dart' as react; import 'package:react/react_client.dart'; import 'package:react/react_client/js_backed_map.dart'; -import 'package:react/react_client/react_interop.dart'; export 'component_type_checking.dart' show isComponentOfType, isValidElementOfType; UiFactory uiFunctionComponent( dynamic Function(T props) functionComponent, - // FIXME allow passing in displayName for generic function components - FunctionComponentConfig config, - { + FunctionComponentConfig config, { PropsFactory propsFactory, String displayName, void Function(UiFunctionComponentStatics) initStatics, @@ -48,19 +45,7 @@ UiFactory uiFunctionComponent( return functionComponent(propsFactory.jsMap(props as JsBackedMap)); } - /// FIXME DartFunctionComponent should be JsBackedMap? - final factory = react.registerFunctionComponent(_uiFunctionComponentWrapper, - displayName: displayName); - - if (propsFactory == null) { - // todo allow passing in of custom uiFactory/typedPropsFactory - // TODO make it easier to pass in parts of generatedInfo - if (T != UiProps && T != GenericUiProps) { - throw ArgumentError('config.propsFactory must be provided when using custom props classes'); - } - propsFactory = PropsFactory.fromUiFactory(([backingMap]) => GenericUiProps(factory, backingMap)) as PropsFactory; - } - + ReactDartFunctionComponentFactoryProxy factory; if (initStatics != null) { final statics = UiFunctionComponentStatics._( newProps: () => propsFactory.jsMap(JsBackedMap()), @@ -69,11 +54,26 @@ UiFactory uiFunctionComponent( initStatics(statics); if (statics.defaultProps != null) { - // fixme need to move to react-dart - (factory.reactFunction as ReactClass).defaultProps = JsBackedMap.from(statics.defaultProps).jsObject; + factory = react.registerFunctionComponent(_uiFunctionComponentWrapper, + displayName: displayName, defaultProps: statics.defaultProps); } // fixme need to implement in react-dart -// if (statics.propTypes != null) {} + if (statics.propTypes != null) { + + } + } else { + // FIXME DartFunctionComponent should be JsBackedMap? + factory = react.registerFunctionComponent(_uiFunctionComponentWrapper, + displayName: displayName); + } + + if (propsFactory == null) { + // todo allow passing in of custom uiFactory/typedPropsFactory + // TODO make it easier to pass in parts of generatedInfo + if (T != UiProps && T != GenericUiProps) { + throw ArgumentError('config.propsFactory must be provided when using custom props classes'); + } + propsFactory = PropsFactory.fromUiFactory(([backingMap]) => GenericUiProps(factory, backingMap)) as PropsFactory; } T _uiFactory([Map backingMap]) { @@ -91,19 +91,13 @@ UiFactory uiFunctionComponent( return _uiFactory; } -String getFunctionName(Function f) { - if (f == null) throw ArgumentError.notNull('f'); - - // DDC - // todo +String getFunctionName(Function function) { + if (function == null) throw ArgumentError.notNull('f'); - // Dart2js - final constructor = getProperty(f, 'constructor'); - if (constructor != null) { - return getProperty(constructor, 'name'); - } + final functionName = getProperty(function, 'name'); + if(functionName.toString().isNotEmpty && functionName != null) return functionName; - return null; + return 'UiFunctionComponent'; } class UiFunctionComponentStatics { @@ -114,11 +108,6 @@ class UiFunctionComponentStatics { final T Function() newProps; UiFunctionComponentStatics._({this.keyFor, this.newProps}); -// -// T newProps() => this._newProps(); -// -// String keyFor(void Function(T) accessProps) => this._keyFor(accessProps); - } class GenericUiProps extends UiProps { From 1d614a9bbdb7144a5cbcb57526dfebd8369d0789 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Tue, 14 Jul 2020 12:07:31 -0700 Subject: [PATCH 14/38] Update pubspec dependency override --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 44af2d79c..dbae20e43 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,4 +51,4 @@ dependency_overrides: react: git: url: https://github.com/cleandart/react-dart.git - ref: 5.4.0-wip + ref: function-component-updates From f57ea6c4fbaa2991db288c3ed5ff4b365f99793c Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Tue, 14 Jul 2020 16:48:11 -0700 Subject: [PATCH 15/38] Add codegen tests --- .../codegen/typed_map_impl_generator.dart | 1 - test/vm_tests/builder/codegen_test.dart | 72 +++++++++++++++++++ .../builder/declaration_parsing_test.dart | 44 +++++++++++- 3 files changed, 114 insertions(+), 3 deletions(-) diff --git a/lib/src/builder/codegen/typed_map_impl_generator.dart b/lib/src/builder/codegen/typed_map_impl_generator.dart index 4ac84987e..4353b1b7a 100644 --- a/lib/src/builder/codegen/typed_map_impl_generator.dart +++ b/lib/src/builder/codegen/typed_map_impl_generator.dart @@ -365,7 +365,6 @@ class _TypedMapImplGenerator extends TypedMapImplGenerator { _TypedMapImplGenerator.propsMapViewOrFunctionComponent( PropsMapViewOrFunctionComponentDeclaration declaration) : names = TypedMapNames(declaration.props.either.name.name), - assert(declaration.factories.length == 1), factoryNames = declaration.factories.map((factory) => FactoryNames(factory.name.name)).toList(), member = declaration.props.either, diff --git a/test/vm_tests/builder/codegen_test.dart b/test/vm_tests/builder/codegen_test.dart index 29f9533a9..26f093596 100644 --- a/test/vm_tests/builder/codegen_test.dart +++ b/test/vm_tests/builder/codegen_test.dart @@ -655,6 +655,78 @@ main() { testStaticMetaField('abstract props', OverReactSrc.abstractProps()); testStaticMetaField('abstract state', OverReactSrc.abstractState()); }); + + group('and generates props config for function components', () { + String generatedPropsConfig(String propsName, String factoryName) { + return 'final FunctionComponentConfig<_\$\$$propsName> ' + '\$${factoryName}PropsConfig = FunctionComponentConfig(\n' + 'propsFactory: PropsFactory(\n' + 'map: (map) => _\$\$$propsName(map),\n' + 'jsMap: (map) => _\$\$$propsName\$JsMap(map),),\n' + 'componentName: \'$factoryName\');\n'; + } + + String generatedPropsMapsForConfig(String propsName) { + return '// Concrete props implementation that can be backed by any [Map].\n' + '@Deprecated(\'This API is for use only within generated code.\'\' Do not reference it in your code, as it may change at any time.\')\n' + 'class _\$\$$propsName\$PlainMap extends _\$\$$propsName {\n' + ' // This initializer of `_props` to an empty map, as well as the reassignment\n' + ' // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217\n' + ' _\$\$$propsName\$PlainMap(Map backingMap) : this._props = {}, super._() {\n' + ' this._props = backingMap ?? {};\n' + ' }\n' + ' /// The backing props map proxied by this class.\n' + ' @override\n' + ' Map get props => _props;\n' + ' Map _props;\n' + '}\n' + '// Concrete props implementation that can only be backed by [JsMap],\n' + '// allowing dart2js to compile more optimal code for key-value pair reads/writes.\n' + '@Deprecated(\'This API is for use only within generated code.\'\' Do not reference it in your code, as it may change at any time.\')\n' + 'class _\$\$$propsName\$JsMap extends _\$\$$propsName {\n' + ' // This initializer of `_props` to an empty map, as well as the reassignment\n' + ' // of `_props` in the constructor body is necessary to work around a DDC bug: https://github.com/dart-lang/sdk/issues/36217\n' + ' _\$\$$propsName\$JsMap(JsBackedMap backingMap) : this._props = JsBackedMap(), super._() {\n' + ' this._props = backingMap ?? JsBackedMap();\n' + ' }\n' + ' /// The backing props map proxied by this class.\n' + ' @override\n' + ' JsBackedMap get props => _props;\n' + ' JsBackedMap _props;\n' + '}\n'; + } + + test('with multiple props mixins and function components in file', () { + setUpAndGenerate(r''' + mixin FooPropsMixin on UiProps {} + class FooProps = UiProps with FooPropsMixin; + + mixin BarPropsMixin on UiProps {} + + final Bar = uiFunctionComponent((props) { + return Dom.div()(); + }, $BarPropsMixinConfig); + + final Foo = uiFunctionComponent((props) { + return Dom.div()(); + }, $FooPropsConfig); + + final Baz = uiFunctionComponent((props) { + return Dom.div()(); + }, $BazPropsConfig); + + mixin UnusedPropsMixin on UiProps {} + '''); + + expect(implGenerator.outputContentsBuffer.toString().contains(generatedPropsMapsForConfig('UnusedPropsMixin')), isFalse); + expect(implGenerator.outputContentsBuffer.toString(), contains(generatedPropsMapsForConfig('BarPropsMixin'))); + expect(implGenerator.outputContentsBuffer.toString(), contains(generatedPropsMapsForConfig('FooProps'))); + + expect(implGenerator.outputContentsBuffer.toString(), contains(generatedPropsConfig('BarPropsMixin', 'Bar'))); + expect(implGenerator.outputContentsBuffer.toString(), contains(generatedPropsConfig('BarPropsMixin', 'Foo'))); + expect(implGenerator.outputContentsBuffer.toString(), contains(generatedPropsConfig('FooProps', 'Baz'))); + }); + }); }); group('logs an error when', () { diff --git a/test/vm_tests/builder/declaration_parsing_test.dart b/test/vm_tests/builder/declaration_parsing_test.dart index 3754e58a8..72b130af9 100644 --- a/test/vm_tests/builder/declaration_parsing_test.dart +++ b/test/vm_tests/builder/declaration_parsing_test.dart @@ -1612,9 +1612,15 @@ main() { $leftHandType Foo = uiFunctionComponent$typeParams((props) { return Dom.div()(); }, \$FooPropsConfig); + mixin FooPropsMixin on UiProps {} class FooProps = UiProps with FooPropsMixin; '''); - final decl = expectSingleOfType(declarations); + + expect(declarations, unorderedEquals([ + isA(), + isA(), + ])); + final decl = declarations.firstWhereType(); expect(decl.factories, isNotNull); expect(decl.factories.length, 1); @@ -1834,7 +1840,7 @@ main() { test('a factory or a component class', () { setUpAndParse(propsSrc); - verify(logger.severe(contains(errorPropsClassOnly)));; + verify(logger.severe(contains(errorPropsClassOnly))); }); test('a component or props class', () { @@ -1843,6 +1849,40 @@ main() { }); }); + group('a function component is declared', () { + test('without a props mixin', () { + setUpAndParse(r''' + final Foo = uiFunctionComponent((props) { + return Dom.div()(); + }, $FooPropsConfig); + '''); + verify(logger.severe(contains(errorFactoryOnly))); + }); + + test('without a matching props mixin', () { + setUpAndParse(r''' + mixin FooPropsMixin on UiProps {} + final Foo = uiFunctionComponent((props) { + return Dom.div()(); + }, $FooPropsConfig); + '''); + verify(logger.severe(contains(errorFactoryOnly))); + }); + + test('with a props mixin that is used by a component class', () { + setUpAndParse(r''' + UiFactory Foo = _$Foo; + mixin FooProps on UiProps {} + class FooComponent extends UiComponent2 {} + + final Foo = uiFunctionComponent((props) { + return Dom.div()(); + }, $FooPropsConfig); + '''); + verify(logger.severe(contains(errorFactoryOnly))); + }); + }); + group('a state class is declared without', () { test('any component pieces', () { setUpAndParse(stateSrc); From 68db3d4384c5a2aeebf1a1e70d3aa9e76aa9d53e Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Thu, 16 Jul 2020 09:56:15 -0700 Subject: [PATCH 16/38] Handle parsing null config and custom props factory --- .../parsing/declarations_from_members.dart | 8 ++--- .../builder/parsing/member_association.dart | 16 +++++++--- .../function_component.dart | 1 - test/vm_tests/builder/codegen_test.dart | 26 +++++++++++++++- .../builder/declaration_parsing_test.dart | 31 +++++++++++++------ 5 files changed, 63 insertions(+), 19 deletions(-) diff --git a/lib/src/builder/parsing/declarations_from_members.dart b/lib/src/builder/parsing/declarations_from_members.dart index 45c930815..0699dda93 100644 --- a/lib/src/builder/parsing/declarations_from_members.dart +++ b/lib/src/builder/parsing/declarations_from_members.dart @@ -303,7 +303,7 @@ Iterable getBoilerplateDeclarations( ].expand((i) => i).whereNot(hasBeenConsumed); final genericFactories = _functionComponentFactories - .where((factory) => getPropsNameFromConfig(factory) == 'UiProps'); + .where((factory) => getPropsNameFromFunctionComponent(factory) == 'UiProps' || !hasConfigArg(factory)); for (final factory in genericFactories) { consume(factory); yield GenericFunctionComponentDeclaration(factory: factory); @@ -311,14 +311,14 @@ Iterable getBoilerplateDeclarations( for (final propsClassOrMixin in allUnusedProps) { final associatedFactories = _functionComponentFactories - .where((factory) => getPropsNameFromConfig(factory) == propsClassOrMixin.name.name); + .where((factory) => !hasBeenConsumed(factory) && getPropsNameFromFunctionComponent(factory) == propsClassOrMixin.name.name); if (associatedFactories.isNotEmpty) { - associatedFactories.forEach(consume); - consume(propsClassOrMixin); yield PropsMapViewOrFunctionComponentDeclaration( factories: associatedFactories.toList(), props: getUnion(propsClassOrMixin), ); + associatedFactories.forEach(consume); + consume(propsClassOrMixin); } } } diff --git a/lib/src/builder/parsing/member_association.dart b/lib/src/builder/parsing/member_association.dart index 7c384502a..d0cce89ac 100644 --- a/lib/src/builder/parsing/member_association.dart +++ b/lib/src/builder/parsing/member_association.dart @@ -126,14 +126,22 @@ Union getPropsFor( } /// ADD DOC COMMENT IF KEPT -String getPropsNameFromConfig(BoilerplateFactory factory) { +String getPropsNameFromFunctionComponent(BoilerplateFactory factory) { if (factory.propsGenericArg != null) { return factory.propsGenericArg.typeNameWithoutPrefix; } final rightHandSide = factory.node.variables.firstInitializer; - if (rightHandSide == null || rightHandSide is! MethodInvocation) return null; - final args = (rightHandSide as MethodInvocation).typeArguments.arguments.firstOrNull; - return args?.typeNameWithoutPrefix; + assert(rightHandSide != null && rightHandSide is MethodInvocation); + final typeArgs = (rightHandSide as MethodInvocation).typeArguments?.arguments?.firstOrNull; + return typeArgs?.typeNameWithoutPrefix; +} + +/// ADD DOC COMMENT IF KEPT +bool hasConfigArg(BoilerplateFactory factory) { + final rightHandSide = factory.node.variables.firstInitializer; + final args = (rightHandSide as MethodInvocation).argumentList.arguments; + if(args == null || args.length < 2) return false; + return args[1] is! NullLiteral; } /// Retrieves the props for a given [member] if it is found in [states] or [stateMixins]. diff --git a/lib/src/component_declaration/function_component.dart b/lib/src/component_declaration/function_component.dart index 4e48579cc..5ec4bc124 100644 --- a/lib/src/component_declaration/function_component.dart +++ b/lib/src/component_declaration/function_component.dart @@ -128,7 +128,6 @@ class GenericUiProps extends UiProps { typedef FunctionFactoryFactory = UiFactory Function(ReactDartFunctionComponentFactoryProxy); - @protected class FunctionComponentConfig { @protected diff --git a/test/vm_tests/builder/codegen_test.dart b/test/vm_tests/builder/codegen_test.dart index 26f093596..670681c12 100644 --- a/test/vm_tests/builder/codegen_test.dart +++ b/test/vm_tests/builder/codegen_test.dart @@ -711,7 +711,7 @@ main() { return Dom.div()(); }, $FooPropsConfig); - final Baz = uiFunctionComponent((props) { + UiFactory Baz = uiFunctionComponent((props) { return Dom.div()(); }, $BazPropsConfig); @@ -726,6 +726,30 @@ main() { expect(implGenerator.outputContentsBuffer.toString(), contains(generatedPropsConfig('BarPropsMixin', 'Foo'))); expect(implGenerator.outputContentsBuffer.toString(), contains(generatedPropsConfig('FooProps', 'Baz'))); }); + + test('unless function component is generic or does not have a props config', () { + setUpAndGenerate(r''' + mixin FooPropsMixin on UiProps {} + + final Bar = uiFunctionComponent((props) { + return Dom.div()(); + }, null); + + final Foo = uiFunctionComponent((props) { + return Dom.div()(); + }, $FooPropsConfig); + + final Baz = uiFunctionComponent((props) { + return Dom.div()(); + }, null, propsFactory: PropsFactory.fromUiFactory(Foo)); + '''); + + expect(implGenerator.outputContentsBuffer.toString(), contains(generatedPropsMapsForConfig('FooPropsMixin'))); + + expect(implGenerator.outputContentsBuffer.toString().contains(generatedPropsConfig('UiProps', 'Bar')), isFalse, reason: '2'); + expect(implGenerator.outputContentsBuffer.toString(), contains(generatedPropsConfig('FooPropsMixin', 'Foo')), reason: '1'); + expect(implGenerator.outputContentsBuffer.toString().contains(generatedPropsConfig('FooPropsMixin', 'Baz')), isFalse, reason: '3'); + }); }); }); diff --git a/test/vm_tests/builder/declaration_parsing_test.dart b/test/vm_tests/builder/declaration_parsing_test.dart index 72b130af9..ed722065e 100644 --- a/test/vm_tests/builder/declaration_parsing_test.dart +++ b/test/vm_tests/builder/declaration_parsing_test.dart @@ -1575,15 +1575,15 @@ main() { setUpAndParse(''' $leftHandType Foo = uiFunctionComponent$typeParams((props) { return Dom.div()(); - }, \$FooPropsMixinConfig); + }, \$FooPropsConfig); $leftHandType Bar = uiFunctionComponent$typeParams((props) { return Dom.div()(); - }, \$FooPropsMixinConfig); + }, \$BarPropsConfig); $leftHandType Baz = uiFunctionComponent$typeParams((props) { return Dom.div()(); - }, \$FooPropsMixinConfig); + }, null, propsFactory: PropsFactory.fromUiFactory(Foo)); mixin FooPropsMixin on UiProps {} '''); @@ -1591,18 +1591,22 @@ main() { expect(declarations, unorderedEquals([ isA(), isA(), + isA(), ])); final decl = declarations.firstWhereType(); + final genericDecl = declarations.firstWhereType(); expect(decl.factories, isNotNull); - expect(decl.factories.length, 3); + expect(decl.factories.length, 2); expect(decl.factories.map((factory) => factory.name.name), unorderedEquals([ 'Foo', 'Bar', - 'Baz', ])); expect(decl.props.b?.name?.name, 'FooPropsMixin'); expect(decl.version, Version.v4_mixinBased); + + expect(genericDecl.factory.name.name, 'Baz'); + expect(genericDecl.version, Version.v4_mixinBased); }); test('(verbose)', () { @@ -1651,15 +1655,15 @@ main() { setUpAndParse(''' $leftHandType Foo = uiFunctionComponent$typeParams((props) { return Dom.div()(); - }, \$FooPropsMixinConfig); + }, \$FooPropsConfig); $leftHandType Bar = uiFunctionComponent$typeParams((props) { return Dom.div()(); - }, \$FooPropsMixinConfig); + }, \$BarPropsConfig); $leftHandType2 Baz = uiFunctionComponent$typeParams2((props) { return Dom.div()(); - }, \$BarPropsMixinConfig); + }, \$BazPropsConfig); mixin FooPropsMixin on UiProps {} mixin BarPropsMixin on UiProps {} @@ -1859,10 +1863,19 @@ main() { verify(logger.severe(contains(errorFactoryOnly))); }); + test('without props typing arguments or left hand typing', () { + setUpAndParse(r''' + final Foo = uiFunctionComponent((props) { + return Dom.div()(); + }, $FooPropsConfig); + '''); + verify(logger.severe(contains(errorFactoryOnly))); + }); + test('without a matching props mixin', () { setUpAndParse(r''' mixin FooPropsMixin on UiProps {} - final Foo = uiFunctionComponent((props) { + UiFactory Foo = uiFunctionComponent((props) { return Dom.div()(); }, $FooPropsConfig); '''); From d9d0708a55897df914c16f537b275b9978be7ed4 Mon Sep 17 00:00:00 2001 From: Sydney Jodon Date: Thu, 16 Jul 2020 14:29:22 -0700 Subject: [PATCH 17/38] Add function component snippets --- snippets/README.md | 2 ++ snippets/snippets.json | 41 +++++++++++++++++++++++++++++++++++++++++ snippets/snippets.xml | 14 ++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/snippets/README.md b/snippets/README.md index 8100a63eb..8193b9ece 100644 --- a/snippets/README.md +++ b/snippets/README.md @@ -6,8 +6,10 @@ Below is a list of all available over_react snippets and their Triggers for Webs | -------: | ------- | | `orStless` | Stateless component skeleton with abbreviated props declaration | | `orStful` | Stateful component skeleton with abbreviated props and state declarations | +| `orFunc` | Function component skeleton with abbreviated props declaration | | `orAdvStless` | Stateless component skeleton with a props class alias| | `orAdvStful` | Stateful component skeleton with props and state class aliases| +| `orAdvFunc` | Function component skeleton with props class alias| ## WebStorm and IntelliJ Snippets diff --git a/snippets/snippets.json b/snippets/snippets.json index 28e5f7fd4..0a72802b8 100644 --- a/snippets/snippets.json +++ b/snippets/snippets.json @@ -98,5 +98,46 @@ "}" ], "description": "Creates a stateful OverReact component with abbreviated props and state declarations" + }, + "abbreviatedFunctionComponent": { + "prefix": "orFunc", + "body": [ + "import 'package:over_react/over_react.dart';", + "", + "part '${1:FileName}.over_react.g.dart';", + "", + "UiFactory<${2:MyComponent}Props> ${2:MyComponent} = uiFunctionComponent(", + "\t(props) {},", + "\t$${2:MyComponent}PropsConfig,", + "\tinitStatics: (statics) {", + "\t\tstatics.defaultProps = (statics.newProps());", + "\t},", + ");", + "", + "mixin ${2:MyComponent}Props on UiProps {}" + ], + "description": "Creates an OverReact function component with an abbreviated props declaration" + }, + "functionComponent": { + "prefix": "orAdvFunc", + "body": [ + "import 'package:over_react/over_react.dart';", + "", + "part '${1:FileName}.over_react.g.dart';", + "", + "UiFactory<${2:MyComponent}Props> ${2:MyComponent} = uiFunctionComponent(", + "\t(props) {},", + "\t$${2:MyComponent}PropsConfig,", + "\tinitStatics: (statics) {", + "\t\tstatics.defaultProps = (statics.newProps());", + "\t},", + ");", + "", + "mixin ${2:MyComponent}PropsMixin on UiProps {}", + "", + "class ${2:MyComponent}Props = UiProps with ${2:MyComponent}PropsMixin;", + "" + ], + "description": "Creates an OverReact function component with a props class alias" } } diff --git a/snippets/snippets.xml b/snippets/snippets.xml index 82aff5d60..6d2377c63 100644 --- a/snippets/snippets.xml +++ b/snippets/snippets.xml @@ -42,3 +42,17 @@