Skip to content

Commit

Permalink
Update builder to differentiate Component2 annotations
Browse files Browse the repository at this point in the history
+ Without the use of fully-resolved AST since we no longer have access to it for build perf reasons (see: #280)
  • Loading branch information
aaronlademann-wf committed May 1, 2019
1 parent 861fcdf commit 8bc90b1
Show file tree
Hide file tree
Showing 23 changed files with 355 additions and 150 deletions.
128 changes: 92 additions & 36 deletions lib/src/builder/generation/declaration_parsing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class ParsedDeclarations {
Map<String, List<CompilationUnitMember>> declarationMap = {
key_factory: <CompilationUnitMember>[],
key_component: <CompilationUnitMember>[],
key_component2: <CompilationUnitMember>[],
key_props: <CompilationUnitMember>[],
key_state: <CompilationUnitMember>[],
key_abstractComponent: <CompilationUnitMember>[],
Expand Down Expand Up @@ -226,27 +227,34 @@ class ParsedDeclarations {

[
key_component,
key_component2,
key_props,
key_state,
key_abstractComponent,
key_abstractComponent2,
key_abstractProps,
key_abstractState,
key_propsMixin,
key_stateMixin,
].forEach((annotationName) {
declarationMap[annotationName] = classesOnly(annotationName, declarationMap[annotationName]);
declarationMap[annotationName] = classesOnly(annotationName, declarationMap[annotationName] ?? const <CompilationUnitMember>[]);
});

// Validate that all the declarations that make up a component are used correctly.

Iterable<List<CompilationUnitMember>> requiredDecls =
key_allComponentRequired.map((annotationName) => declarationMap[annotationName]);

Iterable<List<CompilationUnitMember>> requiredDecls2 =
key_allComponent2Required.map((annotationName) => declarationMap[annotationName]);

Iterable<List<CompilationUnitMember>> optionalDecls =
key_allComponentOptional.map((annotationName) => declarationMap[annotationName]);

bool oneOfEachRequiredDecl = requiredDecls.every((decls) => decls.length == 1);
bool noneOfAnyRequiredDecl = requiredDecls.every((decls) => decls.length == 0);
bool oneOfEachRequiredDecl2 = requiredDecls2.every((decls) => decls.length == 1);
bool oneOfEachRequiredDecl = requiredDecls.every((decls) => decls.length == 1) || oneOfEachRequiredDecl2;
bool noneOfAnyRequiredDecl2 = requiredDecls2.every((decls) => decls.length == 0);
bool noneOfAnyRequiredDecl = requiredDecls.every((decls) => decls.length == 0) && noneOfAnyRequiredDecl2;

bool atMostOneOfEachOptionalDecl = optionalDecls.every((decls) => decls.length <= 1);
bool noneOfAnyOptionalDecl = optionalDecls.every((decls) => decls.length == 0);
Expand All @@ -258,9 +266,42 @@ class ParsedDeclarations {

// Give the consumer some useful errors if the declarations aren't valid.

void _emitDuplicateDeclarationError(String annotationName, int instanceNumber) {
final declarations = declarationMap[annotationName];
error(
'To define a component, there must be a single `@$annotationName` per file, '
'but ${declarations.length} were found. (${instanceNumber + 1} of ${declarations.length})',
getSpan(sourceFile, declarations[instanceNumber])
);
}

if (declarationMap[key_component].isNotEmpty && declarationMap[key_component2].isNotEmpty) {
error(
'To define a component, there must be a single `@$key_component` **OR** `@$key_component2` annotation, '
'but never both.'
);
}

if (!areDeclarationsValid) {
if (!noneOfAnyRequiredDecl) {
key_allComponentRequired.forEach((annotationName) {
if (declarationMap[key_component].isEmpty && declarationMap[key_component2].isEmpty) {
// Can't tell if they were trying to build a version 1 or version 2 component,
// so we'll tailor the error message accordingly.
error(
'To define a component, there must also be a `@$key_component` or `@$key_component2` within the same file, '
'but none were found.'
);
} else if (declarationMap[key_component].length > 1) {
for (int i = 0; i < declarationMap[key_component].length; i++) {
_emitDuplicateDeclarationError(key_component, i);
}
} else if (declarationMap[key_component2].length > 1) {
for (int i = 0; i < declarationMap[key_component2].length; i++) {
_emitDuplicateDeclarationError(key_component2, i);
}
}

key_allComponentVersionsRequired.forEach((annotationName) {
final declarations = declarationMap[annotationName];
if (declarations.length == 0) {
error(
Expand All @@ -269,11 +310,7 @@ class ParsedDeclarations {
);
} else if (declarations.length > 1) {
for (int i = 0; i < declarations.length; i++) {
error(
'To define a component, there must be a single `@$annotationName` per file, '
'but ${declarations.length} were found. (${i + 1} of ${declarations.length})',
getSpan(sourceFile, declarations[i])
);
_emitDuplicateDeclarationError(annotationName, i);
}
}
declarationMap[annotationName] = [];
Expand All @@ -297,7 +334,7 @@ class ParsedDeclarations {
error(
'To define a component, a `@$annotationName` must be accompanied by '
'the following annotations within the same file: '
'${key_allComponentRequired.map((key) => '@$key').join(', ')}.',
'(@$key_component || @$key_component2), ${key_allComponentVersionsRequired.map((key) => '@$key').join(', ')}.',
getSpan(sourceFile, declarations.first)
);
}
Expand Down Expand Up @@ -357,6 +394,7 @@ class ParsedDeclarations {
return new ParsedDeclarations._(
factory: singleOrNull(declarationMap[key_factory]),
component: singleOrNull(declarationMap[key_component]),
component2: singleOrNull(declarationMap[key_component2]),
props: singleOrNull(declarationMap[key_props]),
state: singleOrNull(declarationMap[key_state]),

Expand All @@ -377,7 +415,10 @@ class ParsedDeclarations {

ParsedDeclarations._({
TopLevelVariableDeclaration factory,
// TODO: Remove when `annotations.Component` is removed in the 4.0.0 release.
@Deprecated('4.0.0')
ClassDeclaration component,
ClassDeclaration component2,
ClassDeclaration props,
ClassDeclaration state,

Expand All @@ -395,10 +436,11 @@ class ParsedDeclarations {
bool hasStateCompanionClass,
bool hasAbstractStateCompanionClass,
}) :
this.factory = (factory == null) ? null : new FactoryNode(factory),
this.component = (component == null) ? null : new ComponentNode(component),
this.props = (props == null) ? null : new PropsNode(props, hasPropsCompanionClass),
this.state = (state == null) ? null : new StateNode(state, hasStateCompanionClass),
this.factory = (factory == null) ? null : new FactoryNode(factory),
this.component = (component == null) ? null : new ComponentNode(component),
this.component2 = (component2 == null) ? null : new Component2Node(component2),
this.props = (props == null) ? null : new PropsNode(props, hasPropsCompanionClass),
this.state = (state == null) ? null : new StateNode(state, hasStateCompanionClass),

this.abstractProps = new List.unmodifiable(abstractProps.map((props) => new AbstractPropsNode(props, hasAbstractPropsCompanionClass))),
this.abstractState = new List.unmodifiable(abstractState.map((state) => new AbstractStateNode(state, hasAbstractStateCompanionClass))),
Expand All @@ -409,33 +451,46 @@ class ParsedDeclarations {
this.declaresComponent = factory != null
{
assert(
((this.factory == null && this.component == null && this.props == null) ||
(this.factory != null && this.component != null && this.props != null)) &&
'`factory`, `component`, and `props` must be either all null or all non-null. '
((this.factory == null && ((this.component ?? this.component2) == null) && this.props == null) ||
(this.factory != null && ((this.component ?? this.component2) != null) && this.props != null)) &&
'`factory`, `component` / `component2`, and `props` must be either all null or all non-null. '
'Any other combination represents an invalid component declaration. ' is String
);
}



static final String key_factory = getName(annotations.Factory);
// TODO: Remove when `annotations.Component` is removed in the 4.0.0 release.
@Deprecated('4.0.0')
static final String key_component = getName(annotations.Component);
static final String key_component2 = getName(annotations.Component2);
static final String key_props = getName(annotations.Props);
static final String key_state = getName(annotations.State);

static final String key_abstractComponent = getName(annotations.AbstractComponent);
static final String key_abstractProps = getName(annotations.AbstractProps);
static final String key_abstractState = getName(annotations.AbstractState);
// TODO: Remove when `annotations.AbstractComponent` is removed in the 4.0.0 release.
@Deprecated('4.0.0')
static final String key_abstractComponent = getName(annotations.AbstractComponent);
static final String key_abstractComponent2 = getName(annotations.AbstractComponent2);
static final String key_abstractProps = getName(annotations.AbstractProps);
static final String key_abstractState = getName(annotations.AbstractState);

static final String key_propsMixin = getName(annotations.PropsMixin);
static final String key_stateMixin = getName(annotations.StateMixin);

static final List<String> key_allComponentRequired = new List.unmodifiable([
static final List<String> key_allComponentVersionsRequired = new List.unmodifiable([
key_factory,
key_component,
key_props,
]);

// TODO: Remove when the `@Component` annotation is removed in the 4.0.0 release.
@Deprecated('4.0.0')
static final List<String> key_allComponentRequired = new List.unmodifiable(
new List.from(key_allComponentVersionsRequired)..add(key_component));

static final List<String> key_allComponent2Required = new List.unmodifiable(
new List.from(key_allComponentVersionsRequired)..add(key_component2));

static final List<String> key_allComponentOptional = new List.unmodifiable([
key_state,
]);
Expand All @@ -445,9 +500,11 @@ class ParsedDeclarations {
[
key_factory,
key_component,
key_component2,
key_props,
key_state,
key_abstractComponent,
key_abstractComponent2,
key_abstractProps,
key_abstractState,
key_propsMixin,
Expand All @@ -462,7 +519,10 @@ class ParsedDeclarations {
}

final FactoryNode factory;
// TODO: Remove when `annotations.Component` is removed in the 4.0.0 release.
@Deprecated('4.0.0')
final ComponentNode component;
final Component2Node component2;
final PropsNode props;
final StateNode state;

Expand All @@ -477,29 +537,21 @@ class ParsedDeclarations {
final bool declaresComponent;

/// Helper function that returns the single value of a [list], or null if it is empty.
static dynamic singleOrNull(List list) => list.isNotEmpty ? list.single : null;
static singleOrNull(List list) => list.isNotEmpty ? list.single : null;
}

// Generic type aliases, for readability.

class ComponentNode extends NodeWithMeta<ClassDeclaration, annotations.Component> {
// TODO: Remove when `annotations.Component` is removed in the 4.0.0 release.
@Deprecated('4.0.0')
class ComponentNode<TMeta extends annotations.Component>
extends NodeWithMeta<ClassDeclaration, TMeta> {
static const String _subtypeOfParamName = 'subtypeOf';

/// Whether the component extends from Component2.
final bool isComponent2;

/// The value of the `subtypeOf` parameter passed in to this node's annotation.
Identifier subtypeOfValue;

ComponentNode(ClassDeclaration node)
: this.isComponent2 = node.declaredElement == null
// This can be null when using non-resolved AST in tests; FIXME 3.0.0-wip do we need to update that setup?
? false
// TODO 3.0.0-wip is there a better way to check against react's Component2?
: node.declaredElement.allSupertypes.any((type) {
return type.name == 'Component2';
}),
super(node) {
ComponentNode(AnnotatedNode unit) : super(unit) {
// Perform special handling for the `subtypeOf` parameter of this node's annotation.
//
// If valid, omit it from `unsupportedArguments` so that the `meta` can be accessed without it
Expand All @@ -520,6 +572,10 @@ class ComponentNode extends NodeWithMeta<ClassDeclaration, annotations.Component
}
}

class Component2Node extends ComponentNode<annotations.Component2> {
Component2Node(AnnotatedNode unit) : super(unit);
}

class FactoryNode extends NodeWithMeta<TopLevelVariableDeclaration, annotations.Factory> {FactoryNode(unit) : super(unit);}

class PropsOrStateNode<T> extends NodeWithMeta<ClassDeclaration, T> {
Expand Down
26 changes: 15 additions & 11 deletions lib/src/builder/generation/impl_generation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class ImplGenerator {

generate(ParsedDeclarations declarations) {
if (declarations.declaresComponent) {
final bool isComponent2 = declarations.component2 != null;
final componentDeclNode = isComponent2 ? declarations.component2 : declarations.component;

final factoryName = declarations.factory.node.variables.variables.first.name.toString();

final consumerPropsName = declarations.props.node.name.toString();
Expand All @@ -63,7 +66,7 @@ class ImplGenerator {
final propsImplName = _propsImplClassNameFromConsumerClassName(consumerPropsName);
final propsAccessorsMixinName = _accessorsMixinNameFromConsumerName(consumerPropsName);

final componentClassName = declarations.component.node.name.toString();
final componentClassName = componentDeclNode.node.name.toString();
final componentClassImplMixinName = '$privateSourcePrefix$componentClassName';

final generatedComponentFactoryName = _componentFactoryName(componentClassName);
Expand All @@ -78,7 +81,7 @@ class ImplGenerator {
String parentTypeParam = 'null';
String parentTypeParamComment = '';

Identifier parentType = declarations.component.subtypeOfValue;
Identifier parentType = componentDeclNode.subtypeOfValue;
if (parentType != null) {
parentTypeParamComment = ' /* from `subtypeOf: ${getSpan(sourceFile, parentType).text}` */';

Expand All @@ -99,7 +102,7 @@ class ImplGenerator {
/// if a component's factory variable tries to reference itself during its initialization.
/// Therefore, this is not allowed.
logger.severe(messageWithSpan('A component cannot be a subtype of itself.',
span: getSpan(sourceFile, declarations.component.metaNode))
span: getSpan(sourceFile, componentDeclNode.metaNode))
);
}

Expand All @@ -110,7 +113,7 @@ class ImplGenerator {
..writeln('final $generatedComponentFactoryName = registerComponent(() => new $componentClassImplMixinName(),')
..writeln(' builderFactory: $factoryName,')
..writeln(' componentClass: $componentClassName,')
..writeln(' isWrapper: ${declarations.component.meta.isWrapper},')
..writeln(' isWrapper: ${componentDeclNode.meta.isWrapper},')
..writeln(' parentType: $parentTypeParam,$parentTypeParamComment')
..writeln(' displayName: ${stringLiteral(factoryName)}')
..writeln(');')
Expand All @@ -131,7 +134,8 @@ class ImplGenerator {

outputContentsBuffer.write(
'$propsImplName $privateSourcePrefix$factoryName([Map backingProps]) => ');
if (!declarations.component.isComponent2) {

if (!isComponent2) {
/// _$$FooProps _$Foo([Map backingProps]) => new _$$FooProps(backingProps);
outputContentsBuffer.writeln('new $propsImplName(backingProps);');
} else {
Expand All @@ -152,10 +156,10 @@ class ImplGenerator {
node: declarations.props,
accessorsMixinName: propsAccessorsMixinName,
consumableName: consumablePropsName,
isComponent2: declarations.component.isComponent2,
isComponent2: isComponent2,
));

if (declarations.component.isComponent2) {
if (isComponent2) {
final jsMapImplName = _jsMapAccessorImplClassNameFromImplClassName(propsImplName);
// This implementation here is necessary so that mixin accesses aren't compiled as index$ax
typedPropsFactoryImpl
Expand Down Expand Up @@ -206,10 +210,10 @@ class ImplGenerator {
node: declarations.state,
accessorsMixinName: stateAccessorsMixinName,
consumableName: consumableStateName,
isComponent2: declarations.component.isComponent2,
isComponent2: isComponent2,
));

if (declarations.component.isComponent2) {
if (isComponent2) {
final jsMapImplName = _jsMapAccessorImplClassNameFromImplClassName(stateImplName);
// This implementation here is necessary so that mixin accesses aren't compiled as index$ax
typedStateFactoryImpl
Expand Down Expand Up @@ -256,7 +260,7 @@ class ImplGenerator {
'const [${_metaConstantName(consumablePropsName)}];')
..writeln('}');

final implementsTypedPropsStateFactory = declarations.component.node.members.any((member) =>
final implementsTypedPropsStateFactory = componentDeclNode.node.members.any((member) =>
member is MethodDeclaration &&
!member.isStatic &&
(member.name.name == 'typedPropsFactory' || member.name.name == 'typedStateFactory')
Expand All @@ -266,7 +270,7 @@ class ImplGenerator {
// Can't be an error, because consumers may be implementing typedPropsFactory or typedStateFactory in their components.
logger.warning(messageWithSpan(
'Components should not add their own implementions of typedPropsFactory or typedStateFactory.',
span: getSpan(sourceFile, declarations.component.node))
span: getSpan(sourceFile, componentDeclNode.node))
);
}
}
Expand Down
4 changes: 4 additions & 0 deletions lib/src/component_declaration/annotations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ class State implements TypedMap {
/// Must be accompanied by a [Factory] and [Props] declaration.
///
/// __Deprecated.__ Use the [Component2] annotation alongside `UiComponent2` / `UiStatefulComponent2` instead.
///
/// TODO: 3.0.0-wip is it possible to ensure that this annotation is not used on a "V2" class instance at build time?
@Deprecated('4.0.0')
class Component {
/// Whether the component clones or passes through its children and needs to be
Expand Down Expand Up @@ -115,6 +117,8 @@ class Component {
/// }
///
/// Must be accompanied by a [Factory] and [Props] declaration.
///
/// TODO: 3.0.0-wip is it possible to ensure that this annotation is not used on a "V1" class instance at build time?
class Component2 implements Component {
/// Whether the component clones or passes through its children and needs to be
/// treated as if it were the wrapped component when passed in to `isComponentOfType`.
Expand Down
Loading

0 comments on commit 8bc90b1

Please sign in to comment.