From b217916f37e309829a0bf3d5bb1560ab3b435057 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Fri, 30 Oct 2020 17:37:16 -0400 Subject: [PATCH] Use better inference for getAttributes return type Fixes #4367 --- .../Internal/Analyzer/CommentAnalyzer.php | 10 ++ src/Psalm/Internal/Codebase/Methods.php | 4 +- .../Reflector/FunctionLikeNodeScanner.php | 94 ++++++++----- .../Scanner/FunctionDocblockComment.php | 10 ++ src/Psalm/Storage/MethodStorage.php | 4 + stubs/CoreGenericClasses.phpstub | 126 +++++++++++++----- tests/AttributeTest.php | 41 ++++++ 7 files changed, 221 insertions(+), 68 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index fe0389ab6..3010fa548 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -621,6 +621,16 @@ class CommentAnalyzer } } + if (isset($parsed_docblock->tags['since'])) { + $since = trim(reset($parsed_docblock->tags['since'])); + if (preg_match('/^[4578]\.\d(\.\d+)?$/', $since)) { + $since_parts = explode('.', $since); + + $info->since_php_major_version = (int)$since_parts[0]; + $info->since_php_minor_version = (int)$since_parts[1]; + } + } + if (isset($parsed_docblock->tags['deprecated'])) { $info->deprecated = true; } diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index 7b41217ea..4db90ade8 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -343,7 +343,9 @@ class Methods if (InternalCallMapHandler::inCallMap((string) $callmap_id)) { $class_storage = $this->classlike_storage_provider->get($callmap_id->fq_class_name); - if (!$class_storage->stubbed) { + $declaring_method_name = $declaring_method_id ? $declaring_method_id->method_name : $method_name; + + if (!$class_storage->stubbed || empty($class_storage->methods[$declaring_method_name]->stubbed)) { $function_callables = InternalCallMapHandler::getCallablesFromCallMap((string) $callmap_id); if ($function_callables === null) { diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index b15824e8a..5c6e11715 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -129,7 +129,9 @@ class FunctionLikeNodeScanner $fq_classlike_name = null; $is_functionlike_override = false; + $function_id = null; $method_name_lc = null; + $method_id = null; if ($fake_method && $stmt instanceof PhpParser\Node\Stmt\ClassMethod) { $cased_function_id = '@method ' . $stmt->name->name; @@ -208,16 +210,6 @@ class FunctionLikeNodeScanner } } } - - if ($this->codebase->register_stub_files - || ($this->codebase->register_autoload_files - && !$this->codebase->functions->hasStubbedFunction($function_id)) - ) { - $this->codebase->functions->addGlobalFunction($function_id, $storage); - } - - $this->file_storage->functions[$function_id] = $storage; - $this->file_storage->declaring_function_ids[$function_id] = strtolower($this->file_path); } elseif ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) { if (!$this->classlike_storage) { throw new \LogicException('$this->classlike_storage should not be null'); @@ -255,17 +247,17 @@ class FunctionLikeNodeScanner $duplicate_method_storage->has_visitor_issues = true; return false; - } else { - $is_functionlike_override = true; } + $is_functionlike_override = true; $storage = $this->storage = $classlike_storage->methods[$method_name_lc]; } if (!$storage) { - $storage = $this->storage = $classlike_storage->methods[$method_name_lc] = new MethodStorage(); + $storage = $this->storage = new MethodStorage(); } + $storage->stubbed = $this->codebase->register_stub_files; $storage->defining_fqcln = $fq_classlike_name; $class_name_parts = explode('\\', $fq_classlike_name); @@ -296,28 +288,11 @@ class FunctionLikeNodeScanner $method_name_lc ); - $classlike_storage->declaring_method_ids[$method_name_lc] - = $classlike_storage->appearing_method_ids[$method_name_lc] - = $method_id; - - if (!$stmt->isPrivate() || $method_name_lc === '__construct' || $classlike_storage->is_trait) { - $classlike_storage->inheritable_method_ids[$method_name_lc] = $method_id; - } - - if (!isset($classlike_storage->overridden_method_ids[$method_name_lc])) { - $classlike_storage->overridden_method_ids[$method_name_lc] = []; - } - $storage->is_static = $stmt->isStatic(); $storage->abstract = $stmt->isAbstract(); $storage->final = $classlike_storage->final || $stmt->isFinal(); - if ($storage->final && $method_name_lc === '__construct') { - // a bit of a hack, but makes sure that `new static` works for these classes - $classlike_storage->preserve_constructor_signature = true; - } - if ($stmt->isPrivate()) { $storage->visibility = ClassLikeAnalyzer::VISIBILITY_PRIVATE; } elseif ($stmt->isProtected()) { @@ -634,6 +609,18 @@ class FunctionLikeNodeScanner } if ($docblock_info) { + if ($docblock_info->since_php_major_version && !$this->aliases->namespace) { + if ($docblock_info->since_php_major_version > $this->codebase->php_major_version) { + return false; + } + + if ($docblock_info->since_php_major_version === $this->codebase->php_major_version + && $docblock_info->since_php_minor_version > $this->codebase->php_minor_version + ) { + return false; + } + } + FunctionLike\DocblockScanner::addDocblockInfo( $this->codebase, $this->file_scanner, @@ -652,6 +639,53 @@ class FunctionLikeNodeScanner } } + // register the functionlike once the @since check has been completed + if ($stmt instanceof PhpParser\Node\Stmt\Function_ + && $function_id + && $storage instanceof FunctionStorage + ) { + if ($this->codebase->register_stub_files + || ($this->codebase->register_autoload_files + && !$this->codebase->functions->hasStubbedFunction($function_id)) + ) { + $this->codebase->functions->addGlobalFunction($function_id, $storage); + } + + $this->file_storage->functions[$function_id] = $storage; + $this->file_storage->declaring_function_ids[$function_id] = strtolower($this->file_path); + } elseif ($stmt instanceof PhpParser\Node\Stmt\ClassMethod + && $classlike_storage + && $storage instanceof MethodStorage + && $method_name_lc + && !$fake_method + && $method_id + ) { + $classlike_storage->methods[$method_name_lc] = $storage; + + $classlike_storage->declaring_method_ids[$method_name_lc] + = $classlike_storage->appearing_method_ids[$method_name_lc] + = $method_id; + + if (!$stmt->isPrivate() || $method_name_lc === '__construct' || $classlike_storage->is_trait) { + $classlike_storage->inheritable_method_ids[$method_name_lc] = $method_id; + } + + if (!isset($classlike_storage->overridden_method_ids[$method_name_lc])) { + $classlike_storage->overridden_method_ids[$method_name_lc] = []; + } + + if ($storage->final && $method_name_lc === '__construct') { + // a bit of a hack, but makes sure that `new static` works for these classes + $classlike_storage->preserve_constructor_signature = true; + } + } elseif (($stmt instanceof PhpParser\Node\Expr\Closure + || $stmt instanceof PhpParser\Node\Expr\ArrowFunction) + && $function_id + && $storage instanceof FunctionStorage + ) { + $this->file_storage->functions[$function_id] = $storage; + } + if ($classlike_storage && $method_name_lc === '__construct') { foreach ($stmt->getParams() as $param) { if (!$param->flags || !$param->var instanceof PhpParser\Node\Expr\Variable) { diff --git a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php index 05f264908..d85677425 100644 --- a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php +++ b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php @@ -201,4 +201,14 @@ class FunctionDocblockComment /** @var bool */ public $stub_override = false; + + /** + * @var int + */ + public $since_php_major_version = 0; + + /** + * @var int + */ + public $since_php_minor_version = 0; } diff --git a/src/Psalm/Storage/MethodStorage.php b/src/Psalm/Storage/MethodStorage.php index 6a805f908..08551efa9 100644 --- a/src/Psalm/Storage/MethodStorage.php +++ b/src/Psalm/Storage/MethodStorage.php @@ -79,4 +79,8 @@ class MethodStorage extends FunctionLikeStorage * @var Type\Union|null */ public $if_this_is_type = null; + /* + * @var bool + */ + public $stubbed = false; } diff --git a/stubs/CoreGenericClasses.phpstub b/stubs/CoreGenericClasses.phpstub index 80d099504..470feade1 100644 --- a/stubs/CoreGenericClasses.phpstub +++ b/stubs/CoreGenericClasses.phpstub @@ -218,7 +218,7 @@ class CachingIterator extends IteratorIterator implements OuterIterator , ArrayA /** * @param Iterator $iterator */ - public function __construct(Iterator $iterator, int $flags = self::CALL_TOSTRING) {} + public function __construct(Iterator $iterator, int $flags = self::CALL_TOSTRING) {} /** @return bool */ public function hasNext () {} @@ -246,7 +246,7 @@ class InfiniteIterator extends IteratorIterator implements OuterIterator { /** * @param Iterator $iterator */ - public function __construct(Iterator $iterator) {} + public function __construct(Iterator $iterator) {} /** * @return TValue Can return any type. @@ -267,11 +267,11 @@ class InfiniteIterator extends IteratorIterator implements OuterIterator { * * @template-extends IteratorIterator */ -class LimitIterator extends IteratorIterator implements OuterIterator { +class LimitIterator extends IteratorIterator implements OuterIterator { /** * @param Iterator $iterator */ - public function __construct(Iterator $iterator, int $offset = 0, int $count = -1) {} + public function __construct(Iterator $iterator, int $offset = 0, int $count = -1) {} /** * @return TValue Can return any type. @@ -292,12 +292,12 @@ class LimitIterator extends IteratorIterator implements OuterIterator { * * @template-extends FilterIterator */ -class CallbackFilterIterator extends FilterIterator implements OuterIterator { +class CallbackFilterIterator extends FilterIterator implements OuterIterator { /** * @param Iterator $iterator * @param callable(TValue, TKey, Iterator): bool $callback */ - public function __construct(Iterator $iterator, callable $callback) {} + public function __construct(Iterator $iterator, callable $callback) {} /** * @return TValue Can return any type. @@ -316,11 +316,11 @@ class CallbackFilterIterator extends FilterIterator implements OuterIterator { * * @template-extends IteratorIterator */ -class NoRewindIterator extends IteratorIterator { +class NoRewindIterator extends IteratorIterator { /** * @param Iterator $iterator */ - public function __construct(Iterator $iterator) {} + public function __construct(Iterator $iterator) {} /** * @return TValue Can return any type. @@ -1131,7 +1131,7 @@ class DOMNamedNodeMap implements Traversable, Countable { * @template TKey * @template TValue * @template-implements Iterator - * @template-implements ArrayAccess + * @template-implements ArrayAccess */ class SplDoublyLinkedList implements Iterator, Countable, ArrayAccess, Serializable { @@ -1298,9 +1298,9 @@ class SplDoublyLinkedList implements Iterator, Countable, ArrayAccess, Serializa } /** - * The SplFixedArray class provides the main functionalities of array. - * The main differences between a SplFixedArray and a normal PHP array is that - * the SplFixedArray is of fixed length and allows only integers within the range as indexes. + * The SplFixedArray class provides the main functionalities of array. + * The main differences between a SplFixedArray and a normal PHP array is that + * the SplFixedArray is of fixed length and allows only integers within the range as indexes. * The advantage is that it uses less memory than a standard array. * * @link https://php.net/manual/en/class.splfixedarray.php @@ -1313,7 +1313,7 @@ class SplFixedArray implements Iterator, ArrayAccess, Countable { /** * Constructs a new fixed array * - * Initializes a fixed array with a number of NULL values equal to size. + * Initializes a fixed array with a number of NULL values equal to size. * @link https://php.net/manual/en/splfixedarray.construct.php * * @param int $size The size of the fixed array. This expects a number between 0 and PHP_INT_MAX. @@ -1329,7 +1329,7 @@ class SplFixedArray implements Iterator, ArrayAccess, Countable { * @template TInValue * @param array $array The array to import * @param bool $save_indexes [optional] Try to save the numeric indexes used in the original array. - * + * * @return SplFixedArray Instance of SplFixedArray containing the array content * @since 5.3.0 @@ -1417,7 +1417,7 @@ class SplFixedArray implements Iterator, ArrayAccess, Countable { * @since 5.3.0 */ public function next(): void {} - + /** * Returns whether the specified index exists * @link https://php.net/manual/en/splfixedarray.offsetexists.php @@ -1521,22 +1521,22 @@ class SplHeap implements Iterator, Countable { * * @param TValue $value1 The value of the first node being compared. * @param TValue $value2 The value of the second node being compared. - * @return int Positive integer if value1 is greater than value2, 0 if they are equal, negative integer otherwise. + * @return int Positive integer if value1 is greater than value2, 0 if they are equal, negative integer otherwise. * * @since 5.3.0 */ protected abstract function compare($value1, $value2): int {} - + /** * Counts the number of elements in the heap * @link https://php.net/manual/en/splheap.count.php * - * @return int The number of elements in the heap. + * @return int The number of elements in the heap. * * @since 5.3.0 */ public function count(): int {} - + /** * Get the current datastructure node. * @link https://php.net/manual/en/splheap.current.php @@ -1546,7 +1546,7 @@ class SplHeap implements Iterator, Countable { * @since 5.3.0 */ public function current() {} - + /** * Extracts a node from top of the heap and sift up * @link https://php.net/manual/en/splheap.extract.php @@ -1589,7 +1589,7 @@ class SplHeap implements Iterator, Countable { public function isEmpty(): bool {} /** - * Return current node index + * Return current node index * @link https://php.net/manual/en/splheap.key.php * * @return int The current node index @@ -1599,7 +1599,7 @@ class SplHeap implements Iterator, Countable { public function key() {} /** - * Move to the next node. This will delete the top node of the heap. + * Move to the next node. This will delete the top node of the heap. * @link https://php.net/manual/en/splheap.next.php * * @return void @@ -1617,7 +1617,7 @@ class SplHeap implements Iterator, Countable { * @since 5.3.0 */ public function recoverFromCorruption(): void {} - + /** * Rewind iterator back to the start (no-op) * @link https://php.net/manual/en/splheap.rewind.php @@ -1627,7 +1627,7 @@ class SplHeap implements Iterator, Countable { * @since 5.3.0 */ public function rewind(): void {} - + /** * Peeks at the node from the top of the heap * @link https://php.net/manual/en/splheap.top.php @@ -1637,12 +1637,12 @@ class SplHeap implements Iterator, Countable { * @since 5.3.0 */ public function top() {} - + /** * Check whether the heap contains any more nodes * @link https://php.net/manual/en/splheap.valid.php * - * @return bool Returns true if the heap contains any more nodes, false otherwise. + * @return bool Returns true if the heap contains any more nodes, false otherwise. * * @since 5.3.0 */ @@ -1700,22 +1700,22 @@ class SplPriorityQueue implements Iterator, Countable { * * @param TValue $priority1 The priority of the first node being compared. * @param TValue $priority2 The priority of the second node being compared. - * @return int Positive integer if priority1 is greater than priority2, 0 if they are equal, negative integer otherwise. + * @return int Positive integer if priority1 is greater than priority2, 0 if they are equal, negative integer otherwise. * * @since 5.3.0 */ public function compare($priority1, $priority2): int {} - + /** * Counts the number of elements in the queue * @link https://php.net/manual/en/splpriorityqueue.count.php * - * @return int The number of elements in the queue. + * @return int The number of elements in the queue. * * @since 5.3.0 */ public function count(): int {} - + /** * Get the current datastructure node. * @link https://php.net/manual/en/splpriorityqueue.current.php @@ -1725,7 +1725,7 @@ class SplPriorityQueue implements Iterator, Countable { * @since 5.3.0 */ public function current() {} - + /** * Extracts a node from top of the queue and sift up * @link https://php.net/manual/en/splpriorityqueue.extract.php @@ -1781,7 +1781,7 @@ class SplPriorityQueue implements Iterator, Countable { public function isEmpty(): bool {} /** - * Return current node index + * Return current node index * @link https://php.net/manual/en/splpriorityqueue.key.php * * @return int The current node index @@ -1809,7 +1809,7 @@ class SplPriorityQueue implements Iterator, Countable { * @since 5.3.0 */ public function recoverFromCorruption(): void {} - + /** * Rewind iterator back to the start (no-op) * @link https://php.net/manual/en/splpriorityqueue.rewind.php @@ -1819,12 +1819,12 @@ class SplPriorityQueue implements Iterator, Countable { * @since 5.3.0 */ public function rewind(): void {} - + /** * Sets the mode of extraction * @link https://php.net/manual/en/splpriorityqueue.setextractflags.php * - * @param SplPriorityQueue::EXTR_* $flags Defines what is extracted by SplPriorityQueue::current(), SplPriorityQueue::top() and SplPriorityQueue::extract(). + * @param SplPriorityQueue::EXTR_* $flags Defines what is extracted by SplPriorityQueue::current(), SplPriorityQueue::top() and SplPriorityQueue::extract(). * * @return void * @@ -1841,12 +1841,12 @@ class SplPriorityQueue implements Iterator, Countable { * @since 5.3.0 */ public function top() {} - + /** * Check whether the queue contains any more nodes * @link https://php.net/manual/en/splpriorityqueue.valid.php * - * @return bool Returns true if the queue contains any more nodes, false otherwise. + * @return bool Returns true if the queue contains any more nodes, false otherwise. * * @since 5.3.0 */ @@ -2141,6 +2141,58 @@ class ReflectionClass implements Reflector { * @psalm-ignore-nullable-return */ public function getTraitNames(): array {} + + /** + * @since 8.0 + * @template TClass as object + * @param class-string|null $name + * @return array> + */ + public function getAttributes(?string $name = null, int $flags = 0): array {} +} + +class ReflectionFunction +{ + /** + * @since 8.0 + * @template TClass as object + * @param class-string|null $name + * @return array> + */ + public function getAttributes(?string $name = null, int $flags = 0): array {} +} + +class ReflectionProperty +{ + /** + * @since 8.0 + * @template TClass as object + * @param class-string|null $name + * @return array> + */ + public function getAttributes(?string $name = null, int $flags = 0): array {} +} + +class ReflectionMethod +{ + /** + * @since 8.0 + * @template TClass as object + * @param class-string|null $name + * @return array> + */ + public function getAttributes(?string $name = null, int $flags = 0): array {} +} + +class ReflectionClassConstant +{ + /** + * @since 8.0 + * @template TClass as object + * @param class-string|null $name + * @return array> + */ + public function getAttributes(?string $name = null, int $flags = 0): array {} } /** diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index 3da3f8f08..c68d0e9b7 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -82,6 +82,26 @@ class AttributeTest extends TestCase [], '8.0' ], + 'testReflectingClass' => [ + 'getAttributes(BaseAttribute::class, 2) as $attr) { + $attribute = $attr->newInstance(); + echo $attribute->name; + } + }', + [], + [], + '8.0' + ], ]; } @@ -155,6 +175,27 @@ class AttributeTest extends TestCase false, '8.0' ], + 'testReflectingClass74' => [ + 'getAttributes(BaseAttribute::class, 2) as $attr) { + $attribute = $attr->newInstance(); + echo $attribute->name; + } + }', + 'error_message' => 'UndefinedMethod', + [], + false, + '7.4' + ], ]; } }