Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CPLAT-5211 Add @Component2 annotation #289

Merged
merged 12 commits into from
May 2, 2019
Merged
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#nit Ooh we could make this generic (generics functions weren't supported when this method was authored):

Suggested change
static singleOrNull(List list) => list.isNotEmpty ? list.single : null;
static T singleOrNull<T>(List<T> 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
Loading