From da911b8f89283544bf6a8e7547646a84499089a8 Mon Sep 17 00:00:00 2001 From: William Desportes Date: Tue, 2 May 2023 20:15:44 +0200 Subject: [PATCH 1/5] docs: add a CHANGELOG entry for #59 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 658ebbb8..aad2d034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - yyyy-mm-dd +- Fix crash on `IntersectionType` (intersection types) + ## [5.5.2] - 2023-03-12 - Fixed a JS null pointer error on Doctum.cleanSearchQuery From 454f436f8684fdc122448429d039a8d183f07103 Mon Sep 17 00:00:00 2001 From: William Desportes Date: Sun, 7 May 2023 16:55:02 +0200 Subject: [PATCH 2/5] Add handling for native property types --- src/Parser/NodeVisitor.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/Parser/NodeVisitor.php b/src/Parser/NodeVisitor.php index 189cb974..8715ab07 100644 --- a/src/Parser/NodeVisitor.php +++ b/src/Parser/NodeVisitor.php @@ -475,6 +475,32 @@ protected function getPropertyReflectionFromParserProperty(PropertyNode $node, P $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); + + $typeArr = []; + foreach ($type->types as $type) { + $typeStr = $this->typeToString($type); + $typeArr[] = [$typeStr, false]; + } + + $property->setHint($this->resolveHint($typeArr)); + } else { + $typeStr = $this->typeToString($type); + + if (null !== $typeStr) { + $typeArr = [[$typeStr, false]]; + + if ($type instanceof NullableType) { + $typeArr[] = ['null', false]; + } + $property->setHint($this->resolveHint($typeArr)); + } + } + if ($errors = $comment->getErrors()) { $property->setErrors($errors); } else { From aa6aa56ad1ec8da5ee3513e3086c7ff86dd90756 Mon Sep 17 00:00:00 2001 From: William Desportes Date: Sun, 7 May 2023 17:28:48 +0200 Subject: [PATCH 3/5] Update test data --- tests/phar/data/src/Intersection.php | 11 --------- tests/phar/data/src/IntersectionDocument.php | 24 +++++++++++++++++++ .../phar/data/src/RoundaboutIntersection.php | 2 ++ 3 files changed, 26 insertions(+), 11 deletions(-) delete mode 100644 tests/phar/data/src/Intersection.php create mode 100644 tests/phar/data/src/IntersectionDocument.php diff --git a/tests/phar/data/src/Intersection.php b/tests/phar/data/src/Intersection.php deleted file mode 100644 index d74c4131..00000000 --- a/tests/phar/data/src/Intersection.php +++ /dev/null @@ -1,11 +0,0 @@ - Date: Sun, 7 May 2023 15:59:59 +0200 Subject: [PATCH 4/5] 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 8715ab07..788a21e7 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 038b4bdd..915f76b0 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 a3ff4f49..a36f4f55 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 e4e3d061..5dd186bf 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 0d0fedd4..5ec1f4a1 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 a7190e64..1810e807 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 d3a33a54..3cbf26b4 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 %} From 4b5013b2233684558f8b96c277297d4287c68e7f Mon Sep 17 00:00:00 2001 From: William Desportes Date: Wed, 11 Oct 2023 01:36:13 +0200 Subject: [PATCH 5/5] chore: release 5.5.3 --- CHANGELOG.md | 2 +- src/Doctum.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aad2d034..fb4bfe1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] - yyyy-mm-dd +## [5.5.3] - 2023-10-11 - Fix crash on `IntersectionType` (intersection types) diff --git a/src/Doctum.php b/src/Doctum.php index d1bd8637..f6a2755f 100644 --- a/src/Doctum.php +++ b/src/Doctum.php @@ -53,7 +53,7 @@ class Doctum implements ArrayAccess public const VERSION_MAJOR = 5; public const VERSION_MINOR = 5; public const VERSION_PATCH = 3; - public const IS_DEV = true; + public const IS_DEV = false; //@phpstan-ignore-next-line public const VERSION = self::VERSION_MAJOR . '.' . self::VERSION_MINOR . '.' . self::VERSION_PATCH . (self::IS_DEV ? '-dev' : '');