diff --git a/docs/expressions.rst b/docs/expressions.rst new file mode 100644 index 00000000..679e6d40 --- /dev/null +++ b/docs/expressions.rst @@ -0,0 +1,60 @@ +Expressions +=========== + +Starting with version 5.4, we now support parsing expressions and extracting types and references to elements from them. + +.. info:: + + An expression is, for example, the default value for a property or argument, the definition of an enum case or a + constant value. These are called expressions and can contain more complex combinations of operators and values. + +As this library revolves around reflecting Static information, most parts of an expression are considered irrelevant; +except for type information -such as type hints- and references to other elements, such as constants. As such, whenever +an expression is interpreted, it will result in a string containing placeholders and an array containing the reflected +parts -such as FQSENs-. + +This means that the getters like ``getDefault()`` will return a string or when you provide the optional argument +$isString as being false, it will return an Expression object; which, when cast to string, will provide the same result. + +.. warning:: + + Deprecation: In version 6, we will remove the optional argument and always return an Expression object. When the + result was used as a string nothing will change, but code that checks if the output is a string will no longer + function starting from that version. + +This will allow consumers to be able to extract types and links to elements from expressions. This allows consumers to, +for example, interpret the default value for a constructor promoted properties when it directly instantiates an object. + +Creating expressions +-------------------- + +.. hint:: + + The description below is only for internal usage and to understand how expressions work, this library deals with + this by default. + +In this library, we use the ExpressionPrinter to convert a PHP-Parser node -or expression- into an expression +like this:: + + $printer = new ExpressionPrinter(); + $expressionTemplate = $printer->prettyPrintExpr($phpParserNode); + $expression = new Expression($expressionTemplate, $printer->getParts()); + +In the example above we assume that there is a PHP-Parser node representing an expression; this node is passed to the +ExpressionPrinter -which is an adapted PrettyPrinter from PHP-Parser- which will render the expression as a readable +template string containing placeholders, and a list of parts that can slot into the placeholders. + +Consuming expressions +--------------------- + +When using this library, you can consume these expression objects either by + +1. Directly casting them to a string - this will replace all placeholders with the stringified version of the parts +2. Use the render function - this will do the same as the previous methods but you can specify one or more overrides + for the placeholders in the expression + +The second method can be used to create your own string values from the given parts and render, for example, links in +these locations. + +Another way to use these expressions is to interpret the parts array, and through that way know which elements and +types are referred to in that expression. diff --git a/docs/index.rst b/docs/index.rst index 52b3506e..64982f17 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,4 +25,5 @@ are however several advantages to using this library: getting-started reflection-structure + expressions extending/index diff --git a/phpstan.neon b/phpstan.neon index 71dffd7c..63041f70 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,7 +6,6 @@ parameters: ignoreErrors: - '#Method phpDocumentor\\Reflection\\File\\LocalFile::md5\(\) should return string but returns string\|false\.#' - - '#Else branch is unreachable because ternary operator condition is always true\.#' # # all these $fqsen errors indicate the need for a decorator class around PhpParser\Node to hold the public $fqsen that Reflection is giving it) # diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 2ec6bdbc..dd789ceb 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -53,9 +53,6 @@ - - getValue() !== null]]> - @@ -115,11 +112,6 @@ getAttribute('fqsen')]]> - - - getValue() !== null]]> - - constant->consts[$this->index]->getAttribute('fqsen')]]> diff --git a/psalm.xml b/psalm.xml index bdac9dad..b72053c2 100644 --- a/psalm.xml +++ b/psalm.xml @@ -9,6 +9,11 @@ + + diff --git a/src/phpDocumentor/Reflection/Php/Argument.php b/src/phpDocumentor/Reflection/Php/Argument.php index 25fb6010..2551553b 100644 --- a/src/phpDocumentor/Reflection/Php/Argument.php +++ b/src/phpDocumentor/Reflection/Php/Argument.php @@ -16,6 +16,11 @@ use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Types\Mixed_; +use function is_string; +use function trigger_error; + +use const E_USER_DEPRECATED; + /** * Descriptor representing a single Argument of a method or function. * @@ -33,8 +38,8 @@ public function __construct( /** @var string name of the Argument */ private readonly string $name, Type|null $type = null, - /** @var string|null the default value for an argument or null if none is provided */ - private readonly string|null $default = null, + /** @var Expression|string|null the default value for an argument or null if none is provided */ + private Expression|string|null $default = null, /** @var bool whether the argument passes the parameter by reference instead of by value */ private readonly bool $byReference = false, /** @var bool Determines if this Argument represents a variadic argument */ @@ -44,6 +49,15 @@ public function __construct( $type = new Mixed_(); } + if (is_string($this->default)) { + trigger_error( + 'Default values for arguments should be of type Expression, support for strings will be ' + . 'removed in 7.x', + E_USER_DEPRECATED, + ); + $this->default = new Expression($this->default, []); + } + $this->type = $type; } @@ -60,8 +74,21 @@ public function getType(): Type|null return $this->type; } - public function getDefault(): string|null + public function getDefault(bool $asString = true): Expression|string|null { + if ($this->default === null) { + return null; + } + + if ($asString) { + trigger_error( + 'The Default value will become of type Expression by default', + E_USER_DEPRECATED, + ); + + return (string) $this->default; + } + return $this->default; } diff --git a/src/phpDocumentor/Reflection/Php/Constant.php b/src/phpDocumentor/Reflection/Php/Constant.php index 3d9bf700..1b334758 100644 --- a/src/phpDocumentor/Reflection/Php/Constant.php +++ b/src/phpDocumentor/Reflection/Php/Constant.php @@ -20,6 +20,11 @@ use phpDocumentor\Reflection\Location; use phpDocumentor\Reflection\Metadata\MetaDataContainer as MetaDataContainerInterface; +use function is_string; +use function trigger_error; + +use const E_USER_DEPRECATED; + /** * Descriptor representing a constant * @@ -42,7 +47,7 @@ final class Constant implements Element, MetaDataContainerInterface, AttributeCo public function __construct( private readonly Fqsen $fqsen, private readonly DocBlock|null $docBlock = null, - private readonly string|null $value = null, + private Expression|string|null $value = null, Location|null $location = null, Location|null $endLocation = null, Visibility|null $visibility = null, @@ -51,13 +56,37 @@ public function __construct( $this->location = $location ?: new Location(-1); $this->endLocation = $endLocation ?: new Location(-1); $this->visibility = $visibility ?: new Visibility(Visibility::PUBLIC_); + + if (!is_string($this->value)) { + return; + } + + trigger_error( + 'Constant values should be of type Expression, support for strings will be ' + . 'removed in 6.x', + E_USER_DEPRECATED, + ); + $this->value = new Expression($this->value, []); } /** - * Returns the value of this constant. + * Returns the expression value for this constant. */ - public function getValue(): string|null + public function getValue(bool $asString = true): Expression|string|null { + if ($this->value === null) { + return null; + } + + if ($asString) { + trigger_error( + 'The expression value will become of type Expression by default', + E_USER_DEPRECATED, + ); + + return (string) $this->value; + } + return $this->value; } diff --git a/src/phpDocumentor/Reflection/Php/EnumCase.php b/src/phpDocumentor/Reflection/Php/EnumCase.php index 1d30fc36..f70934ff 100644 --- a/src/phpDocumentor/Reflection/Php/EnumCase.php +++ b/src/phpDocumentor/Reflection/Php/EnumCase.php @@ -11,6 +11,11 @@ use phpDocumentor\Reflection\Location; use phpDocumentor\Reflection\Metadata\MetaDataContainer as MetaDataContainerInterface; +use function is_string; +use function trigger_error; + +use const E_USER_DEPRECATED; + /** * Represents a case in an Enum. * @@ -30,7 +35,7 @@ public function __construct( private readonly DocBlock|null $docBlock, Location|null $location = null, Location|null $endLocation = null, - private readonly string|null $value = null, + private Expression|string|null $value = null, ) { if ($location === null) { $location = new Location(-1); @@ -42,6 +47,16 @@ public function __construct( $this->location = $location; $this->endLocation = $endLocation; + if (!is_string($this->value)) { + return; + } + + trigger_error( + 'Expression values for enum cases should be of type Expression, support for strings will be ' + . 'removed in 7.x', + E_USER_DEPRECATED, + ); + $this->value = new Expression($this->value, []); } #[Override] @@ -71,8 +86,24 @@ public function getEndLocation(): Location return $this->endLocation; } - public function getValue(): string|null + /** + * Returns the value for this enum case. + */ + public function getValue(bool $asString = true): Expression|string|null { + if ($this->value === null) { + return null; + } + + if ($asString) { + trigger_error( + 'The enum case value will become of type Expression by default', + E_USER_DEPRECATED, + ); + + return (string) $this->value; + } + return $this->value; } } diff --git a/src/phpDocumentor/Reflection/Php/Expression.php b/src/phpDocumentor/Reflection/Php/Expression.php new file mode 100644 index 00000000..d1a253cf --- /dev/null +++ b/src/phpDocumentor/Reflection/Php/Expression.php @@ -0,0 +1,156 @@ + value pair + * that can be used by consumers to map the data to another formatting, adding links for example, and then render + * the expression. + * + * @var array + */ + private array $parts; + + /** + * Returns the recommended placeholder string format given a name. + * + * Consumers can use their own formats when needed, the placeholders are all keys in the {@see self::$parts} array + * and not interpreted by this class. However, to prevent collisions it is recommended to use this method to + * generate a placeholder. + * + * @param string $name a string identifying the element for which the placeholder is generated. + */ + public static function generatePlaceholder(string $name): string + { + Assert::notEmpty($name); + + return '{{ PHPDOC' . md5($name) . ' }}'; + } + + /** @param array $parts */ + public function __construct(string $expression, array $parts = []) + { + Assert::notEmpty($expression); + Assert::allIsInstanceOfAny($parts, [Fqsen::class, Type::class]); + + $this->expression = $expression; + $this->parts = $parts; + } + + /** + * The raw expression string containing placeholders for any extracted Types or FQSENs. + * + * @see self::render() to render a human-readable expression and to replace some parts with custom values. + * @see self::__toString() to render a human-readable expression with the previously extracted parts. + */ + public function getExpression(): string + { + return $this->expression; + } + + /** + * A list of extracted parts for which placeholders exist in the expression string. + * + * The returned array will have the placeholders of the expression string as keys, and the related FQSEN or Type as + * value. This can be used as a basis for doing your own transformations to {@see self::render()} the expression + * in a custom way; or to extract type information from an expression and use that elsewhere in your application. + * + * @see ExpressionPrinter to transform a PHP-Parser expression into an expression string and list of parts. + * + * @return array + */ + public function getParts(): array + { + return $this->parts; + } + + /** + * Renders the expression as a string and replaces all placeholders with either a provided value, or the + * stringified value from the parts in this expression. + * + * The keys of the replacement parts should match those of {@see self::getParts()}, any unrecognized key is not + * handled. + * + * @param array $replacementParts + */ + public function render(array $replacementParts = []): string + { + Assert::allStringNotEmpty($replacementParts); + + $valuesAsStrings = []; + foreach ($this->parts as $placeholder => $part) { + $valuesAsStrings[$placeholder] = $replacementParts[$placeholder] ?? (string) $part; + } + + return str_replace(array_keys($this->parts), $valuesAsStrings, $this->expression); + } + + /** + * Returns a rendered version of the expression string where all placeholders are replaced by the stringified + * versions of the included parts. + * + * @see self::$parts for the list of parts used in rendering + * @see self::render() to influence rendering of the expression. + */ + public function __toString(): string + { + return $this->render(); + } +} diff --git a/src/phpDocumentor/Reflection/Php/Expression/ExpressionPrinter.php b/src/phpDocumentor/Reflection/Php/Expression/ExpressionPrinter.php new file mode 100644 index 00000000..c56b2473 --- /dev/null +++ b/src/phpDocumentor/Reflection/Php/Expression/ExpressionPrinter.php @@ -0,0 +1,72 @@ + */ + private array $parts = []; + + protected function resetState(): void + { + parent::resetState(); + + $this->parts = []; + } + + protected function pName(Name $node): string + { + $renderedName = parent::pName($node); + $placeholder = Expression::generatePlaceholder($renderedName); + $this->parts[$placeholder] = new Fqsen('\\' . $renderedName); + + return $placeholder; + } + + // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps + protected function pName_FullyQualified(Name\FullyQualified $node): string + { + $renderedName = parent::pName_FullyQualified($node); + $placeholder = Expression::generatePlaceholder($renderedName); + $this->parts[$placeholder] = new Fqsen($renderedName); + + return $placeholder; + } + + protected function pExpr_ClassConstFetch(Expr\ClassConstFetch $node): string + { + $renderedName = parent::pObjectProperty($node->name); + $className = $node->class instanceof Name ? parent::pName($node->class) : $this->p($node->class); + $placeholder = Expression::generatePlaceholder($renderedName); + $this->parts[$placeholder] = new Fqsen( + '\\' . $className . '::' . $renderedName + ); + + return $placeholder; + } + + + /** @return array */ + public function getParts(): array + { + return $this->parts; + } +} diff --git a/src/phpDocumentor/Reflection/Php/Factory/ClassConstant.php b/src/phpDocumentor/Reflection/Php/Factory/ClassConstant.php index 9c594109..7d6ed2b9 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/ClassConstant.php +++ b/src/phpDocumentor/Reflection/Php/Factory/ClassConstant.php @@ -19,6 +19,8 @@ use phpDocumentor\Reflection\Php\Class_; use phpDocumentor\Reflection\Php\Constant as ConstantElement; use phpDocumentor\Reflection\Php\Enum_; +use phpDocumentor\Reflection\Php\Expression; +use phpDocumentor\Reflection\Php\Expression\ExpressionPrinter; use phpDocumentor\Reflection\Php\Factory\Reducer\Reducer; use phpDocumentor\Reflection\Php\Interface_; use phpDocumentor\Reflection\Php\StrategyContainer; @@ -28,6 +30,8 @@ use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use Webmozart\Assert\Assert; +use function is_string; + /** * Strategy to convert ClassConstantIterator to ConstantElement * @@ -84,7 +88,7 @@ protected function doCreate( $constant = new ConstantElement( $const->getFqsen(), $this->createDocBlock($const->getDocComment(), $context->getTypeContext()), - $const->getValue() !== null ? $this->valueConverter->prettyPrintExpr($const->getValue()) : null, + $this->determineValue($const), new Location($const->getLine()), new Location($const->getEndLine()), $this->buildVisibility($const), @@ -105,6 +109,20 @@ protected function doCreate( return null; } + private function determineValue(ClassConstantIterator $value): Expression + { + $expression = $this->valueConverter->prettyPrintExpr($value->getValue()); + if ($this->valueConverter instanceof ExpressionPrinter) { + $expression = new Expression($expression, $this->valueConverter->getParts()); + } + + if (is_string($expression)) { + $expression = new Expression($expression, []); + } + + return $expression; + } + /** * Converts the visibility of the constant to a valid Visibility object. */ diff --git a/src/phpDocumentor/Reflection/Php/Factory/ConstructorPromotion.php b/src/phpDocumentor/Reflection/Php/Factory/ConstructorPromotion.php index ef8754e8..69c709bd 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/ConstructorPromotion.php +++ b/src/phpDocumentor/Reflection/Php/Factory/ConstructorPromotion.php @@ -79,8 +79,8 @@ private function promoteParameterToProperty(ContextStack $context, StrategyConta ->default($param->default) ->readOnly($this->readOnly($param->flags)) ->static(false) - ->startLocation(new Location($param->getLine(), $param->getStartFilePos())) - ->endLocation(new Location($param->getEndLine(), $param->getEndFilePos())) + ->startLocation(new Location($param->getLine())) + ->endLocation(new Location($param->getEndLine())) ->hooks($param->hooks ?? []) ->build($context); diff --git a/src/phpDocumentor/Reflection/Php/Factory/Define.php b/src/phpDocumentor/Reflection/Php/Factory/Define.php index 28a4fe36..e51e0bfc 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/Define.php +++ b/src/phpDocumentor/Reflection/Php/Factory/Define.php @@ -18,6 +18,8 @@ use phpDocumentor\Reflection\Fqsen; use phpDocumentor\Reflection\Location; use phpDocumentor\Reflection\Php\Constant as ConstantElement; +use phpDocumentor\Reflection\Php\Expression as ValueExpression; +use phpDocumentor\Reflection\Php\Expression\ExpressionPrinter; use phpDocumentor\Reflection\Php\File as FileElement; use phpDocumentor\Reflection\Php\StrategyContainer; use phpDocumentor\Reflection\Php\ValueEvaluator\ConstantEvaluator; @@ -31,6 +33,7 @@ use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use function assert; +use function is_string; use function sprintf; use function str_starts_with; @@ -118,13 +121,22 @@ protected function doCreate( return $constant; } - private function determineValue(Arg|null $value): string|null + private function determineValue(Arg|null $value): ValueExpression|null { if ($value === null) { return null; } - return $this->valueConverter->prettyPrintExpr($value->value); + $expression = $this->valueConverter->prettyPrintExpr($value->value); + if ($this->valueConverter instanceof ExpressionPrinter) { + $expression = new ValueExpression($expression, $this->valueConverter->getParts()); + } + + if (is_string($expression)) { + $expression = new ValueExpression($expression, []); + } + + return $expression; } private function determineFqsen(Arg $name, ContextStack $context): Fqsen|null diff --git a/src/phpDocumentor/Reflection/Php/Factory/EnumCase.php b/src/phpDocumentor/Reflection/Php/Factory/EnumCase.php index 14338d94..8e1fc9bc 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/EnumCase.php +++ b/src/phpDocumentor/Reflection/Php/Factory/EnumCase.php @@ -9,12 +9,15 @@ use phpDocumentor\Reflection\Location; use phpDocumentor\Reflection\Php\Enum_ as EnumElement; use phpDocumentor\Reflection\Php\EnumCase as EnumCaseElement; +use phpDocumentor\Reflection\Php\Expression as ValueExpression; +use phpDocumentor\Reflection\Php\Expression\ExpressionPrinter; use phpDocumentor\Reflection\Php\Factory\Reducer\Reducer; use phpDocumentor\Reflection\Php\StrategyContainer; use PhpParser\Node\Stmt\EnumCase as EnumCaseNode; use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use function assert; +use function is_string; final class EnumCase extends AbstractFactory { @@ -46,11 +49,29 @@ protected function doCreate(ContextStack $context, object $object, StrategyConta $docBlock, new Location($object->getLine()), new Location($object->getEndLine()), - $object->expr !== null ? $this->prettyPrinter->prettyPrintExpr($object->expr) : null, + $this->determineValue($object), ); $enum->addCase($case); return $case; } + + private function determineValue(EnumCaseNode $value): ValueExpression|null + { + $expression = $value->expr !== null ? $this->prettyPrinter->prettyPrintExpr($value->expr) : null; + if ($expression === null) { + return null; + } + + if ($this->prettyPrinter instanceof ExpressionPrinter) { + $expression = new ValueExpression($expression, $this->prettyPrinter->getParts()); + } + + if (is_string($expression)) { + $expression = new ValueExpression($expression, []); + } + + return $expression; + } } diff --git a/src/phpDocumentor/Reflection/Php/Factory/GlobalConstant.php b/src/phpDocumentor/Reflection/Php/Factory/GlobalConstant.php index fc1e04ac..eeb4b81a 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/GlobalConstant.php +++ b/src/phpDocumentor/Reflection/Php/Factory/GlobalConstant.php @@ -17,12 +17,16 @@ use phpDocumentor\Reflection\DocBlockFactoryInterface; use phpDocumentor\Reflection\Location; use phpDocumentor\Reflection\Php\Constant as ConstantElement; +use phpDocumentor\Reflection\Php\Expression; +use phpDocumentor\Reflection\Php\Expression\ExpressionPrinter; use phpDocumentor\Reflection\Php\File as FileElement; use phpDocumentor\Reflection\Php\StrategyContainer; use PhpParser\Node\Stmt\Const_; use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use Webmozart\Assert\Assert; +use function is_string; + /** * Strategy to convert GlobalConstantIterator to ConstantElement * @@ -70,7 +74,7 @@ protected function doCreate( new ConstantElement( $const->getFqsen(), $this->createDocBlock($const->getDocComment(), $context->getTypeContext()), - $const->getValue() !== null ? $this->valueConverter->prettyPrintExpr($const->getValue()) : null, + $this->determineValue($const), new Location($const->getLine()), new Location($const->getEndLine()), ), @@ -79,4 +83,18 @@ protected function doCreate( return null; } + + private function determineValue(GlobalConstantIterator $value): Expression + { + $expression = $this->valueConverter->prettyPrintExpr($value->getValue()); + if ($this->valueConverter instanceof ExpressionPrinter) { + $expression = new Expression($expression, $this->valueConverter->getParts()); + } + + if (is_string($expression)) { + $expression = new Expression($expression, []); + } + + return $expression; + } } diff --git a/src/phpDocumentor/Reflection/Php/Factory/Property.php b/src/phpDocumentor/Reflection/Php/Factory/Property.php index 4c146677..ea996baa 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/Property.php +++ b/src/phpDocumentor/Reflection/Php/Factory/Property.php @@ -25,6 +25,8 @@ use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use Webmozart\Assert\Assert; +use function is_string; + /** * Strategy to convert PropertyIterator to PropertyDescriptor * @@ -84,7 +86,7 @@ protected function doCreate( ->visibility($stmt) ->type($stmt->getType()) ->docblock($stmt->getDocComment()) - ->default($iterator->getDefault()) + ->default($stmt->getDefault()) ->static($stmt->isStatic()) ->startLocation(new Location($stmt->getLine())) ->endLocation(new Location($stmt->getEndLine())) diff --git a/src/phpDocumentor/Reflection/Php/Factory/PropertyBuilder.php b/src/phpDocumentor/Reflection/Php/Factory/PropertyBuilder.php index db1a2776..8be342fe 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/PropertyBuilder.php +++ b/src/phpDocumentor/Reflection/Php/Factory/PropertyBuilder.php @@ -9,6 +9,8 @@ use phpDocumentor\Reflection\Location; use phpDocumentor\Reflection\NodeVisitor\FindingVisitor; use phpDocumentor\Reflection\Php\AsymmetricVisibility; +use phpDocumentor\Reflection\Php\Expression; +use phpDocumentor\Reflection\Php\Expression\ExpressionPrinter; use phpDocumentor\Reflection\Php\Factory\Reducer\Reducer; use phpDocumentor\Reflection\Php\Property as PropertyElement; use phpDocumentor\Reflection\Php\PropertyHook; @@ -26,11 +28,12 @@ use PhpParser\Node\Param; use PhpParser\Node\PropertyHook as PropertyHookNode; use PhpParser\NodeTraverser; -use PhpParser\PrettyPrinter\Standard as PrettyPrinter; +use PhpParser\PrettyPrinter; use function array_filter; use function array_map; use function count; +use function is_string; use function method_exists; /** @@ -159,7 +162,7 @@ public function build(ContextStack $context): PropertyElement $this->fqsen, $this->visibility, $this->docblock !== null ? $this->docBlockFactory->create($this->docblock->getText(), $context->getTypeContext()) : null, - $this->default !== null ? $this->valueConverter->prettyPrintExpr($this->default) : null, + $this->determineDefault(), $this->static, $this->startLocation, $this->endLocation, @@ -341,4 +344,22 @@ private function buildHookVisibility(string $hookName, Visibility $propertyVisib default => $propertyVisibility, }; } + + private function determineDefault(): Expression|null + { + $expression = $this->default !== null ? $this->valueConverter->prettyPrintExpr($this->default) : null; + if ($expression === null) { + return null; + } + + if ($this->valueConverter instanceof ExpressionPrinter) { + $expression = new Expression($expression, $this->valueConverter->getParts()); + } + + if (is_string($expression)) { + $expression = new Expression($expression, []); + } + + return $expression; + } } diff --git a/src/phpDocumentor/Reflection/Php/Factory/Reducer/Parameter.php b/src/phpDocumentor/Reflection/Php/Factory/Reducer/Parameter.php index 074defc1..c885932f 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/Reducer/Parameter.php +++ b/src/phpDocumentor/Reflection/Php/Factory/Reducer/Parameter.php @@ -6,6 +6,8 @@ use Override; use phpDocumentor\Reflection\Php\Argument as ArgumentDescriptor; +use phpDocumentor\Reflection\Php\Expression; +use phpDocumentor\Reflection\Php\Expression\ExpressionPrinter; use phpDocumentor\Reflection\Php\Factory\ContextStack; use phpDocumentor\Reflection\Php\Factory\Type; use phpDocumentor\Reflection\Php\Function_; @@ -14,6 +16,7 @@ use phpDocumentor\Reflection\Php\StrategyContainer; use PhpParser\Node\Expr\Variable; use PhpParser\Node\FunctionLike; +use PhpParser\Node\Param; use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use Webmozart\Assert\Assert; @@ -47,7 +50,7 @@ public function reduce( new ArgumentDescriptor( is_string($param->var->name) ? $param->var->name : $this->valueConverter->prettyPrintExpr($param->var->name), (new Type())->fromPhpParser($param->type), - $param->default !== null ? $this->valueConverter->prettyPrintExpr($param->default) : null, + $this->determineDefault($param), $param->byRef, $param->variadic, ), @@ -56,4 +59,22 @@ public function reduce( return $carry; } + + private function determineDefault(Param $value): Expression|null + { + $expression = $value->default !== null ? $this->valueConverter->prettyPrintExpr($value->default) : null; + if ($expression === null) { + return null; + } + + if ($this->valueConverter instanceof ExpressionPrinter) { + $expression = new Expression($expression, $this->valueConverter->getParts()); + } + + if (is_string($expression)) { + $expression = new Expression($expression, []); + } + + return $expression; + } } diff --git a/src/phpDocumentor/Reflection/Php/ProjectFactory.php b/src/phpDocumentor/Reflection/Php/ProjectFactory.php index dd461b76..6fab4653 100644 --- a/src/phpDocumentor/Reflection/Php/ProjectFactory.php +++ b/src/phpDocumentor/Reflection/Php/ProjectFactory.php @@ -18,6 +18,7 @@ use phpDocumentor\Reflection\Exception; use phpDocumentor\Reflection\File as SourceFile; use phpDocumentor\Reflection\Fqsen; +use phpDocumentor\Reflection\Php\Expression\ExpressionPrinter; use phpDocumentor\Reflection\Php\Factory\Class_; use phpDocumentor\Reflection\Php\Factory\ClassConstant; use phpDocumentor\Reflection\Php\Factory\ConstructorPromotion; @@ -38,7 +39,6 @@ use phpDocumentor\Reflection\Php\Factory\TraitUse; use phpDocumentor\Reflection\Project as ProjectInterface; use phpDocumentor\Reflection\ProjectFactory as ProjectFactoryInterface; -use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use function is_array; @@ -67,9 +67,10 @@ public function __construct(array|ProjectFactoryStrategies $strategies) public static function createInstance(): self { $docblockFactory = DocBlockFactory::createInstance(); + $expressionPrinter = new ExpressionPrinter(); $attributeReducer = new Attribute(); - $parameterReducer = new Parameter(new PrettyPrinter()); + $parameterReducer = new Parameter($expressionPrinter); $methodStrategy = new Method($docblockFactory, [$attributeReducer, $parameterReducer]); @@ -78,15 +79,15 @@ public static function createInstance(): self new \phpDocumentor\Reflection\Php\Factory\Namespace_(), new Class_($docblockFactory, [$attributeReducer]), new Enum_($docblockFactory, [$attributeReducer]), - new EnumCase($docblockFactory, new PrettyPrinter(), [$attributeReducer]), - new Define($docblockFactory, new PrettyPrinter()), - new GlobalConstant($docblockFactory, new PrettyPrinter()), - new ClassConstant($docblockFactory, new PrettyPrinter(), [$attributeReducer]), + new EnumCase($docblockFactory, $expressionPrinter, [$attributeReducer]), + new Define($docblockFactory, $expressionPrinter), + new GlobalConstant($docblockFactory, $expressionPrinter), + new ClassConstant($docblockFactory, $expressionPrinter, [$attributeReducer]), new Factory\File($docblockFactory, NodesFactory::createInstance()), new Function_($docblockFactory, [$attributeReducer, $parameterReducer]), new Interface_($docblockFactory, [$attributeReducer]), $methodStrategy, - new Property($docblockFactory, new PrettyPrinter(), [$attributeReducer, $parameterReducer]), + new Property($docblockFactory, $expressionPrinter, [$attributeReducer, $parameterReducer]), new Trait_($docblockFactory, [$attributeReducer]), new IfStatement(), @@ -95,7 +96,7 @@ public static function createInstance(): self ); $strategies->addStrategy( - new ConstructorPromotion($methodStrategy, $docblockFactory, new PrettyPrinter(), [$attributeReducer, $parameterReducer]), + new ConstructorPromotion($methodStrategy, $docblockFactory, $expressionPrinter, [$attributeReducer, $parameterReducer]), 1100, ); $strategies->addStrategy(new Noop(), -PHP_INT_MAX); diff --git a/src/phpDocumentor/Reflection/Php/Property.php b/src/phpDocumentor/Reflection/Php/Property.php index 2f72d5f0..7f0c00c5 100644 --- a/src/phpDocumentor/Reflection/Php/Property.php +++ b/src/phpDocumentor/Reflection/Php/Property.php @@ -21,6 +21,11 @@ use phpDocumentor\Reflection\Metadata\MetaDataContainer as MetaDataContainerInterface; use phpDocumentor\Reflection\Type; +use function is_string; +use function trigger_error; + +use const E_USER_DEPRECATED; + /** * Descriptor representing a property. * @@ -48,7 +53,7 @@ public function __construct( private readonly Fqsen $fqsen, Visibility|null $visibility = null, private readonly DocBlock|null $docBlock = null, - private readonly string|null $default = null, + private Expression|string|null $default = null, private readonly bool $static = false, Location|null $location = null, Location|null $endLocation = null, @@ -60,13 +65,37 @@ public function __construct( $this->visibility = $visibility ?: new Visibility('public'); $this->location = $location ?: new Location(-1); $this->endLocation = $endLocation ?: new Location(-1); + + if (!is_string($this->default)) { + return; + } + + trigger_error( + 'Default values for properties should be of type Expression, support for strings will be ' + . 'removed in 7.x', + E_USER_DEPRECATED, + ); + $this->default = new Expression($this->default, []); } /** - * returns the default value of this property. + * Returns the default value for this property. */ - public function getDefault(): string|null + public function getDefault(bool $asString = true): Expression|string|null { + if ($this->default === null) { + return null; + } + + if ($asString) { + trigger_error( + 'The Default value will become of type Expression by default', + E_USER_DEPRECATED, + ); + + return (string) $this->default; + } + return $this->default; } diff --git a/tests/integration/FileDocblockTest.php b/tests/integration/FileDocblockTest.php index d6fa98d6..7e64e1c1 100644 --- a/tests/integration/FileDocblockTest.php +++ b/tests/integration/FileDocblockTest.php @@ -11,6 +11,8 @@ /** * Integration tests to check the correct working of processing a namespace into a project. + * + * @coversNothing */ #[CoversNothing] final class FileDocblockTest extends TestCase diff --git a/tests/integration/PHP8/ConstructorPromotionTest.php b/tests/integration/PHP8/ConstructorPromotionTest.php index 46e3b84f..236ab092 100644 --- a/tests/integration/PHP8/ConstructorPromotionTest.php +++ b/tests/integration/PHP8/ConstructorPromotionTest.php @@ -4,6 +4,7 @@ namespace integration\PHP8; +use DateTimeImmutable; use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlock\Tags\Param; use phpDocumentor\Reflection\DocBlock\Tags\Var_; @@ -11,11 +12,13 @@ use phpDocumentor\Reflection\Fqsen; use phpDocumentor\Reflection\Location; use phpDocumentor\Reflection\Php\Argument; +use phpDocumentor\Reflection\Php\Expression; use phpDocumentor\Reflection\Php\Method; -use phpDocumentor\Reflection\Php\ProjectFactory; use phpDocumentor\Reflection\Php\Project; +use phpDocumentor\Reflection\Php\ProjectFactory; use phpDocumentor\Reflection\Php\Property; use phpDocumentor\Reflection\Php\Visibility; +use phpDocumentor\Reflection\Types\Array_; use phpDocumentor\Reflection\Types\Context; use phpDocumentor\Reflection\Types\Object_; use phpDocumentor\Reflection\Types\String_; @@ -26,12 +29,10 @@ */ class ConstructorPromotionTest extends TestCase { - const FILE = __DIR__ . '/../data/PHP8/ConstructorPromotion.php'; - /** @var ProjectFactory */ - private $fixture; + private const FILE = __DIR__ . '/../data/PHP8/ConstructorPromotion.php'; - /** @var Project */ - private $project; + private ProjectFactory $fixture; + private Project $project; protected function setUp() : void { @@ -44,30 +45,63 @@ protected function setUp() : void ); } - public function testPropertiesAreCreated() : void + public function testArgumentsAreReadCorrectly() : void { $file = $this->project->getFiles()[self::FILE]; $class = $file->getClasses()['\\PHP8\\ConstructorPromotion']; - $constructor = $this->expectedContructorMethod(); + $constructor = $this->expectedConstructorMethod(); $constructor->addArgument(new Argument('name', new String_(), "'default name'")); $constructor->addArgument(new Argument('email', new Object_(new Fqsen('\\PHP8\\Email')))); - $constructor->addArgument(new Argument('birth_date', new Object_(new Fqsen('\\' . \DateTimeImmutable::class)))); + $constructor->addArgument(new Argument('birth_date', new Object_(new Fqsen('\\' . DateTimeImmutable::class)))); + $constructor->addArgument( + new Argument( + 'created_at', + new Object_(new Fqsen('\\' . DateTimeImmutable::class)), + new Expression( + 'new {{ PHPDOC6ffacd918e2f70478d2fd33dcb58c4d4 }}(\'now\')', + [ + '{{ PHPDOC6ffacd918e2f70478d2fd33dcb58c4d4 }}' => new Fqsen('\\DateTimeImmutable') + ] + ) + ) + ); + $constructor->addArgument( + new Argument( + 'uses_constants', + new Array_(), + new Expression( + '[{{ PHPDOC19b72d1f430d952a8dfe2384dd4e93dc }}]', + [ + '{{ PHPDOC19b72d1f430d952a8dfe2384dd4e93dc }}' => new Fqsen('\PHP8\ConstructorPromotion::DEFAULT_VALUE'), + ], + ), + ), + ); self::assertEquals($constructor, $class->getMethods()['\PHP8\ConstructorPromotion::__construct()']); + } + + public function testPropertiesAreCreated() : void + { + $file = $this->project->getFiles()[self::FILE]; + $class = $file->getClasses()['\\PHP8\\ConstructorPromotion']; + self::assertEquals( [ '\PHP8\ConstructorPromotion::$name' => $this->expectedNameProperty(), '\PHP8\ConstructorPromotion::$email' => $this->expectedEmailProperty(), - '\PHP8\ConstructorPromotion::$birth_date' => $this->expectedBirthDateProperty() + '\PHP8\ConstructorPromotion::$birth_date' => $this->expectedBirthDateProperty(), + '\PHP8\ConstructorPromotion::$created_at' => $this->expectedCreatedAtProperty(), + '\PHP8\ConstructorPromotion::$uses_constants' => $this->expectedUsesConstantsProperty(), ], $class->getProperties() ); } - private function expectedContructorMethod(): Method + private function expectedConstructorMethod(): Method { - $constructor = new Method( + return new Method( new Fqsen('\PHP8\ConstructorPromotion::__construct()'), new Visibility(Visibility::PUBLIC_), new DocBlock( @@ -86,15 +120,14 @@ private function expectedContructorMethod(): Method false, false, false, - new Location(16, 218), - new Location(27, 517) + new Location(18, 264), + new Location(31, 704) ); - return $constructor; } private function expectedNameProperty(): Property { - $name = new Property( + return new Property( new Fqsen('\PHP8\ConstructorPromotion::$name'), new Visibility(Visibility::PUBLIC_), new DocBlock( @@ -107,40 +140,75 @@ private function expectedNameProperty(): Property ), "'default name'", false, - new Location(24, 393), - new Location(24, 428), + new Location(26), + new Location(26), new String_() ); - return $name; } private function expectedEmailProperty(): Property { - $email = new Property( + return new Property( new Fqsen('\PHP8\ConstructorPromotion::$email'), new Visibility(Visibility::PROTECTED_), null, null, false, - new Location(25, 439), - new Location(25, 460), - new Object_(new Fqsen('\\PHP8\\Email')) + new Location(27), + new Location(27), + New Object_(new Fqsen('\\PHP8\\Email')), ); - return $email; } private function expectedBirthDateProperty(): Property { - $birthDate = new Property( + return new Property( new Fqsen('\PHP8\ConstructorPromotion::$birth_date'), new Visibility(Visibility::PRIVATE_), null, null, false, - new Location(26, 471), - new Location(26, 507), - new Object_(new Fqsen('\\' . \DateTimeImmutable::class)) + new Location(28), + new Location(28), + new Object_(new Fqsen('\\' . DateTimeImmutable::class)) + ); + } + + private function expectedCreatedAtProperty(): Property + { + return new Property( + new Fqsen('\PHP8\ConstructorPromotion::$created_at'), + new Visibility(Visibility::PRIVATE_), + null, + new Expression( + 'new {{ PHPDOC6ffacd918e2f70478d2fd33dcb58c4d4 }}(\'now\')', + [ + '{{ PHPDOC6ffacd918e2f70478d2fd33dcb58c4d4 }}' => new Fqsen('\\DateTimeImmutable'), + ], + ), + false, + new Location(29), + new Location(29), + new Object_(new Fqsen('\\' . DateTimeImmutable::class)), + ); + } + + private function expectedUsesConstantsProperty() + { + return new Property( + new Fqsen('\PHP8\ConstructorPromotion::$uses_constants'), + new Visibility(Visibility::PRIVATE_), + null, + new Expression( + '[{{ PHPDOC19b72d1f430d952a8dfe2384dd4e93dc }}]', + [ + '{{ PHPDOC19b72d1f430d952a8dfe2384dd4e93dc }}' => new Fqsen('\PHP8\ConstructorPromotion::DEFAULT_VALUE'), + ], + ), + false, + new Location(30), + new Location(30), + new Array_(), ); - return $birthDate; } } diff --git a/tests/integration/data/PHP8/ConstructorPromotion.php b/tests/integration/data/PHP8/ConstructorPromotion.php index 494ddbe0..23eebcf4 100644 --- a/tests/integration/data/PHP8/ConstructorPromotion.php +++ b/tests/integration/data/PHP8/ConstructorPromotion.php @@ -8,6 +8,8 @@ class ConstructorPromotion { + private const DEFAULT_VALUE = 'default'; + /** * Constructor with promoted properties * @@ -24,5 +26,7 @@ public function __construct( public string $name = 'default name', protected Email $email, private DateTimeImmutable $birth_date, + private DateTimeImmutable $created_at = new DateTimeImmutable('now'), + private array $uses_constants = [self::DEFAULT_VALUE], ) {} } diff --git a/tests/unit/phpDocumentor/Reflection/Php/ArgumentTest.php b/tests/unit/phpDocumentor/Reflection/Php/ArgumentTest.php index a07de699..39f67da2 100644 --- a/tests/unit/phpDocumentor/Reflection/Php/ArgumentTest.php +++ b/tests/unit/phpDocumentor/Reflection/Php/ArgumentTest.php @@ -26,49 +26,50 @@ final class ArgumentTest extends TestCase { public function testGetTypes(): void { - $argument = new Argument('myArgument', null, 'myDefaultValue', true, true); - $this->assertInstanceOf(Mixed_::class, $argument->getType()); + $argument = new Argument('myArgument', null, new Expression('myDefaultValue'), true, true); + self::assertInstanceOf(Mixed_::class, $argument->getType()); $argument = new Argument( 'myArgument', new String_(), - 'myDefaultValue', + new Expression('myDefaultValue'), true, true, ); - $this->assertEquals(new String_(), $argument->getType()); + self::assertEquals(new String_(), $argument->getType()); } public function testGetName(): void { - $argument = new Argument('myArgument', null, 'myDefault', true, true); - $this->assertEquals('myArgument', $argument->getName()); + $argument = new Argument('myArgument', null, new Expression('myDefault'), true, true); + + self::assertEquals('myArgument', $argument->getName()); } public function testGetDefault(): void { - $argument = new Argument('myArgument', null, 'myDefaultValue', true, true); - $this->assertEquals('myDefaultValue', $argument->getDefault()); + $argument = new Argument('myArgument', null, new Expression('myDefaultValue'), true, true); + self::assertEquals(new Expression('myDefaultValue'), $argument->getDefault()); $argument = new Argument('myArgument', null, null, true, true); - $this->assertNull($argument->getDefault()); + self::assertNull($argument->getDefault()); } public function testGetWhetherArgumentIsPassedByReference(): void { - $argument = new Argument('myArgument', null, 'myDefaultValue', true, true); - $this->assertTrue($argument->isByReference()); + $argument = new Argument('myArgument', null, new Expression('myDefaultValue'), true, true); + self::assertTrue($argument->isByReference()); $argument = new Argument('myArgument', null, null, false, true); - $this->assertFalse($argument->isByReference()); + self::assertFalse($argument->isByReference()); } public function testGetWhetherArgumentisVariadic(): void { - $argument = new Argument('myArgument', null, 'myDefaultValue', true, true); - $this->assertTrue($argument->isVariadic()); + $argument = new Argument('myArgument', null, new Expression('myDefaultValue'), true, true); + self::assertTrue($argument->isVariadic()); - $argument = new Argument('myArgument', null, 'myDefaultValue', true, false); - $this->assertFalse($argument->isVariadic()); + $argument = new Argument('myArgument', null, new Expression('myDefaultValue'), true, false); + self::assertFalse($argument->isVariadic()); } } diff --git a/tests/unit/phpDocumentor/Reflection/Php/ConstantTest.php b/tests/unit/phpDocumentor/Reflection/Php/ConstantTest.php index 0fbed684..b0843d20 100644 --- a/tests/unit/phpDocumentor/Reflection/Php/ConstantTest.php +++ b/tests/unit/phpDocumentor/Reflection/Php/ConstantTest.php @@ -42,7 +42,7 @@ protected function setUp(): void { $this->fqsen = new Fqsen('\MySpace\CONSTANT'); $this->docBlock = new DocBlock(''); - $this->fixture = new Constant($this->fqsen, $this->docBlock, $this->value); + $this->fixture = new Constant($this->fqsen, $this->docBlock, new Expression($this->value)); } private function getFixture(): MetaDataContainerInterface @@ -52,28 +52,28 @@ private function getFixture(): MetaDataContainerInterface public function testGetValue(): void { - $this->assertSame($this->value, $this->fixture->getValue()); + self::assertEquals(new Expression($this->value), $this->fixture->getValue()); } public function testIsFinal(): void { - $this->assertFalse($this->fixture->isFinal()); + self::assertFalse($this->fixture->isFinal()); } public function testGetFqsen(): void { - $this->assertSame($this->fqsen, $this->fixture->getFqsen()); - $this->assertSame($this->fqsen->getName(), $this->fixture->getName()); + self::assertSame($this->fqsen, $this->fixture->getFqsen()); + self::assertSame($this->fqsen->getName(), $this->fixture->getName()); } public function testGetDocblock(): void { - $this->assertSame($this->docBlock, $this->fixture->getDocBlock()); + self::assertSame($this->docBlock, $this->fixture->getDocBlock()); } public function testGetVisibility(): void { - $this->assertEquals(new Visibility(Visibility::PUBLIC_), $this->fixture->getVisibility()); + self::assertEquals(new Visibility(Visibility::PUBLIC_), $this->fixture->getVisibility()); } public function testLineAndColumnNumberIsReturnedWhenALocationIsProvided(): void diff --git a/tests/unit/phpDocumentor/Reflection/Php/EnumCaseTest.php b/tests/unit/phpDocumentor/Reflection/Php/EnumCaseTest.php index 11267ac2..7fc536d8 100644 --- a/tests/unit/phpDocumentor/Reflection/Php/EnumCaseTest.php +++ b/tests/unit/phpDocumentor/Reflection/Php/EnumCaseTest.php @@ -32,14 +32,18 @@ final class EnumCaseTest extends TestCase private DocBlock $docBlock; /** - * Creates a new (emoty) fixture object. + * Creates a new (empty) fixture object. */ protected function setUp(): void { $this->fqsen = new Fqsen('\Enum::VALUE'); $this->docBlock = new DocBlock(''); - $this->fixture = new EnumCase($this->fqsen, $this->docBlock); + // needed for MetaDataContainer testing + $this->fixture = new EnumCase( + $this->fqsen, + $this->docBlock, + ); } private function getFixture(): MetaDataContainerInterface @@ -49,26 +53,133 @@ private function getFixture(): MetaDataContainerInterface public function testGettingName(): void { - $this->assertSame($this->fqsen->getName(), $this->fixture->getName()); + $fixture = new EnumCase( + $this->fqsen, + $this->docBlock, + ); + + $this->assertSame($this->fqsen->getName(), $fixture->getName()); } public function testGettingFqsen(): void { - $this->assertSame($this->fqsen, $this->fixture->getFqsen()); + $fixture = new EnumCase( + $this->fqsen, + $this->docBlock, + ); + + $this->assertSame($this->fqsen, $fixture->getFqsen()); } public function testGettingDocBlock(): void { - $this->assertSame($this->docBlock, $this->fixture->getDocBlock()); + $fixture = new EnumCase( + $this->fqsen, + $this->docBlock, + ); + + $this->assertSame($this->docBlock, $fixture->getDocBlock()); + } + + /** @covers ::getValue */ + public function testValueCanBeOmitted(): void + { + $fixture = new EnumCase( + $this->fqsen, + $this->docBlock, + ); + + $this->assertNull($fixture->getValue()); + } + + /** + * @uses Expression + * + * @covers ::getValue + */ + public function testValueCanBeProvidedAsAnExpression(): void + { + $expression = new Expression('Enum case expression'); + $fixture = new EnumCase( + $this->fqsen, + $this->docBlock, + null, + null, + $expression, + ); + + $this->assertSame($expression, $fixture->getValue(false)); } - public function testGetValue(): void + /** + * @uses Expression + * + * @covers ::getValue + */ + public function testValueCanBeReturnedAsString(): void { - $this->assertNull($this->fixture->getValue()); + $expression = new Expression('Enum case expression'); + $fixture = new EnumCase( + $this->fqsen, + $this->docBlock, + null, + null, + $expression, + ); + + $this->assertSame('Enum case expression', $fixture->getValue(true)); } - public function testGetLocationReturnsDefault(): void + /** @covers ::getLocation */ + public function testGetLocationReturnsProvidedValue(): void { - self::assertEquals(new Location(-1), $this->fixture->getLocation()); + $location = new Location(15, 10); + $fixture = new EnumCase( + $this->fqsen, + $this->docBlock, + $location, + ); + + self::assertSame($location, $fixture->getLocation()); + } + + /** + * @uses Location + * + * @covers ::getLocation + */ + public function testGetLocationReturnsUnknownByDefault(): void + { + $fixture = new EnumCase( + $this->fqsen, + $this->docBlock, + ); + + self::assertEquals(new Location(-1), $fixture->getLocation()); + } + + /** @covers ::getEndLocation */ + public function testGetEndLocationReturnsProvidedValue(): void + { + $location = new Location(11, 23); + $fixture = new EnumCase( + $this->fqsen, + $this->docBlock, + null, + $location, + ); + + self::assertSame($location, $fixture->getEndLocation()); + } + + /** @covers ::getEndLocation */ + public function testGetEndLocationReturnsUnknownByDefault(): void + { + $fixture = new EnumCase( + $this->fqsen, + $this->docBlock, + ); + + self::assertEquals(new Location(-1), $fixture->getEndLocation()); } } diff --git a/tests/unit/phpDocumentor/Reflection/Php/ExpressionTest.php b/tests/unit/phpDocumentor/Reflection/Php/ExpressionTest.php new file mode 100644 index 00000000..33e4cc93 --- /dev/null +++ b/tests/unit/phpDocumentor/Reflection/Php/ExpressionTest.php @@ -0,0 +1,131 @@ + + */ +final class ExpressionTest extends TestCase +{ + private const EXAMPLE_FQSEN = '\\' . self::class; + private const EXAMPLE_FQSEN_PLACEHOLDER = '{{ PHPDOC0450ed2a7bac1efcf0c13b6560767954 }}'; + + /** @covers ::generatePlaceholder */ + public function testGeneratingPlaceholder(): void + { + $placeholder = Expression::generatePlaceholder(self::EXAMPLE_FQSEN); + + self::assertSame(self::EXAMPLE_FQSEN_PLACEHOLDER, $placeholder); + } + + /** @covers ::generatePlaceholder */ + public function testGeneratingPlaceholderErrorsUponPassingAnEmptyName(): void + { + $this->expectException(InvalidArgumentException::class); + + Expression::generatePlaceholder(''); + } + + /** @covers ::__construct */ + public function testExpressionTemplateCannotBeEmpty(): void + { + $this->expectException(InvalidArgumentException::class); + + new Expression('', []); + } + + /** @covers ::__construct */ + public function testPartsShouldContainFqsensOrTypes(): void + { + $this->expectException(InvalidArgumentException::class); + + new Expression('This is an expression', [self::EXAMPLE_FQSEN_PLACEHOLDER => self::EXAMPLE_FQSEN]); + } + + /** + * @covers ::__construct + * @covers ::getExpression + */ + public function testGetExpressionTemplateString(): void + { + $expressionTemplate = sprintf('This is an %s expression', self::EXAMPLE_FQSEN_PLACEHOLDER); + $parts = [self::EXAMPLE_FQSEN_PLACEHOLDER => new Fqsen(self::EXAMPLE_FQSEN)]; + $expression = new Expression($expressionTemplate, $parts); + + $result = $expression->getExpression(); + + self::assertSame($expressionTemplate, $result); + } + + /** + * @covers ::__construct + * @covers ::getParts + */ + public function testGetExtractedParts(): void + { + $expressionTemplate = sprintf('This is an %s expression', self::EXAMPLE_FQSEN_PLACEHOLDER); + $parts = [self::EXAMPLE_FQSEN_PLACEHOLDER => new Fqsen(self::EXAMPLE_FQSEN)]; + $expression = new Expression($expressionTemplate, $parts); + + $result = $expression->getParts(); + + self::assertSame($parts, $result); + } + + /** @covers ::__toString */ + public function testReplacePlaceholdersWhenCastingToString(): void + { + $expressionTemplate = sprintf('This is an %s expression', self::EXAMPLE_FQSEN_PLACEHOLDER); + $parts = [self::EXAMPLE_FQSEN_PLACEHOLDER => new Fqsen(self::EXAMPLE_FQSEN)]; + $expression = new Expression($expressionTemplate, $parts); + + $result = (string) $expression; + + self::assertSame(sprintf('This is an %s expression', self::EXAMPLE_FQSEN), $result); + } + + /** @covers ::render */ + public function testRenderingExpressionWithoutOverridesIsTheSameAsWhenCastingToString(): void + { + $expressionTemplate = sprintf('This is an %s expression', self::EXAMPLE_FQSEN_PLACEHOLDER); + $parts = [self::EXAMPLE_FQSEN_PLACEHOLDER => new Fqsen(self::EXAMPLE_FQSEN)]; + $expression = new Expression($expressionTemplate, $parts); + + $result = $expression->render(); + + self::assertSame((string) $expression, $result); + } + + /** @covers ::render */ + public function testOverridePartsWhenRenderingExpression(): void + { + $replacement = 'ExpressionTest'; + + $expressionTemplate = sprintf('This is an %s expression', self::EXAMPLE_FQSEN_PLACEHOLDER); + $parts = [self::EXAMPLE_FQSEN_PLACEHOLDER => new Fqsen(self::EXAMPLE_FQSEN)]; + $expression = new Expression($expressionTemplate, $parts); + + $result = $expression->render([self::EXAMPLE_FQSEN_PLACEHOLDER => $replacement]); + + self::assertSame(sprintf('This is an %s expression', $replacement), $result); + } +} diff --git a/tests/unit/phpDocumentor/Reflection/Php/Factory/PropertyTest.php b/tests/unit/phpDocumentor/Reflection/Php/Factory/PropertyTest.php index 42c5a722..dee73daf 100644 --- a/tests/unit/phpDocumentor/Reflection/Php/Factory/PropertyTest.php +++ b/tests/unit/phpDocumentor/Reflection/Php/Factory/PropertyTest.php @@ -63,9 +63,9 @@ public function testMatches(): void #[DataProvider('visibilityProvider')] public function testCreateWithVisibility(int $input, string $expectedVisibility): void { - $constantStub = $this->buildPropertyMock($input); + $propertyStub = $this->buildPropertyMock($input); - $class = $this->performCreate($constantStub); + $class = $this->performCreate($propertyStub); $property = current($class->getProperties()); $this->assertProperty($property, $expectedVisibility); diff --git a/tests/unit/phpDocumentor/Reflection/Php/PropertyTest.php b/tests/unit/phpDocumentor/Reflection/Php/PropertyTest.php index 44a7d5ef..6dc29b43 100644 --- a/tests/unit/phpDocumentor/Reflection/Php/PropertyTest.php +++ b/tests/unit/phpDocumentor/Reflection/Php/PropertyTest.php @@ -56,23 +56,23 @@ public function testGetFqsenAndGetName(): void { $property = new Property($this->fqsen); - $this->assertSame($this->fqsen, $property->getFqsen()); - $this->assertEquals($this->fqsen->getName(), $property->getName()); + self::assertSame($this->fqsen, $property->getFqsen()); + self::assertEquals($this->fqsen->getName(), $property->getName()); } public function testGettingWhetherPropertyIsStatic(): void { $property = new Property($this->fqsen, $this->visibility, $this->docBlock, null, false); - $this->assertFalse($property->isStatic()); + self::assertFalse($property->isStatic()); $property = new Property($this->fqsen, $this->visibility, $this->docBlock, null, true); - $this->assertTrue($property->isStatic()); + self::assertTrue($property->isStatic()); } public function testGettingWhetherPropertyIsReadOnly(): void { $property = new Property($this->fqsen, $this->visibility, $this->docBlock, null); - $this->assertFalse($property->isReadOnly()); + self::assertFalse($property->isReadOnly()); $property = new Property( $this->fqsen, @@ -86,38 +86,40 @@ public function testGettingWhetherPropertyIsReadOnly(): void true, ); - $this->assertTrue($property->isReadOnly()); + self::assertTrue($property->isReadOnly()); } public function testGettingVisibility(): void { $property = new Property($this->fqsen, $this->visibility, $this->docBlock, null, true); - $this->assertSame($this->visibility, $property->getVisibility()); + self::assertSame($this->visibility, $property->getVisibility()); } public function testSetAndGetTypes(): void { $property = new Property($this->fqsen, $this->visibility, $this->docBlock, null, true); - $this->assertEquals([], $property->getTypes()); + self::assertEquals([], $property->getTypes()); $property->addType('a'); - $this->assertEquals(['a'], $property->getTypes()); + self::assertEquals(['a'], $property->getTypes()); } public function testGetDefault(): void { $property = new Property($this->fqsen, $this->visibility, $this->docBlock, null, false); - $this->assertNull($property->getDefault()); + self::assertNull($property->getDefault()); - $property = new Property($this->fqsen, $this->visibility, $this->docBlock, 'a', true); - $this->assertEquals('a', $property->getDefault()); + $expression = new Expression('a'); + $property = new Property($this->fqsen, $this->visibility, $this->docBlock, $expression, true); + self::assertSame('a', $property->getDefault()); + self::assertSame($expression, $property->getDefault(false)); } public function testGetDocBlock(): void { $property = new Property($this->fqsen, $this->visibility, $this->docBlock, null, false); - $this->assertSame($this->docBlock, $property->getDocBlock()); + self::assertSame($this->docBlock, $property->getDocBlock()); } public function testLineAndColumnNumberIsReturnedWhenALocationIsProvided(): void @@ -140,9 +142,9 @@ public function testGetType(): void $type, ); - $this->assertSame($type, $fixture->getType()); + self::assertSame($type, $fixture->getType()); $fixture = new Property($this->fqsen); - $this->assertNull($fixture->getType()); + self::assertNull($fixture->getType()); } }