From c867ba8271ea69a61665a1036a8dc7ea30ebb676 Mon Sep 17 00:00:00 2001 From: William Desportes Date: Sun, 7 May 2023 15:59:59 +0200 Subject: [PATCH] Fix #59 - Implement intersection types correctly --- src/Parser/NodeVisitor.php | 125 ++++++++++------------- src/Reflection/FunctionReflection.php | 17 +++ src/Reflection/MethodReflection.php | 39 ++++--- src/Reflection/ParameterReflection.php | 34 ++++-- src/Reflection/PropertyReflection.php | 17 +++ src/Resources/themes/default/class.twig | 10 +- src/Resources/themes/default/macros.twig | 8 +- 7 files changed, 147 insertions(+), 103 deletions(-) diff --git a/src/Parser/NodeVisitor.php b/src/Parser/NodeVisitor.php index 8715ab071..788a21e77 100644 --- a/src/Parser/NodeVisitor.php +++ b/src/Parser/NodeVisitor.php @@ -131,18 +131,7 @@ protected function addFunction(FunctionNode $node, ?string $namespace = null) $parameter->setVariadic($param->variadic); - $type = $param->type; - $typeStr = $this->typeToString($type); - - if (null !== $typeStr) { - $typeArr = [[$typeStr, false]]; - - if ($param->type instanceof NullableType) { - $typeArr[] = ['null', false]; - } - - $parameter->setHint($this->resolveHint($typeArr)); - } + $this->manageHint($param->type, $parameter); $function->addParameter($parameter); } @@ -167,18 +156,7 @@ protected function addFunction(FunctionNode $node, ?string $namespace = null) $function->setModifiersFromTags(); $function->setErrors($errors); - $returnType = $node->getReturnType(); - $returnTypeStr = $this->typeToString($returnType); - - if (null !== $returnTypeStr) { - $returnTypeArr = [[$returnTypeStr, false]]; - - if ($returnType instanceof NullableType) { - $returnTypeArr[] = ['null', false]; - } - - $function->setHint($this->resolveHint($returnTypeArr)); - } + $this->manageHint($node->getReturnType(), $function); $this->context->addFunction($function); @@ -188,7 +166,7 @@ protected function addFunction(FunctionNode $node, ?string $namespace = null) } /** - * @param \PhpParser\Node\Identifier|\PhpParser\Node\Name|NullableType|UnionType|IntersectionType|null $type Type declaration + * @param \PhpParser\Node\ComplexType|\PhpParser\Node\Identifier|\PhpParser\Node\Name|NullableType|UnionType|IntersectionType|null $type Type declaration */ protected function typeToString($type): ?string { @@ -206,9 +184,13 @@ protected function typeToString($type): ?string } elseif ($type instanceof IntersectionType) { $typeString = []; foreach ($type->types as $type) { - $typeString[] = $type->__toString(); + $typeAsStr = $type->__toString(); + if ($type instanceof FullyQualified && 0 !== strpos($typeAsStr, '\\')) { + $typeAsStr = '\\' . $typeAsStr; + } + $typeString[] = $typeAsStr; } - $typeString = implode('&', $typeString); + return implode('&', $typeString); } if ($typeString === null) { @@ -332,18 +314,7 @@ protected function addMethod(ClassMethodNode $node) $parameter->setVariadic($param->variadic); - $type = $param->type; - $typeStr = $this->typeToString($type); - - if (null !== $typeStr) { - $typeArr = [[$typeStr, false]]; - - if ($param->type instanceof NullableType) { - $typeArr[] = ['null', false]; - } - - $parameter->setHint($this->resolveHint($typeArr)); - } + $this->manageHint($param->type, $parameter); $method->addParameter($parameter); } @@ -371,18 +342,7 @@ protected function addMethod(ClassMethodNode $node) $method->setModifiersFromTags(); $method->setErrors($errors); - $returnType = $node->getReturnType(); - $returnTypeStr = $this->typeToString($returnType); - - if (null !== $returnTypeStr) { - $returnTypeArr = [[$returnTypeStr, false]]; - - if ($returnType instanceof NullableType) { - $returnTypeArr[] = ['null', false]; - } - - $method->setHint($this->resolveHint($returnTypeArr)); - } + $this->manageHint($node->getReturnType(), $method); if ($this->context->getFilter()->acceptMethod($method)) { $this->context->getClass()->addMethod($method); @@ -417,6 +377,14 @@ protected function addTagFromCommentToMethod( if (is_array($firstTagFound)) { $hint = $firstTagFound[0]; $hintDescription = $firstTagFound[1] ?? null; + if (is_array($hint) && isset($hint[0]) && stripos($hint[0][0] ?? '', '&') !== false) {// Detect intersection type + $methodOrFunctionOrProperty->setIntersectionType(true); + $intersectionParts = explode('&', $hint[0][0]); + $hint = []; + foreach ($intersectionParts as $part) { + $hint[] = [$part, false]; + } + } $methodOrFunctionOrProperty->setHint(is_array($hint) ? $this->resolveHint($hint) : $hint); if ($hintDescription !== null) { if (is_string($hintDescription)) { @@ -458,28 +426,13 @@ protected function addProperty(PropertyNode $node) } /** - * @return array - * @phpstan-return array{PropertyReflection,string[]} + * @param \PhpParser\Node\ComplexType|\PhpParser\Node\Identifier|\PhpParser\Node\Name|NullableType|UnionType|IntersectionType|null $type Type declaration + * @param MethodReflection|FunctionReflection|ParameterReflection|PropertyReflection $object */ - protected function getPropertyReflectionFromParserProperty(PropertyNode $node, PropertyProperty $prop): array + protected function manageHint($type, Reflection $object): void { - $property = new PropertyReflection($prop->name->toString(), $prop->getLine()); - $property->setModifiers($node->flags); - - $property->setDefault($prop->default); - - $docComment = $node->getDocComment(); - $docComment = $docComment === null ? null : $docComment->__toString(); - $comment = $this->context->getDocBlockParser()->parse($docComment, $this->context, $property); - $property->setDocComment($docComment); - $property->setShortDesc($comment->getShortDesc()); - $property->setLongDesc($comment->getLongDesc()); - $property->setSee($this->resolveSee($comment->getTag('see'))); - - $type = $node->type; - if ($type instanceof IntersectionType) { - $property->setIntersectionType(true); + $object->setIntersectionType(true); $typeArr = []; foreach ($type->types as $type) { @@ -487,7 +440,7 @@ protected function getPropertyReflectionFromParserProperty(PropertyNode $node, P $typeArr[] = [$typeStr, false]; } - $property->setHint($this->resolveHint($typeArr)); + $object->setHint($this->resolveHint($typeArr)); } else { $typeStr = $this->typeToString($type); @@ -497,9 +450,31 @@ protected function getPropertyReflectionFromParserProperty(PropertyNode $node, P if ($type instanceof NullableType) { $typeArr[] = ['null', false]; } - $property->setHint($this->resolveHint($typeArr)); + $object->setHint($this->resolveHint($typeArr)); } } + } + + /** + * @return array + * @phpstan-return array{PropertyReflection,string[]} + */ + protected function getPropertyReflectionFromParserProperty(PropertyNode $node, PropertyProperty $prop): array + { + $property = new PropertyReflection($prop->name->toString(), $prop->getLine()); + $property->setModifiers($node->flags); + + $property->setDefault($prop->default); + + $docComment = $node->getDocComment(); + $docComment = $docComment === null ? null : $docComment->__toString(); + $comment = $this->context->getDocBlockParser()->parse($docComment, $this->context, $property); + $property->setDocComment($docComment); + $property->setShortDesc($comment->getShortDesc()); + $property->setLongDesc($comment->getLongDesc()); + $property->setSee($this->resolveSee($comment->getTag('see'))); + + $this->manageHint($node->type, $property); if ($errors = $comment->getErrors()) { $property->setErrors($errors); @@ -643,6 +618,9 @@ protected function updateMethodParametersFromTags(Reflection $method, array $tag return $errors; } + /** + * @phpstan-param $hints array{0: string, 1: bool} + */ protected function resolveHint(array $hints): array { foreach ($hints as $i => $hint) { @@ -652,6 +630,9 @@ protected function resolveHint(array $hints): array return $hints; } + /** + * @phpstan-param $alias array{0: string, 1: bool} + */ protected function resolveAlias($alias) { // not a class diff --git a/src/Reflection/FunctionReflection.php b/src/Reflection/FunctionReflection.php index 038b4bddb..915f76b05 100644 --- a/src/Reflection/FunctionReflection.php +++ b/src/Reflection/FunctionReflection.php @@ -19,6 +19,8 @@ class FunctionReflection extends Reflection /** @var array */ protected $parameters = []; protected $byRef; + /** @var bool */ + protected $isIntersectionType = false; protected $project; /** @var string|null */ protected $file = null; @@ -43,6 +45,16 @@ public function isByRef() return $this->byRef; } + public function setIntersectionType(bool $boolean): void + { + $this->isIntersectionType = $boolean; + } + + public function isIntersectionType(): bool + { + return $this->isIntersectionType; + } + /** * @return Project */ @@ -202,6 +214,7 @@ public function toArray() 'tags' => $this->tags, 'modifiers' => $this->modifiers, 'is_by_ref' => $this->byRef, + 'is_intersection_type' => $this->isIntersectionType(), 'exceptions' => $this->exceptions, 'errors' => $this->errors, 'parameters' => array_map( @@ -233,6 +246,10 @@ public static function fromArray(Project $project, array $array) $method->relativeFilePath = $array['relative_file'] ?? '';// New in 5.5.0 $method->fromCache = true; + if (isset($array['is_intersection_type'])) {// New in 5.5.3 + $method->setIntersectionType($array['is_intersection_type']); + } + foreach ($array['parameters'] as $parameter) { $method->addParameter(ParameterReflection::fromArray($project, $parameter)); } diff --git a/src/Reflection/MethodReflection.php b/src/Reflection/MethodReflection.php index a3ff4f49e..a36f4f556 100644 --- a/src/Reflection/MethodReflection.php +++ b/src/Reflection/MethodReflection.php @@ -21,7 +21,9 @@ class MethodReflection extends Reflection protected $class; protected $parameters = []; protected $byRef; - protected $exceptions = []; + /** @var bool */ + protected $isIntersectionType = false; + protected $exceptions = []; public function __toString() { @@ -38,6 +40,16 @@ public function isByRef() return $this->byRef; } + public function setIntersectionType(bool $boolean): void + { + $this->isIntersectionType = $boolean; + } + + public function isIntersectionType(): bool + { + return $this->isIntersectionType; + } + /** * {@inheritDoc} */ @@ -135,6 +147,7 @@ public function toArray() 'see' => $this->see, 'modifiers' => $this->modifiers, 'is_by_ref' => $this->byRef, + 'is_intersection_type' => $this->isIntersectionType(), 'exceptions' => $this->exceptions, 'errors' => $this->errors, 'parameters' => array_map( @@ -151,17 +164,19 @@ static function ($parameter) { */ public static function fromArray(Project $project, array $array) { - $method = new self($array['name'], $array['line']); - $method->shortDesc = $array['short_desc']; - $method->longDesc = $array['long_desc']; - $method->hint = $array['hint']; - $method->hintDesc = $array['hint_desc']; - $method->tags = $array['tags']; - $method->modifiers = $array['modifiers']; - $method->byRef = $array['is_by_ref']; - $method->exceptions = $array['exceptions']; - $method->errors = $array['errors']; - $method->see = $array['see'] ?? [];// New in 5.4.0 + $method = new self($array['name'], $array['line']); + $method->shortDesc = $array['short_desc']; + $method->longDesc = $array['long_desc']; + $method->hint = $array['hint']; + $method->hintDesc = $array['hint_desc']; + $method->tags = $array['tags']; + $method->modifiers = $array['modifiers']; + $method->byRef = $array['is_by_ref']; + $method->exceptions = $array['exceptions']; + $method->errors = $array['errors']; + $method->see = $array['see'] ?? [];// New in 5.4.0 + $method->isIntersectionType = $array['is_intersection_type'] ?? false;// New in 5.5.3 + foreach ($array['parameters'] as $parameter) { $method->addParameter(ParameterReflection::fromArray($project, $parameter)); diff --git a/src/Reflection/ParameterReflection.php b/src/Reflection/ParameterReflection.php index e4e3d061d..5dd186bff 100644 --- a/src/Reflection/ParameterReflection.php +++ b/src/Reflection/ParameterReflection.php @@ -24,6 +24,8 @@ class ParameterReflection extends Reflection protected $byRef; protected $default; protected $variadic; + /** @var bool */ + protected $isIntersectionType = false; public function __toString() { @@ -38,6 +40,16 @@ public function getClass() return $this->method->getClass(); } + public function setIntersectionType(bool $boolean): void + { + $this->isIntersectionType = $boolean; + } + + public function isIntersectionType(): bool + { + return $this->isIntersectionType; + } + public function setByRef($boolean) { $this->byRef = $boolean; @@ -134,6 +146,7 @@ public function toArray() 'variadic' => $this->variadic, 'is_by_ref' => $this->byRef, 'is_read_only' => $this->isReadOnly(), + 'is_intersection_type' => $this->isIntersectionType(), ]; } @@ -142,16 +155,17 @@ public function toArray() */ public static function fromArray(Project $project, array $array) { - $parameter = new self($array['name'], $array['line']); - $parameter->shortDesc = $array['short_desc']; - $parameter->longDesc = $array['long_desc']; - $parameter->hint = $array['hint']; - $parameter->tags = $array['tags']; - $parameter->modifiers = $array['modifiers']; - $parameter->default = $array['default']; - $parameter->variadic = $array['variadic']; - $parameter->byRef = $array['is_by_ref']; - $parameter->isReadOnly = $array['is_read_only'] ?? false;// New in 5.4.0 + $parameter = new self($array['name'], $array['line']); + $parameter->shortDesc = $array['short_desc']; + $parameter->longDesc = $array['long_desc']; + $parameter->hint = $array['hint']; + $parameter->tags = $array['tags']; + $parameter->modifiers = $array['modifiers']; + $parameter->default = $array['default']; + $parameter->variadic = $array['variadic']; + $parameter->byRef = $array['is_by_ref']; + $parameter->isReadOnly = $array['is_read_only'] ?? false;// New in 5.4.0 + $parameter->isIntersectionType = $array['is_intersection_type'] ?? false;// New in 5.5.3 return $parameter; } diff --git a/src/Reflection/PropertyReflection.php b/src/Reflection/PropertyReflection.php index 0d0fedd42..5ec1f4a1d 100644 --- a/src/Reflection/PropertyReflection.php +++ b/src/Reflection/PropertyReflection.php @@ -21,6 +21,8 @@ class PropertyReflection extends Reflection protected $default; /** @var bool */ protected $isWriteOnly = false; + /** @var bool */ + protected $isIntersectionType = false; public function __toString() { @@ -70,6 +72,16 @@ public function setClass(ClassReflection $class): void $this->class = $class; } + public function setIntersectionType(bool $boolean): void + { + $this->isIntersectionType = $boolean; + } + + public function isIntersectionType(): bool + { + return $this->isIntersectionType; + } + /** * @return array */ @@ -88,6 +100,7 @@ public function toArray() 'errors' => $this->errors, 'is_read_only' => $this->isReadOnly(), 'is_write_only' => $this->isWriteOnly(), + 'is_intersection_type' => $this->isIntersectionType(), ]; } @@ -114,6 +127,10 @@ public static function fromArray(Project $project, array $array) $property->setWriteOnly($array['is_write_only']); } + if (isset($array['is_intersection_type'])) {// New in 5.5.3 + $property->setIntersectionType($array['is_intersection_type']); + } + return $property; } diff --git a/src/Resources/themes/default/class.twig b/src/Resources/themes/default/class.twig index a7190e64c..1810e807d 100644 --- a/src/Resources/themes/default/class.twig +++ b/src/Resources/themes/default/class.twig @@ -107,7 +107,7 @@ {% if method.static %}static{% endif %} {% if method.protected %}protected{% endif %} {% if method.private %}private{% endif %} - {{ hint_link(method.hint) }} + {{ hint_link(method.hint, method.isIntersectionType()) }} {{ method.name|raw }}{{ block('method_parameters_signature') }} {%- endblock %} @@ -121,7 +121,7 @@ {% for parameter in method.parameters %} - + @@ -132,7 +132,7 @@ {% block return %}
{% if parameter.hint %}{{ hint_link(parameter.hint) }}{% endif %}{% if parameter.hint %}{{ hint_link(parameter.hint, parameter.isIntersectionType()) }}{% endif %} {%- if parameter.variadic %}...{% endif %}${{ parameter.name|raw }} {{ parameter.shortdesc|desc(class)|md_to_html }}
- +
{{ hint_link(method.hint) }}{{ hint_link(method.hint, method.isIntersectionType()) }} {{ method.hintDesc|desc(class)|md_to_html }}
@@ -216,7 +216,7 @@ {% if property.isStatic() %}static{% endif %} {% if property.isProtected() %}protected{% endif %} {% if property.isPrivate() %}private{% endif %} - {{ hint_link(property.hint) }} + {{ hint_link(property.hint, property.isIntersectionType()) }} {% if property.isInternal() %}{% trans 'internal' %}{% endif %} {% if property.isDeprecated() %}{% trans 'deprecated' %}{% endif %} {% if property.isReadOnly() %}{% trans 'read-only' %}{% endif %} @@ -244,7 +244,7 @@ {% for method in methods %}
- {% if method.static %}static {% endif %}{{ hint_link(method.hint) }} + {% if method.static %}static {% endif %}{{ hint_link(method.hint, method.isIntersectionType()) }}
{{ method.name|raw }}{{ block('method_parameters_signature') }} diff --git a/src/Resources/themes/default/macros.twig b/src/Resources/themes/default/macros.twig index d3a33a54b..3cbf26b46 100644 --- a/src/Resources/themes/default/macros.twig +++ b/src/Resources/themes/default/macros.twig @@ -31,7 +31,7 @@ {# #} {%- endmacro %} -{% macro hint_link(hints) -%} +{% macro hint_link(hints, isIntersectionType = false) -%} {%- from _self import class_link %} {%- if hints %} @@ -42,7 +42,7 @@ {{- abbr_class(hint.name) }} {%- endif %} {%- if hint.array %}[]{% endif %} - {%- if not loop.last %}|{% endif %} + {%- if not loop.last %}{%- if isIntersectionType %}&{% else %}|{% endif %}{% endif %} {%- endfor %} {%- endif %} {%- endmacro %} @@ -71,7 +71,7 @@ {%- from "macros.twig" import hint_link -%} ( {%- for parameter in method.parameters %} - {%- if parameter.hashint %}{{ hint_link(parameter.hint) }} {% endif -%} + {%- if parameter.hashint %}{{ hint_link(parameter.hint, parameter.isIntersectionType()) }} {% endif -%} {%- if parameter.variadic %}...{% endif %}${{ parameter.name|raw }} {%- if parameter.default is not null %} = {{ parameter.default }}{% endif %} {%- if not loop.last %}, {% endif %} @@ -83,7 +83,7 @@ {%- from "macros.twig" import hint_link -%} ( {%- for parameter in method.parameters %} - {%- if parameter.hashint %}{{ hint_link(parameter.hint) }} {% endif -%} + {%- if parameter.hashint %}{{ hint_link(parameter.hint, parameter.isIntersectionType()) }} {% endif -%} {%- if parameter.variadic %}...{% endif %}${{ parameter.name|raw }} {%- if parameter.default is not null %} = {{ parameter.default }}{% endif %} {%- if not loop.last %}, {% endif %}