diff --git a/Makefile b/Makefile index 1aab96f2..63707afb 100644 --- a/Makefile +++ b/Makefile @@ -16,16 +16,16 @@ static-code-analysis: vendor ## Runs a static code analysis with phpstan/phpstan docker run -it --rm -v${PWD}:/opt/project -w /opt/project php:7.4 vendor/bin/psalm .PHONY: test -test: test-unit test-functional ## Runs all test suites with phpunit/phpunit +test: test-unit test-integration ## Runs all test suites with phpunit/phpunit docker run -it --rm -v${PWD}:/opt/project -w /opt/project php:7.4 vendor/bin/phpunit .PHONY: test-unit test-unit: ## Runs unit tests with phpunit/phpunit docker run -it --rm -v${PWD}:/opt/project -w /opt/project php:7.4 vendor/bin/phpunit --testsuite=unit -.PHONY: test-functional -test-functional: ## Runs unit tests with phpunit/phpunit - docker run -it --rm -v${PWD}:/opt/project -w /opt/project php:7.4 vendor/bin/phpunit --testsuite=functional +.PHONY: test-integration +test-integration: ## Runs unit tests with phpunit/phpunit + docker run -it --rm -v${PWD}:/opt/project -w /opt/project php:7.4 vendor/bin/phpunit --testsuite=integration .PHONY: dependency-analysis dependency-analysis: vendor ## Runs a dependency analysis with maglnet/composer-require-checker 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/filtering.rst b/docs/filtering.rst deleted file mode 100644 index e8425d00..00000000 --- a/docs/filtering.rst +++ /dev/null @@ -1,4 +0,0 @@ -Filtering -========= - -.. info:: The contents for this page are planned but need to be written, please come back later to check for this \ No newline at end of file diff --git a/docs/incremental-updates.rst b/docs/incremental-updates.rst deleted file mode 100644 index 8f83164b..00000000 --- a/docs/incremental-updates.rst +++ /dev/null @@ -1,4 +0,0 @@ -Incremental updates -=================== - -.. info:: The contents for this page are planned but need to be written, please come back later to check for this \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index d4f2f984..59d0b765 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,6 +2,5 @@ Reflection ========== .. toctree:: - :hidden: - + expressions meta-data diff --git a/docs/inspecting.rst b/docs/inspecting.rst deleted file mode 100644 index 660040f2..00000000 --- a/docs/inspecting.rst +++ /dev/null @@ -1,4 +0,0 @@ -Inspecting -========== - -.. info:: The contents for this page are planned but need to be written, please come back later to check for this \ No newline at end of file diff --git a/docs/integrating-with-silex-and-cilex.rst b/docs/integrating-with-silex-and-cilex.rst deleted file mode 100644 index 4b80a411..00000000 --- a/docs/integrating-with-silex-and-cilex.rst +++ /dev/null @@ -1,4 +0,0 @@ -Integrating with Silex and Cilex -================================ - -.. info:: The contents for this page are planned but need to be written, please come back later to check for this \ No newline at end of file diff --git a/docs/usage.rst b/docs/usage.rst deleted file mode 100644 index 35474b2a..00000000 --- a/docs/usage.rst +++ /dev/null @@ -1,4 +0,0 @@ -Usage -===== - -.. info:: The contents for this page are planned but need to be written, please come back later to check for this \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon index d49d462e..f2b4e184 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -17,10 +17,6 @@ parameters: - '#Parameter \#1 \$fqsen of class phpDocumentor\\Reflection\\Php\\(.*) constructor expects phpDocumentor\\Reflection\\Fqsen, mixed given\.#' - '#Parameter \#1 \$fqsen of method phpDocumentor\\Reflection\\Php\\File::addNamespace\(\) expects phpDocumentor\\Reflection\\Fqsen, mixed given\.#' # - # there is one test case that prevents changing PropertyIterator::getDefault() to just return Expr (this is set in PhpParser) - # src/phpDocumentor/Reflection/Php/Factory/Property.php - - '#Parameter \#1 \$node of method PhpParser\\PrettyPrinterAbstract::prettyPrintExpr\(\) expects PhpParser\\Node\\Expr, PhpParser\\Node\\Expr\|string given\.#' - # # Type hint in php-parser is incorrect. - '#Cannot cast PhpParser\\Node\\Expr\|string to string.#' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 235ec710..57d87416 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -21,6 +21,9 @@ ./tests/integration + + + ./src/ @@ -35,8 +38,8 @@ diff --git a/psalm.xml b/psalm.xml index 8b73b240..e49273fe 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 8a0669cd..81b2a0cc 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. */ @@ -27,8 +32,8 @@ final class Argument /** @var Type a normalized type that should be in this Argument */ private Type $type; - /** @var string|null the default value for an argument or null if none is provided */ - private ?string $default; + /** @var Expression|null the default value for an argument or null if none is provided */ + private ?Expression $default; /** @var bool whether the argument passes the parameter by reference instead of by value */ private bool $byReference; @@ -38,22 +43,34 @@ final class Argument /** * Initializes the object. + * + * @param string|Expression|null $default */ public function __construct( string $name, ?Type $type = null, - ?string $default = null, + $default = null, bool $byReference = false, bool $isVariadic = false ) { $this->name = $name; - $this->default = $default; $this->byReference = $byReference; $this->isVariadic = $isVariadic; if ($type === null) { $type = new Mixed_(); } + if (is_string($default)) { + trigger_error( + 'Default values for arguments should be of type Expression, support for strings will be ' + . 'removed in 6.x', + E_USER_DEPRECATED + ); + $default = new Expression($default, []); + } + + $this->default = $default; + $this->type = $type; } @@ -70,8 +87,24 @@ public function getType(): ?Type return $this->type; } - public function getDefault(): ?string + /** + * @return Expression|string|null + */ + public function getDefault(bool $asString = true) { + 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 8b95e652..ede8bc41 100644 --- a/src/phpDocumentor/Reflection/Php/Constant.php +++ b/src/phpDocumentor/Reflection/Php/Constant.php @@ -19,6 +19,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 */ @@ -30,7 +35,8 @@ final class Constant implements Element, MetaDataContainerInterface private ?DocBlock $docBlock; - private ?string $value; + /** @var string|Expression|null */ + private $value; private Location $location; @@ -42,11 +48,13 @@ final class Constant implements Element, MetaDataContainerInterface /** * Initializes the object. + * + * @param Expression|string|null $value */ public function __construct( Fqsen $fqsen, ?DocBlock $docBlock = null, - ?string $value = null, + $value = null, ?Location $location = null, ?Location $endLocation = null, ?Visibility $visibility = null, @@ -54,18 +62,43 @@ public function __construct( ) { $this->fqsen = $fqsen; $this->docBlock = $docBlock; - $this->value = $value; $this->location = $location ?: new Location(-1); $this->endLocation = $endLocation ?: new Location(-1); $this->visibility = $visibility ?: new Visibility(Visibility::PUBLIC_); $this->final = $final; + + if (is_string($value)) { + trigger_error( + 'Constant values should be of type Expression, support for strings will be ' + . 'removed in 6.x', + E_USER_DEPRECATED + ); + $value = new Expression($value, []); + } + + $this->value = $value; } /** - * Returns the value of this constant. + * Returns the expression value for this constant. + * + * @return Expression|string|null */ - public function getValue(): ?string + public function getValue(bool $asString = true) { + 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 0d32e369..2ff6f992 100644 --- a/src/phpDocumentor/Reflection/Php/EnumCase.php +++ b/src/phpDocumentor/Reflection/Php/EnumCase.php @@ -10,6 +10,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; + final class EnumCase implements Element, MetaDataContainerInterface { use MetadataContainer; @@ -22,14 +27,18 @@ final class EnumCase implements Element, MetaDataContainerInterface private Location $endLocation; - private ?string $value; + /** @var Expression|string|null */ + private $value; + /** + * @param Expression|string|null $value + */ public function __construct( Fqsen $fqsen, ?DocBlock $docBlock, ?Location $location = null, ?Location $endLocation = null, - ?string $value = null + $value = null ) { if ($location === null) { $location = new Location(-1); @@ -43,6 +52,15 @@ public function __construct( $this->docBlock = $docBlock; $this->location = $location; $this->endLocation = $endLocation; + if (is_string($value)) { + trigger_error( + 'Expression values for enum cases should be of type Expression, support for strings will be ' + . 'removed in 6.x', + E_USER_DEPRECATED + ); + $value = new Expression($value, []); + } + $this->value = $value; } @@ -71,8 +89,26 @@ public function getEndLocation(): Location return $this->endLocation; } - public function getValue(): ?string + /** + * Returns the value for this enum case. + * + * @return Expression|string|null + */ + public function getValue(bool $asString = true) { + 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..acbad7cc --- /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..3d52c188 --- /dev/null +++ b/src/phpDocumentor/Reflection/Php/Expression/ExpressionPrinter.php @@ -0,0 +1,60 @@ + */ + 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; + } + + /** + * @return array + */ + public function getParts(): array + { + return $this->parts; + } +} diff --git a/src/phpDocumentor/Reflection/Php/Factory/Argument.php b/src/phpDocumentor/Reflection/Php/Factory/Argument.php index e04daa3a..6d07b30e 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/Argument.php +++ b/src/phpDocumentor/Reflection/Php/Factory/Argument.php @@ -14,6 +14,8 @@ namespace phpDocumentor\Reflection\Php\Factory; use phpDocumentor\Reflection\Php\Argument as ArgumentDescriptor; +use phpDocumentor\Reflection\Php\Expression; +use phpDocumentor\Reflection\Php\Expression\ExpressionPrinter; use phpDocumentor\Reflection\Php\Function_; use phpDocumentor\Reflection\Php\Method; use phpDocumentor\Reflection\Php\ProjectFactoryStrategy; @@ -23,6 +25,8 @@ use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use Webmozart\Assert\Assert; +use function is_string; + /** * Strategy to convert Param to Argument * @@ -77,10 +81,28 @@ protected function doCreate( new ArgumentDescriptor( (string) $object->var->name, (new Type())->fromPhpParser($object->type), - $object->default !== null ? $this->valueConverter->prettyPrintExpr($object->default) : null, + $this->determineDefault($object), $object->byRef, $object->variadic ) ); } + + private function determineDefault(Param $value): ?Expression + { + $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/Factory/ClassConstant.php b/src/phpDocumentor/Reflection/Php/Factory/ClassConstant.php index 4d479ae8..75261de7 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/ClassConstant.php +++ b/src/phpDocumentor/Reflection/Php/Factory/ClassConstant.php @@ -18,6 +18,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\Interface_; use phpDocumentor\Reflection\Php\StrategyContainer; use phpDocumentor\Reflection\Php\Trait_; @@ -26,6 +28,8 @@ use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use Webmozart\Assert\Assert; +use function is_string; + /** * Strategy to convert ClassConstantIterator to ConstantElement * @@ -79,7 +83,7 @@ protected function doCreate( $constantContainer->addConstant(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), @@ -88,6 +92,20 @@ protected function doCreate( } } + 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 00e77718..ea0436ba 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/ConstructorPromotion.php +++ b/src/phpDocumentor/Reflection/Php/Factory/ConstructorPromotion.php @@ -9,6 +9,8 @@ use phpDocumentor\Reflection\Fqsen; use phpDocumentor\Reflection\Location; use phpDocumentor\Reflection\Php\Class_ as ClassElement; +use phpDocumentor\Reflection\Php\Expression; +use phpDocumentor\Reflection\Php\Expression\ExpressionPrinter; use phpDocumentor\Reflection\Php\ProjectFactoryStrategy; use phpDocumentor\Reflection\Php\Property; use phpDocumentor\Reflection\Php\StrategyContainer; @@ -20,6 +22,8 @@ use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use Webmozart\Assert\Assert; +use function is_string; + final class ConstructorPromotion extends AbstractFactory { private PrettyPrinter $valueConverter; @@ -72,7 +76,7 @@ private function promoteParameterToProperty(ContextStack $context, Param $param) new Fqsen($methodContainer->getFqsen() . '::$' . (string) $param->var->name), $this->buildPropertyVisibilty($param->flags), $this->createDocBlock($param->getDocComment(), $context->getTypeContext()), - $param->default !== null ? $this->valueConverter->prettyPrintExpr($param->default) : null, + $this->determineDefault($param), false, new Location($param->getLine()), new Location($param->getEndLine()), @@ -83,6 +87,24 @@ private function promoteParameterToProperty(ContextStack $context, Param $param) $methodContainer->addProperty($property); } + private function determineDefault(Param $value): ?Expression + { + $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; + } + private function buildPropertyVisibilty(int $flags): Visibility { if ((bool) ($flags & Class_::MODIFIER_PRIVATE) === true) { diff --git a/src/phpDocumentor/Reflection/Php/Factory/Define.php b/src/phpDocumentor/Reflection/Php/Factory/Define.php index dca55fbd..4178d3b8 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/Define.php +++ b/src/phpDocumentor/Reflection/Php/Factory/Define.php @@ -17,6 +17,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; @@ -30,6 +32,7 @@ use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use function assert; +use function is_string; use function sprintf; use function strpos; @@ -119,13 +122,22 @@ protected function doCreate( $file->addConstant($constant); } - private function determineValue(?Arg $value): ?string + private function determineValue(?Arg $value): ?ValueExpression { 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 diff --git a/src/phpDocumentor/Reflection/Php/Factory/EnumCase.php b/src/phpDocumentor/Reflection/Php/Factory/EnumCase.php index de419b42..c71b2b90 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/EnumCase.php +++ b/src/phpDocumentor/Reflection/Php/Factory/EnumCase.php @@ -8,11 +8,14 @@ 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\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 { @@ -21,6 +24,7 @@ final class EnumCase extends AbstractFactory public function __construct(DocBlockFactoryInterface $docBlockFactory, PrettyPrinter $prettyPrinter) { parent::__construct($docBlockFactory); + $this->prettyPrinter = $prettyPrinter; } @@ -37,12 +41,31 @@ protected function doCreate(ContextStack $context, object $object, StrategyConta $docBlock = $this->createDocBlock($object->getDocComment(), $context->getTypeContext()); $enum = $context->peek(); assert($enum instanceof EnumElement); + $enum->addCase(new EnumCaseElement( $object->getAttribute('fqsen'), $docBlock, new Location($object->getLine()), new Location($object->getEndLine()), - $object->expr !== null ? $this->prettyPrinter->prettyPrintExpr($object->expr) : null + $this->determineValue($object) )); } + + private function determineValue(EnumCaseNode $value): ?ValueExpression + { + $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 e2e0abd3..59900e66 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/GlobalConstant.php +++ b/src/phpDocumentor/Reflection/Php/Factory/GlobalConstant.php @@ -16,12 +16,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,11 +74,25 @@ 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()) ) ); } } + + 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 4be697d4..347f292a 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/Property.php +++ b/src/phpDocumentor/Reflection/Php/Factory/Property.php @@ -16,6 +16,8 @@ use phpDocumentor\Reflection\DocBlockFactoryInterface; use phpDocumentor\Reflection\Location; use phpDocumentor\Reflection\Php\Class_; +use phpDocumentor\Reflection\Php\Expression; +use phpDocumentor\Reflection\Php\Expression\ExpressionPrinter; use phpDocumentor\Reflection\Php\ProjectFactoryStrategy; use phpDocumentor\Reflection\Php\Property as PropertyDescriptor; use phpDocumentor\Reflection\Php\StrategyContainer; @@ -25,6 +27,8 @@ use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use Webmozart\Assert\Assert; +use function is_string; + /** * Strategy to convert PropertyIterator to PropertyDescriptor * @@ -74,17 +78,12 @@ protected function doCreate( $iterator = new PropertyIterator($object); foreach ($iterator as $stmt) { - $default = null; - if ($iterator->getDefault() !== null) { - $default = $this->valueConverter->prettyPrintExpr($iterator->getDefault()); - } - $propertyContainer->addProperty( new PropertyDescriptor( $stmt->getFqsen(), $this->buildVisibility($stmt), $this->createDocBlock($stmt->getDocComment(), $context->getTypeContext()), - $default, + $this->determineDefault($stmt), $stmt->isStatic(), new Location($stmt->getLine()), new Location($stmt->getEndLine()), @@ -95,6 +94,25 @@ protected function doCreate( } } + private function determineDefault(PropertyIterator $value): ?Expression + { + $default = $value->getDefault(); + $expression = $default !== null ? $this->valueConverter->prettyPrintExpr($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; + } + /** * Converts the visibility of the property to a valid Visibility object. */ diff --git a/src/phpDocumentor/Reflection/Php/Factory/PropertyIterator.php b/src/phpDocumentor/Reflection/Php/Factory/PropertyIterator.php index 6447c29d..bcc25c22 100644 --- a/src/phpDocumentor/Reflection/Php/Factory/PropertyIterator.php +++ b/src/phpDocumentor/Reflection/Php/Factory/PropertyIterator.php @@ -133,11 +133,9 @@ public function getName(): string } /** - * returns the default value of the current property. - * - * @return string|Expr|null + * Returns the default value of the current property. */ - public function getDefault() + public function getDefault(): ?Expr { return $this->property->props[$this->index]->default; } diff --git a/src/phpDocumentor/Reflection/Php/ProjectFactory.php b/src/phpDocumentor/Reflection/Php/ProjectFactory.php index fea139d0..49c4c7d6 100644 --- a/src/phpDocumentor/Reflection/Php/ProjectFactory.php +++ b/src/phpDocumentor/Reflection/Php/ProjectFactory.php @@ -17,6 +17,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\Argument; use phpDocumentor\Reflection\Php\Factory\Class_; use phpDocumentor\Reflection\Php\Factory\ClassConstant; @@ -30,13 +31,13 @@ use phpDocumentor\Reflection\Php\Factory\IfStatement; use phpDocumentor\Reflection\Php\Factory\Interface_; use phpDocumentor\Reflection\Php\Factory\Method; +use phpDocumentor\Reflection\Php\Factory\Namespace_ as NamespaceNode; use phpDocumentor\Reflection\Php\Factory\Noop; use phpDocumentor\Reflection\Php\Factory\Property; use phpDocumentor\Reflection\Php\Factory\Trait_; 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; @@ -65,24 +66,24 @@ public function __construct($strategies) public static function createInstance(): self { $docblockFactory = DocBlockFactory::createInstance(); - + $expressionPrinter = new ExpressionPrinter(); $methodStrategy = new Method($docblockFactory); $strategies = new ProjectFactoryStrategies( [ - new \phpDocumentor\Reflection\Php\Factory\Namespace_(), - new Argument(new PrettyPrinter()), + new NamespaceNode(), + new Argument($expressionPrinter), new Class_($docblockFactory), new Enum_($docblockFactory), - new EnumCase($docblockFactory, new PrettyPrinter()), - new Define($docblockFactory, new PrettyPrinter()), - new GlobalConstant($docblockFactory, new PrettyPrinter()), - new ClassConstant($docblockFactory, new PrettyPrinter()), + new EnumCase($docblockFactory, $expressionPrinter), + new Define($docblockFactory, $expressionPrinter), + new GlobalConstant($docblockFactory, $expressionPrinter), + new ClassConstant($docblockFactory, $expressionPrinter), new Factory\File($docblockFactory, NodesFactory::createInstance()), new Function_($docblockFactory), new Interface_($docblockFactory), $methodStrategy, - new Property($docblockFactory, new PrettyPrinter()), + new Property($docblockFactory, $expressionPrinter), new Trait_($docblockFactory), new IfStatement(), new TraitUse(), @@ -90,14 +91,12 @@ public static function createInstance(): self ); $strategies->addStrategy( - new ConstructorPromotion($methodStrategy, $docblockFactory, new PrettyPrinter()), + new ConstructorPromotion($methodStrategy, $docblockFactory, $expressionPrinter), 1100 ); $strategies->addStrategy(new Noop(), -PHP_INT_MAX); - return new static( - $strategies - ); + return new static($strategies); } public function addStrategy( diff --git a/src/phpDocumentor/Reflection/Php/Property.php b/src/phpDocumentor/Reflection/Php/Property.php index 326ab221..0a115a6c 100644 --- a/src/phpDocumentor/Reflection/Php/Property.php +++ b/src/phpDocumentor/Reflection/Php/Property.php @@ -20,6 +20,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. */ @@ -34,7 +39,7 @@ final class Property implements Element, MetaDataContainerInterface /** @var string[] $types */ private array $types = []; - private ?string $default = null; + private ?Expression $default = null; private bool $static = false; @@ -50,12 +55,13 @@ final class Property implements Element, MetaDataContainerInterface /** * @param Visibility|null $visibility when null is provided a default 'public' is set. + * @param string|Expression|null $default */ public function __construct( Fqsen $fqsen, ?Visibility $visibility = null, ?DocBlock $docBlock = null, - ?string $default = null, + $default = null, bool $static = false, ?Location $location = null, ?Location $endLocation = null, @@ -65,19 +71,44 @@ public function __construct( $this->fqsen = $fqsen; $this->visibility = $visibility ?: new Visibility('public'); $this->docBlock = $docBlock; - $this->default = $default; $this->static = $static; $this->location = $location ?: new Location(-1); $this->endLocation = $endLocation ?: new Location(-1); $this->type = $type; $this->readOnly = $readOnly; + + if (is_string($default)) { + trigger_error( + 'Default values for properties should be of type Expression, support for strings will be ' + . 'removed in 6.x', + E_USER_DEPRECATED + ); + $default = new Expression($default, []); + } + + $this->default = $default; } /** - * returns the default value of this property. + * Returns the default value for this property. + * + * @return Expression|string|null */ - public function getDefault(): ?string + public function getDefault(bool $asString = true) { + 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 e33edb27..a6af1131 100644 --- a/tests/integration/FileDocblockTest.php +++ b/tests/integration/FileDocblockTest.php @@ -10,6 +10,8 @@ /** * Integration tests to check the correct working of processing a namespace into a project. + * + * @coversNothing */ final class FileDocblockTest extends TestCase { diff --git a/tests/integration/PHP8/ConstructorPromotionTest.php b/tests/integration/PHP8/ConstructorPromotionTest.php index d14b435a..ae377f0a 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\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,62 @@ 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_())); $constructor->addArgument(new Argument('email', new String_(), '\'test@example.com\'')); - $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( + '[{{ PHPDOC590f53e8699817c6fa498cc11a4cbe63 }}]', + [ + '{{ PHPDOC590f53e8699817c6fa498cc11a4cbe63 }}' => 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(), ], $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 +119,14 @@ private function expectedContructorMethod(): Method false, false, false, - new Location(16, 218), - new Location(27, 522) + new Location(18, 264), + new Location(31, 709) ); - 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 +139,56 @@ private function expectedNameProperty(): Property ), null, false, - new Location(24), - new Location(24), + 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, '\'test@example.com\'', false, - new Location(25), - new Location(25), + new Location(27), + new Location(27), new String_() ); - 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), - new Location(26), - 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)) ); - return $birthDate; } } diff --git a/tests/integration/data/PHP8/ConstructorPromotion.php b/tests/integration/data/PHP8/ConstructorPromotion.php index cc1883c6..1ccf68ac 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, protected string $email = 'test@example.com', 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 38478a10..d44f6f28 100644 --- a/tests/unit/phpDocumentor/Reflection/Php/ArgumentTest.php +++ b/tests/unit/phpDocumentor/Reflection/Php/ArgumentTest.php @@ -32,17 +32,17 @@ 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()); } /** @@ -50,8 +50,9 @@ public function testGetTypes(): void */ 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()); } /** @@ -59,11 +60,11 @@ public function testGetName(): void */ 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()); } /** @@ -71,11 +72,11 @@ public function testGetDefault(): void */ 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()); } /** @@ -83,10 +84,10 @@ public function testGetWhetherArgumentIsPassedByReference(): void */ 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 40cf4144..3717ce84 100644 --- a/tests/unit/phpDocumentor/Reflection/Php/ConstantTest.php +++ b/tests/unit/phpDocumentor/Reflection/Php/ConstantTest.php @@ -19,9 +19,9 @@ use phpDocumentor\Reflection\Metadata\MetaDataContainer as MetaDataContainerInterface; /** - * @uses \phpDocumentor\Reflection\DocBlock - * @uses \phpDocumentor\Reflection\Php\Visibility - * @uses \phpDocumentor\Reflection\Fqsen + * @uses DocBlock + * @uses Visibility + * @uses Fqsen * * @coversDefaultClass \phpDocumentor\Reflection\Php\Constant * @covers ::__construct @@ -46,7 +46,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 @@ -60,7 +60,7 @@ private function getFixture(): MetaDataContainerInterface */ public function testGetValue(): void { - $this->assertSame($this->value, $this->fixture->getValue()); + self::assertEquals(new Expression($this->value), $this->fixture->getValue()); } /** @@ -69,7 +69,7 @@ public function testGetValue(): void */ public function testIsFinal(): void { - $this->assertFalse($this->fixture->isFinal()); + self::assertFalse($this->fixture->isFinal()); } /** @@ -78,8 +78,8 @@ public function testIsFinal(): void */ 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()); } /** @@ -87,7 +87,7 @@ public function testGetFqsen(): void */ public function testGetDocblock(): void { - $this->assertSame($this->docBlock, $this->fixture->getDocBlock()); + self::assertSame($this->docBlock, $this->fixture->getDocBlock()); } /** @@ -95,7 +95,7 @@ public function testGetDocblock(): void */ 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 d2f4dabe..09e2583a 100644 --- a/tests/unit/phpDocumentor/Reflection/Php/EnumCaseTest.php +++ b/tests/unit/phpDocumentor/Reflection/Php/EnumCaseTest.php @@ -36,14 +36,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 @@ -56,7 +60,12 @@ 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()); } /** @@ -64,7 +73,12 @@ public function testGettingName(): void */ public function testGettingFqsen(): void { - $this->assertSame($this->fqsen, $this->fixture->getFqsen()); + $fixture = new EnumCase( + $this->fqsen, + $this->docBlock + ); + + $this->assertSame($this->fqsen, $fixture->getFqsen()); } /** @@ -72,22 +86,121 @@ public function testGettingFqsen(): void */ 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 testGetValue(): void + 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)); + } + + /** + * @uses Expression + * + * @covers ::getValue + */ + public function testValueCanBeReturnedAsString(): void + { + $expression = new Expression('Enum case expression'); + $fixture = new EnumCase( + $this->fqsen, + $this->docBlock, + null, + null, + $expression + ); + + $this->assertSame('Enum case expression', $fixture->getValue(true)); + } + + /** + * @covers ::getLocation + */ + public function testGetLocationReturnsProvidedValue(): void { - $this->assertNull($this->fixture->getValue()); + $location = new Location(15, 10); + $fixture = new EnumCase( + $this->fqsen, + $this->docBlock, + $location + ); + + self::assertSame($location, $fixture->getLocation()); } /** + * @uses Location + * * @covers ::getLocation */ - public function testGetLocationReturnsDefault(): void + public function testGetLocationReturnsUnknownByDefault(): void { - self::assertEquals(new Location(-1), $this->fixture->getLocation()); + $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..9d290216 --- /dev/null +++ b/tests/unit/phpDocumentor/Reflection/Php/ExpressionTest.php @@ -0,0 +1,145 @@ + + */ +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/ArgumentTest.php b/tests/unit/phpDocumentor/Reflection/Php/Factory/ArgumentTest.php index 2cae57cf..c1cc9882 100644 --- a/tests/unit/phpDocumentor/Reflection/Php/Factory/ArgumentTest.php +++ b/tests/unit/phpDocumentor/Reflection/Php/Factory/ArgumentTest.php @@ -16,12 +16,12 @@ use Mockery as m; use phpDocumentor\Reflection\Fqsen; use phpDocumentor\Reflection\Php\Argument as ArgumentDescriptor; +use phpDocumentor\Reflection\Php\Expression\ExpressionPrinter; use phpDocumentor\Reflection\Php\Method as MethodElement; use phpDocumentor\Reflection\Php\ProjectFactoryStrategies; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Param; use PhpParser\Node\Scalar\String_; -use PhpParser\PrettyPrinter\Standard as PrettyPrinter; use stdClass; /** @@ -40,7 +40,7 @@ class ArgumentTest extends TestCase { protected function setUp(): void { - $this->fixture = new Argument(new PrettyPrinter()); + $this->fixture = new Argument(new ExpressionPrinter()); } /** diff --git a/tests/unit/phpDocumentor/Reflection/Php/Factory/PropertyIteratorTest.php b/tests/unit/phpDocumentor/Reflection/Php/Factory/PropertyIteratorTest.php index ce1d1945..3dc59115 100644 --- a/tests/unit/phpDocumentor/Reflection/Php/Factory/PropertyIteratorTest.php +++ b/tests/unit/phpDocumentor/Reflection/Php/Factory/PropertyIteratorTest.php @@ -16,6 +16,7 @@ use Mockery as m; use Mockery\Adapter\Phpunit\MockeryTestCase; use PhpParser\Comment\Doc; +use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Property as PropertyNode; use PhpParser\Node\Stmt\PropertyProperty; @@ -98,12 +99,12 @@ public function testProxyMethods(): void public function testGetDefault(): void { $prop = m::mock(PropertyProperty::class); - $prop->default = 'myDefault'; + $prop->default = new String_('myDefault'); $property = new PropertyNode(1, [$prop]); $fixture = new PropertyIterator($property); - $this->assertEquals('myDefault', $fixture->getDefault()); + $this->assertEquals(new String_('myDefault'), $fixture->getDefault()); } /** diff --git a/tests/unit/phpDocumentor/Reflection/Php/Factory/PropertyTest.php b/tests/unit/phpDocumentor/Reflection/Php/Factory/PropertyTest.php index 047f2574..57e47bd5 100644 --- a/tests/unit/phpDocumentor/Reflection/Php/Factory/PropertyTest.php +++ b/tests/unit/phpDocumentor/Reflection/Php/Factory/PropertyTest.php @@ -33,11 +33,11 @@ use function next; /** - * @uses \phpDocumentor\Reflection\Php\Factory\PropertyIterator - * @uses \phpDocumentor\Reflection\Php\Property + * @uses PropertyIterator + * @uses PropertyDescriptor * @uses \phpDocumentor\Reflection\Php\Visibility - * @uses \phpDocumentor\Reflection\Php\ProjectFactoryStrategies - * @uses \phpDocumentor\Reflection\Php\Factory\Type + * @uses ProjectFactoryStrategies + * @uses Type * * @covers \phpDocumentor\Reflection\Php\Factory\Property * @covers \phpDocumentor\Reflection\Php\Factory\AbstractFactory @@ -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/Factory/TestCase.php b/tests/unit/phpDocumentor/Reflection/Php/Factory/TestCase.php index 6ca49fbd..6ab04661 100644 --- a/tests/unit/phpDocumentor/Reflection/Php/Factory/TestCase.php +++ b/tests/unit/phpDocumentor/Reflection/Php/Factory/TestCase.php @@ -27,8 +27,7 @@ */ abstract class TestCase extends MockeryTestCase { - /** @var ProjectFactoryStrategy */ - protected $fixture; + protected ProjectFactoryStrategy $fixture; public static function createContext(?Context $typeContext = null): ContextStack { diff --git a/tests/unit/phpDocumentor/Reflection/Php/PropertyTest.php b/tests/unit/phpDocumentor/Reflection/Php/PropertyTest.php index ba05b9d9..cecaf6bc 100644 --- a/tests/unit/phpDocumentor/Reflection/Php/PropertyTest.php +++ b/tests/unit/phpDocumentor/Reflection/Php/PropertyTest.php @@ -60,8 +60,8 @@ 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()); } /** @@ -73,10 +73,10 @@ public function testGetFqsenAndGetName(): void 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()); } /** @@ -88,7 +88,7 @@ public function testGettingWhetherPropertyIsStatic(): void 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, @@ -102,7 +102,7 @@ public function testGettingWhetherPropertyIsReadOnly(): void true ); - $this->assertTrue($property->isReadOnly()); + self::assertTrue($property->isReadOnly()); } /** @@ -115,7 +115,7 @@ 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()); } /** @@ -127,10 +127,10 @@ public function testGettingVisibility(): void 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()); } /** @@ -141,10 +141,12 @@ public function testSetAndGetTypes(): void 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)); } /** @@ -155,7 +157,7 @@ public function testGetDefault(): void 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 @@ -184,9 +186,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()); } }