From 83e81a37e2f66b0fd158aa318d820a05530c5a77 Mon Sep 17 00:00:00 2001 From: Fred Emmott Date: Wed, 12 Feb 2020 13:38:33 -0800 Subject: [PATCH] RFC/Proof-of-concept: Implement children declarations as a trait (#214) * RFC: Implement children declarations as a trait Currently serializes to the same format as HackC, to allow migrating to the new syntax with no changes to validation. Some normalization is needed, as HackC emits some overly-complex structures - e.g. with `$expr = tuple(EXACTLY_ONE, EXPRESSION, $inner_expr)` can be simplified to `$expr = $inner_expr`. It is not currently in a form suitable for use - the trait I've added merely confirms that the two forms of children declaration are semanticaly equivalent. I think that's a meaningful first step; if we're happy for this, we can merge it, start on migration tools, then add an alternative trait to allow removal of the legacy syntax. This is very much a draft API; open to a complete rewrite if desired :) refs #212 * make child declaration a static method * rename trait for consistency, add new-way-only trait too * remove bogus child declaration --- src/children/Any.hack | 20 ++ src/children/AnyNumberOf.hack | 15 ++ src/children/AnyOf.hack | 41 ++++ src/children/AtLeastOneOf.hack | 15 ++ src/children/Category.hack | 27 +++ src/children/Constraint.hack | 15 ++ src/children/LeafConstraint.hack | 21 ++ src/children/LegacyConstraintType.hack | 18 ++ src/children/LegacyExpression.hack | 14 ++ src/children/LegacyExpressionType.hack | 19 ++ src/children/None.hack | 20 ++ src/children/OfType.hack | 19 ++ src/children/Optional.hack | 15 ++ src/children/PCData.hack | 16 ++ src/children/QuantifierConstraint.hack | 36 ++++ src/children/Sequence.hack | 42 ++++ ...ChildDeclarationConsistencyValidation.hack | 49 +++++ src/children/XHPChildValidation.hack | 35 ++++ src/children/functions.hack | 53 +++++ src/core/ComposableElement.php | 16 +- tests/ChildRuleTest.php | 181 +++++++++++++++++- 21 files changed, 682 insertions(+), 5 deletions(-) create mode 100644 src/children/Any.hack create mode 100644 src/children/AnyNumberOf.hack create mode 100644 src/children/AnyOf.hack create mode 100644 src/children/AtLeastOneOf.hack create mode 100644 src/children/Category.hack create mode 100644 src/children/Constraint.hack create mode 100644 src/children/LeafConstraint.hack create mode 100644 src/children/LegacyConstraintType.hack create mode 100644 src/children/LegacyExpression.hack create mode 100644 src/children/LegacyExpressionType.hack create mode 100644 src/children/None.hack create mode 100644 src/children/OfType.hack create mode 100644 src/children/Optional.hack create mode 100644 src/children/PCData.hack create mode 100644 src/children/QuantifierConstraint.hack create mode 100644 src/children/Sequence.hack create mode 100644 src/children/XHPChildDeclarationConsistencyValidation.hack create mode 100644 src/children/XHPChildValidation.hack create mode 100644 src/children/functions.hack diff --git a/src/children/Any.hack b/src/children/Any.hack new file mode 100644 index 00000000..dd06c68a --- /dev/null +++ b/src/children/Any.hack @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +final class Any implements Constraint { + public function legacySerialize(): mixed { + return 1; + } + + public function legacySerializeAsLeaf(): (LegacyConstraintType, mixed) { + return tuple(LegacyConstraintType::ANY, null); + } +} diff --git a/src/children/AnyNumberOf.hack b/src/children/AnyNumberOf.hack new file mode 100644 index 00000000..368ab982 --- /dev/null +++ b/src/children/AnyNumberOf.hack @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +final class AnyNumberOf extends QuantifierConstraint { + const LegacyExpressionType LEGACY_EXPRESSION_TYPE = + LegacyExpressionType::ANY_QUANTITY; +} diff --git a/src/children/AnyOf.hack b/src/children/AnyOf.hack new file mode 100644 index 00000000..b45e5847 --- /dev/null +++ b/src/children/AnyOf.hack @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +use namespace HH\Lib\{C, Vec}; + +final class AnyOf implements LegacyExpression { + private vec $children; + public function __construct(T $a, T $b, T ...$rest) { + $this->children = Vec\concat(vec[$a, $b], $rest); + } + + public function legacySerialize(): (LegacyExpressionType, mixed, mixed) { + $it = tuple( + LegacyExpressionType::EITHER, + $this->children[0]->legacySerialize(), + $this->children[1]->legacySerialize(), + ); + $rest = Vec\drop($this->children, 2); + while (!C\is_empty($rest)) { + $it = tuple( + LegacyExpressionType::EITHER, + $it, + $rest[0]->legacySerialize(), + ); + $rest = Vec\drop($rest, 1); + } + return $it; + } + + final public function legacySerializeAsLeaf(): null { + return null; + } +} diff --git a/src/children/AtLeastOneOf.hack b/src/children/AtLeastOneOf.hack new file mode 100644 index 00000000..0e105ed0 --- /dev/null +++ b/src/children/AtLeastOneOf.hack @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +final class AtLeastOneOf extends QuantifierConstraint { + const LegacyExpressionType LEGACY_EXPRESSION_TYPE = + LegacyExpressionType::AT_LEAST_ONE; +} diff --git a/src/children/Category.hack b/src/children/Category.hack new file mode 100644 index 00000000..2c18d05a --- /dev/null +++ b/src/children/Category.hack @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +use namespace HH\Lib\Str; + +final class Category extends LeafConstraint { + public function __construct(private string $category) { + } + + public function legacySerializeAsLeaf(): (LegacyConstraintType, string) { + return tuple( + LegacyConstraintType::CATEGORY, + $this->category + |> Str\strip_prefix($$, '%') + |> Str\replace($$, ':', '__') + |> Str\replace($$, '-', '_'), + ); + } +} diff --git a/src/children/Constraint.hack b/src/children/Constraint.hack new file mode 100644 index 00000000..193f2bdd --- /dev/null +++ b/src/children/Constraint.hack @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +interface Constraint { + public function legacySerialize(): mixed; + public function legacySerializeAsLeaf(): ?(LegacyConstraintType, mixed); +} diff --git a/src/children/LeafConstraint.hack b/src/children/LeafConstraint.hack new file mode 100644 index 00000000..5fdf8385 --- /dev/null +++ b/src/children/LeafConstraint.hack @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +abstract class LeafConstraint implements LegacyExpression { + abstract public function legacySerializeAsLeaf( + ): (LegacyConstraintType, mixed); + + final public function legacySerialize( + ): (LegacyExpressionType, LegacyConstraintType, mixed) { + $as_leaf = $this->legacySerializeAsLeaf(); + return tuple(LegacyExpressionType::EXACTLY_ONE, $as_leaf[0], $as_leaf[1]); + } +} diff --git a/src/children/LegacyConstraintType.hack b/src/children/LegacyConstraintType.hack new file mode 100644 index 00000000..2d20899f --- /dev/null +++ b/src/children/LegacyConstraintType.hack @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +enum LegacyConstraintType: int { + ANY = 1; + PCDATA = 2; + CLASSNAME = 3; + CATEGORY = 4; + EXPRESSION = 5; +} diff --git a/src/children/LegacyExpression.hack b/src/children/LegacyExpression.hack new file mode 100644 index 00000000..05b98dd6 --- /dev/null +++ b/src/children/LegacyExpression.hack @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +interface LegacyExpression extends Constraint { + public function legacySerialize(): (LegacyExpressionType, mixed, mixed); +} diff --git a/src/children/LegacyExpressionType.hack b/src/children/LegacyExpressionType.hack new file mode 100644 index 00000000..284e8c15 --- /dev/null +++ b/src/children/LegacyExpressionType.hack @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +enum LegacyExpressionType: int { + EXACTLY_ONE = 0; + ANY_QUANTITY = 1; + ZERO_OR_ONE = 2; + AT_LEAST_ONE = 3; + SEQUENCE = 4; + EITHER = 5; +} diff --git a/src/children/None.hack b/src/children/None.hack new file mode 100644 index 00000000..5277d1d7 --- /dev/null +++ b/src/children/None.hack @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +final class None implements Constraint { + public function legacySerialize(): mixed { + return 0; + } + + public function legacySerializeAsLeaf(): null { + return null; + } +} diff --git a/src/children/OfType.hack b/src/children/OfType.hack new file mode 100644 index 00000000..9a3d8837 --- /dev/null +++ b/src/children/OfType.hack @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +final class OfType<<<__Enforceable>> reify T> extends LeafConstraint { + public function legacySerializeAsLeaf(): (LegacyConstraintType, string) { + return tuple( + LegacyConstraintType::CLASSNAME, + \HH\ReifiedGenerics\get_classname(), + ); + } +} diff --git a/src/children/Optional.hack b/src/children/Optional.hack new file mode 100644 index 00000000..d66eb370 --- /dev/null +++ b/src/children/Optional.hack @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +final class Optional extends QuantifierConstraint { + const LegacyExpressionType LEGACY_EXPRESSION_TYPE = + LegacyExpressionType::ZERO_OR_ONE; +} diff --git a/src/children/PCData.hack b/src/children/PCData.hack new file mode 100644 index 00000000..dc7cfa76 --- /dev/null +++ b/src/children/PCData.hack @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +final class PCData extends LeafConstraint { + public function legacySerializeAsLeaf(): (LegacyConstraintType, mixed) { + return tuple(LegacyConstraintType::PCDATA, null); + } +} diff --git a/src/children/QuantifierConstraint.hack b/src/children/QuantifierConstraint.hack new file mode 100644 index 00000000..4181763e --- /dev/null +++ b/src/children/QuantifierConstraint.hack @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +abstract class QuantifierConstraint + implements LegacyExpression { + abstract const LegacyExpressionType LEGACY_EXPRESSION_TYPE; + + final public function __construct(private T $child) {} + + final public function legacySerialize( + ): (LegacyExpressionType, mixed, mixed) { + $inner = $this->child; + $as_leaf = $inner->legacySerializeAsLeaf(); + if ($as_leaf is nonnull) { + return tuple(static::LEGACY_EXPRESSION_TYPE, $as_leaf[0], $as_leaf[1]); + } + + return tuple( + static::LEGACY_EXPRESSION_TYPE, + LegacyConstraintType::EXPRESSION, + $inner->legacySerialize(), + ); + } + + final public function legacySerializeAsLeaf(): null { + return null; + } +} diff --git a/src/children/Sequence.hack b/src/children/Sequence.hack new file mode 100644 index 00000000..a4d3848f --- /dev/null +++ b/src/children/Sequence.hack @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +use namespace HH\Lib\{C, Vec}; + +final class Sequence implements LegacyExpression { + private vec $children; + + public function __construct(T $a, T $b, T ...$rest) { + $this->children = Vec\concat(vec[$a, $b], $rest); + } + + public function legacySerialize(): (LegacyExpressionType, mixed, mixed) { + $it = tuple( + LegacyExpressionType::SEQUENCE, + $this->children[0]->legacySerialize(), + $this->children[1]->legacySerialize(), + ); + $rest = Vec\drop($this->children, 2); + while (!C\is_empty($rest)) { + $it = tuple( + LegacyExpressionType::SEQUENCE, + $it, + $rest[0]->legacySerialize(), + ); + $rest = Vec\drop($rest, 1); + } + return $it; + } + + public function legacySerializeAsLeaf(): null { + return null; + } +} diff --git a/src/children/XHPChildDeclarationConsistencyValidation.hack b/src/children/XHPChildDeclarationConsistencyValidation.hack new file mode 100644 index 00000000..9a016b50 --- /dev/null +++ b/src/children/XHPChildDeclarationConsistencyValidation.hack @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +use namespace \Facebook\XHP\ChildValidation as XHPChild; + +/** Verify that a new child declaration matches the legacy codegen. */ +trait XHPChildDeclarationConsistencyValidation { + require extends :x:element; + + abstract protected static function getChildrenDeclaration( + ): XHPChild\Constraint; + + final private static function normalize(mixed $x): mixed { + if ( + $x is (int, int, mixed) && + $x[0] === XHPChild\LegacyExpressionType::EXACTLY_ONE && + $x[1] === XHPChild\LegacyConstraintType::EXPRESSION + ) { + return self::normalize($x[2]); + } + + if ($x is (int, mixed, mixed)) { + return tuple($x[0], self::normalize($x[1]), self::normalize($x[2])); + } + + return $x; + } + + final public function validateChildren(): void { + $old = self::normalize($this->__xhpChildrenDeclaration()); + $new = self::normalize(static::getChildrenDeclaration()->legacySerialize()); + + invariant( + $old === $new, + "Old and new XHP children declarations differ in class %s.\nOld\n---\n\n%s\n\nNew\n---\n\n%s\n---\n\n%s", + static::class, + \var_export($old, true), + \var_export($new, true), + \var_export(static::getChildrenDeclaration(), true), + ); + parent::validateChildren(); + } +} diff --git a/src/children/XHPChildValidation.hack b/src/children/XHPChildValidation.hack new file mode 100644 index 00000000..b2c73537 --- /dev/null +++ b/src/children/XHPChildValidation.hack @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +use namespace \Facebook\XHP\ChildValidation as XHPChild; + +/** Verify that a new child declaration matches the legacy codegen. */ +trait XHPChildValidation { + require extends :x:element; + + abstract protected static function getChildrenDeclaration( + ): XHPChild\Constraint; + + <<__Override>> + final protected static function __legacySerializedXHPChildrenDeclaration( + ): mixed { + return static::getChildrenDeclaration()->legacySerialize(); + } + + final public function validateChildren(): void { + invariant( + $this->__xhpChildrenDeclaration() === + :x:element::__NO_LEGACY_CHILDREN_DECLARATION, + "The XHPChildValidation trait can not be used with a 'children' ". + "declaration; override 'getChildrenDeclaration()'' instead", + ); + + parent::validateChildren(); + } +} diff --git a/src/children/functions.hack b/src/children/functions.hack new file mode 100644 index 00000000..aa37f9fc --- /dev/null +++ b/src/children/functions.hack @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2004-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +namespace Facebook\XHP\ChildValidation; + +<<__Memoize>> +function any(): Any { + return new Any(); +} + +function anyNumberOf(T $a): AnyNumberOf { + return new AnyNumberOf($a); +} + +function anyOf(T $a, T $b, T ...$rest): AnyOf { + return new AnyOf($a, $b, ...$rest); +} + +function atLeastOneOf(T $a): AtLeastOneOf { + return new AtLeastOneOf($a); +} + +<<__Memoize>> +function category(string $c): Category { + return new Category($c); +} + +<<__Memoize>> +function empty(): None { + return new None(); +} + +function ofType<<<__Enforceable>> reify T>(): OfType { + return new OfType(); +} + +function optional(T $a): Optional { + return new Optional($a); +} + +function pcdata(): PCData { + return new PCData(); +} + +function sequence(T $a, T $b, T ...$rest): Sequence { + return new Sequence($a, $b, ...$rest); +} diff --git a/src/core/ComposableElement.php b/src/core/ComposableElement.php index 2d948116..d8203726 100644 --- a/src/core/ComposableElement.php +++ b/src/core/ComposableElement.php @@ -270,12 +270,20 @@ final public static function __xhpReflectionAttributes( return $map; } + protected static function __legacySerializedXHPChildrenDeclaration(): mixed { + $decl = self::emptyInstance()->__xhpChildrenDeclaration(); + if ($decl === self::__NO_LEGACY_CHILDREN_DECLARATION) { + return 1; // any children + } + return $decl; + } + <<__MemoizeLSB>> final public static function __xhpReflectionChildrenDeclaration( ): ReflectionXHPChildrenDeclaration { return new ReflectionXHPChildrenDeclaration( :xhp::class2element(static::class), - self::emptyInstance()->__xhpChildrenDeclaration(), + static::__legacySerializedXHPChildrenDeclaration(), ); } @@ -498,6 +506,8 @@ protected function __xhpCategoryDeclaration(): darray { return darray[]; } + const int __NO_LEGACY_CHILDREN_DECLARATION = -31337; + /** * Defined in elements by the `children` keyword. This returns a pattern of * allowed children. The return value is potentially very complicated. The @@ -506,7 +516,7 @@ protected function __xhpCategoryDeclaration(): darray { * biggest mess you've ever seen. */ protected function __xhpChildrenDeclaration(): mixed { - return 1; + return self::__NO_LEGACY_CHILDREN_DECLARATION; } /** @@ -580,7 +590,7 @@ final protected function validateEnumValuesAndCoerceScalars( * Validates that this element's children match its children descriptor, and * throws an exception if that's not the case. */ - final protected function validateChildren(): void { + protected function validateChildren(): void { $decl = self::__xhpReflectionChildrenDeclaration(); $type = $decl->getType(); if ($type === XHPChildrenDeclarationType::ANY_CHILDREN) { diff --git a/tests/ChildRuleTest.php b/tests/ChildRuleTest.php index 37511a59..fa04b744 100644 --- a/tests/ChildRuleTest.php +++ b/tests/ChildRuleTest.php @@ -10,73 +10,193 @@ use function Facebook\FBExpect\expect; use type Facebook\HackTest\DataProvider; +use namespace Facebook\XHP\ChildValidation as XHPChild; + +class :test:new-child-declaration-only extends :x:element { + use XHPChildValidation; + + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\ofType<:div>(); + } + + protected function render(): XHPRoot { + return {$this->getChildren()}; + } +} + +class :test:new-and-old-child-declarations extends :x:element { + // Providing all of these is invalid; for a migration consistency check, use + // the XHPChildDeclarationConsistencyValidation trait instead. + use XHPChildValidation; + children (:div); + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\ofType<:div>(); + } + + protected function render(): XHPRoot { + return
; + } +} + +class :test:old-child-declaration-only extends :x:element { + children (:div); + + protected function render(): XHPRoot { + return {$this->getChildren()}; + } +} class :test:any-children extends :x:element { + use XHPChildDeclarationConsistencyValidation; children any; + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\any(); + } + protected function render(): XHPRoot { return
; } } class :test:no-children extends :x:element { + use XHPChildDeclarationConsistencyValidation; children empty; + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\empty(); + } + protected function render(): XHPRoot { return
; } } class :test:single-child extends :x:element { + use XHPChildDeclarationConsistencyValidation; children (:div); + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\ofType<:div>(); + } + protected function render(): XHPRoot { return
; } } class :test:optional-child extends :x:element { + use XHPChildDeclarationConsistencyValidation; children (:div?); + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\optional(XHPChild\ofType<:div>()); + } + protected function render(): XHPRoot { return
; } } - class :test:any-number-of-child extends :x:element { + use XHPChildDeclarationConsistencyValidation; children (:div*); + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\anyNumberOf(XHPChild\ofType<:div>()); + } + protected function render(): XHPRoot { return
; } } class :test:at-least-one-child extends :x:element { + use XHPChildDeclarationConsistencyValidation; children (:div+); + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\atLeastOneOf(XHPChild\ofType<:div>()); + } + protected function render(): XHPRoot { return
; } } class :test:two-children extends :x:element { + use XHPChildDeclarationConsistencyValidation; children (:div, :div); + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\sequence(XHPChild\ofType<:div>(), XHPChild\ofType<:div>()); + } + + protected function render(): XHPRoot { + return
; + } +} + +class :test:three-children extends :x:element { + use XHPChildDeclarationConsistencyValidation; + children (:div, :div, :div); + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\sequence( + XHPChild\ofType<:div>(), + XHPChild\ofType<:div>(), + XHPChild\ofType<:div>(), + ); + } + protected function render(): XHPRoot { return
; } } + class :test:either-of-two-children extends :x:element { + use XHPChildDeclarationConsistencyValidation; children (:div | :code); + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\anyOf(XHPChild\ofType<:div>(), XHPChild\ofType<:code>()); + } + protected function render(): XHPRoot { return
; } } +class :test:any-of-three-children extends :x:element { + use XHPChildDeclarationConsistencyValidation; + children (:div | :code | :p); + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\anyOf( + XHPChild\ofType<:div>(), + XHPChild\ofType<:code>(), + XHPChild\ofType<:p>(), + ); + } + + protected function render(): XHPRoot { + return
; + } +} + + class :test:nested-rule extends :x:element { + use XHPChildDeclarationConsistencyValidation; children (:div | (:code+)); + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\anyOf( + XHPChild\ofType<:div>(), + XHPChild\atLeastOneOf(XHPChild\ofType<:code>()), + ); + } + protected function render(): XHPRoot { return
; } } class :test:pcdata-child extends :x:element { + use XHPChildDeclarationConsistencyValidation; children (pcdata); + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\pcdata(); + } protected function render(): XHPRoot { return
{$this->getChildren()}
; @@ -84,7 +204,11 @@ protected function render(): XHPRoot { } class :test:category-child extends :x:element { + use XHPChildDeclarationConsistencyValidation; children (%flow); + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\category('%flow'); + } protected function render(): XHPRoot { return
; @@ -100,7 +224,11 @@ protected function render(): XHPRoot { } class :test:needs-comma-category extends :x:element { + use XHPChildDeclarationConsistencyValidation; children (%foo:bar); + protected static function getChildrenDeclaration(): XHPChild\Constraint { + return XHPChild\category('%foo:bar'); + } protected function render(): XHPRoot { return
; @@ -135,6 +263,7 @@ public function testSingleChild(): void { , , , + , , , }; @@ -161,7 +290,9 @@ public function toStringProvider(): vec<(:xhp, string)> { tuple(, '(:div*)'), tuple(, '(:div+)'), tuple(, '(:div,:div)'), + tuple(, '(:div,:div,:div)'), tuple(, '(:div|:code)'), + tuple(, '(:div|:code|:p)'), tuple(, '(:div|(:code+))'), tuple(, '(pcdata)'), tuple(, '(%flow)'), @@ -192,13 +323,14 @@ public function testTooManyChildren(): void { , , , + , , , , }; foreach ($elems as $elem) { $exception = null; - $elem->appendChild(
); + $elem->appendChild(
); try { $elem->toString(); } catch (Exception $e) { @@ -215,6 +347,7 @@ public function testIncorrectChild(): void { , , , + , , , }; @@ -290,4 +423,48 @@ public function testNested(): void { $x->toString(); })->toThrow(XHPInvalidChildrenException::class); } + + public function testNewChildDeclarations(): void { + expect( + ( + +
foo
+
+ )->toString(), + )->toEqual('
foo
'); + + expect(() ==> ()->toString())->toThrow( + XHPInvalidChildrenException::class, + ); + expect( + () ==> ( +

+ )->toString(), + )->toThrow(XHPInvalidChildrenException::class); + } + + public function testOldChildDeclarations(): void { + expect( + ( + +

foo
+ + )->toString(), + )->toEqual('
foo
'); + + expect(() ==> ()->toString())->toThrow( + XHPInvalidChildrenException::class, + ); + expect( + () ==> ( +

+ )->toString(), + )->toThrow(XHPInvalidChildrenException::class); + } + + + public function testConflictingNewAndOldChildDeclarations(): void { + expect(() ==> ()->toString()) + ->toThrow(InvariantException::class); + } }