diff --git a/composer.lock b/composer.lock index db09a7e4..26fd01f4 100644 --- a/composer.lock +++ b/composer.lock @@ -382,33 +382,33 @@ }, { "name": "nette/finder", - "version": "v2.4.2", + "version": "v2.5.0", "source": { "type": "git", "url": "https://github.com/nette/finder.git", - "reference": "ee951a656cb8ac622e5dd33474a01fd2470505a0" + "reference": "6be1b83ea68ac558aff189d640abe242e0306fe2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/finder/zipball/ee951a656cb8ac622e5dd33474a01fd2470505a0", - "reference": "ee951a656cb8ac622e5dd33474a01fd2470505a0", + "url": "https://api.github.com/repos/nette/finder/zipball/6be1b83ea68ac558aff189d640abe242e0306fe2", + "reference": "6be1b83ea68ac558aff189d640abe242e0306fe2", "shasum": "" }, "require": { - "nette/utils": "~2.4", - "php": ">=5.6.0" + "nette/utils": "^2.4 || ~3.0.0", + "php": ">=7.1" }, "conflict": { "nette/nette": "<2.2" }, "require-dev": { - "nette/tester": "~2.0", + "nette/tester": "^2.0", "tracy/tracy": "^2.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.5-dev" } }, "autoload": { @@ -440,7 +440,7 @@ "iterator", "nette" ], - "time": "2018-06-28T11:49:23+00:00" + "time": "2019-02-28T18:13:25+00:00" }, { "name": "nette/neon", @@ -567,16 +567,16 @@ }, { "name": "nette/robot-loader", - "version": "v3.1.0", + "version": "v3.1.1", "source": { "type": "git", "url": "https://github.com/nette/robot-loader.git", - "reference": "fc76c70e740b10f091e502b2e393d0be912f38d4" + "reference": "3e8d75d6d976e191bdf46752ca40a286671219d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/robot-loader/zipball/fc76c70e740b10f091e502b2e393d0be912f38d4", - "reference": "fc76c70e740b10f091e502b2e393d0be912f38d4", + "url": "https://api.github.com/repos/nette/robot-loader/zipball/3e8d75d6d976e191bdf46752ca40a286671219d2", + "reference": "3e8d75d6d976e191bdf46752ca40a286671219d2", "shasum": "" }, "require": { @@ -628,7 +628,7 @@ "nette", "trait" ], - "time": "2018-08-13T14:19:06+00:00" + "time": "2019-03-01T20:23:02+00:00" }, { "name": "nette/utils", @@ -861,16 +861,16 @@ }, { "name": "phpstan/phpstan", - "version": "0.11.2", + "version": "0.11.3", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "8e185a74004920419ee97bf9dc62e6a175e8dca5" + "reference": "e4644b4a8fd393c346f1137305fb2f76a7dc20a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8e185a74004920419ee97bf9dc62e6a175e8dca5", - "reference": "8e185a74004920419ee97bf9dc62e6a175e8dca5", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e4644b4a8fd393c346f1137305fb2f76a7dc20a7", + "reference": "e4644b4a8fd393c346f1137305fb2f76a7dc20a7", "shasum": "" }, "require": { @@ -930,7 +930,7 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", - "time": "2019-02-12T14:54:38+00:00" + "time": "2019-03-10T16:25:30+00:00" }, { "name": "psr/log", @@ -981,16 +981,16 @@ }, { "name": "symfony/console", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4" + "reference": "9dc2299a016497f9ee620be94524e6c0af0280a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4", - "reference": "1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4", + "url": "https://api.github.com/repos/symfony/console/zipball/9dc2299a016497f9ee620be94524e6c0af0280a9", + "reference": "9dc2299a016497f9ee620be94524e6c0af0280a9", "shasum": "" }, "require": { @@ -1049,7 +1049,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2019-01-25T14:35:16+00:00" + "time": "2019-02-23T15:17:42+00:00" }, { "name": "symfony/contracts", @@ -1121,16 +1121,16 @@ }, { "name": "symfony/finder", - "version": "v4.2.3", + "version": "v4.2.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c" + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ef71816cbb264988bb57fe6a73f610888b9aa70c", - "reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c", + "url": "https://api.github.com/repos/symfony/finder/zipball/267b7002c1b70ea80db0833c3afe05f0fbde580a", + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a", "shasum": "" }, "require": { @@ -1166,7 +1166,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2019-01-16T20:35:37+00:00" + "time": "2019-02-23T15:42:05+00:00" }, { "name": "symfony/polyfill-mbstring", diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index b514155b..d4cbd919 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -249,35 +249,35 @@ }, { "name": "nette/finder", - "version": "v2.4.2", - "version_normalized": "2.4.2.0", + "version": "v2.5.0", + "version_normalized": "2.5.0.0", "source": { "type": "git", "url": "https://github.com/nette/finder.git", - "reference": "ee951a656cb8ac622e5dd33474a01fd2470505a0" + "reference": "6be1b83ea68ac558aff189d640abe242e0306fe2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/finder/zipball/ee951a656cb8ac622e5dd33474a01fd2470505a0", - "reference": "ee951a656cb8ac622e5dd33474a01fd2470505a0", + "url": "https://api.github.com/repos/nette/finder/zipball/6be1b83ea68ac558aff189d640abe242e0306fe2", + "reference": "6be1b83ea68ac558aff189d640abe242e0306fe2", "shasum": "" }, "require": { - "nette/utils": "~2.4", - "php": ">=5.6.0" + "nette/utils": "^2.4 || ~3.0.0", + "php": ">=7.1" }, "conflict": { "nette/nette": "<2.2" }, "require-dev": { - "nette/tester": "~2.0", + "nette/tester": "^2.0", "tracy/tracy": "^2.3" }, - "time": "2018-06-28T11:49:23+00:00", + "time": "2019-02-28T18:13:25+00:00", "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.5-dev" } }, "installation-source": "dist", @@ -440,17 +440,17 @@ }, { "name": "nette/robot-loader", - "version": "v3.1.0", - "version_normalized": "3.1.0.0", + "version": "v3.1.1", + "version_normalized": "3.1.1.0", "source": { "type": "git", "url": "https://github.com/nette/robot-loader.git", - "reference": "fc76c70e740b10f091e502b2e393d0be912f38d4" + "reference": "3e8d75d6d976e191bdf46752ca40a286671219d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/robot-loader/zipball/fc76c70e740b10f091e502b2e393d0be912f38d4", - "reference": "fc76c70e740b10f091e502b2e393d0be912f38d4", + "url": "https://api.github.com/repos/nette/robot-loader/zipball/3e8d75d6d976e191bdf46752ca40a286671219d2", + "reference": "3e8d75d6d976e191bdf46752ca40a286671219d2", "shasum": "" }, "require": { @@ -466,7 +466,7 @@ "nette/tester": "^2.0", "tracy/tracy": "^2.3" }, - "time": "2018-08-13T14:19:06+00:00", + "time": "2019-03-01T20:23:02+00:00", "type": "library", "extra": { "branch-alias": { @@ -744,17 +744,17 @@ }, { "name": "phpstan/phpstan", - "version": "0.11.2", - "version_normalized": "0.11.2.0", + "version": "0.11.3", + "version_normalized": "0.11.3.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "8e185a74004920419ee97bf9dc62e6a175e8dca5" + "reference": "e4644b4a8fd393c346f1137305fb2f76a7dc20a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8e185a74004920419ee97bf9dc62e6a175e8dca5", - "reference": "8e185a74004920419ee97bf9dc62e6a175e8dca5", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e4644b4a8fd393c346f1137305fb2f76a7dc20a7", + "reference": "e4644b4a8fd393c346f1137305fb2f76a7dc20a7", "shasum": "" }, "require": { @@ -792,7 +792,7 @@ "slevomat/coding-standard": "^4.7.2", "squizlabs/php_codesniffer": "^3.3.2" }, - "time": "2019-02-12T14:54:38+00:00", + "time": "2019-03-10T16:25:30+00:00", "bin": [ "bin/phpstan" ], @@ -868,17 +868,17 @@ }, { "name": "symfony/console", - "version": "v4.2.3", - "version_normalized": "4.2.3.0", + "version": "v4.2.4", + "version_normalized": "4.2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4" + "reference": "9dc2299a016497f9ee620be94524e6c0af0280a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4", - "reference": "1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4", + "url": "https://api.github.com/repos/symfony/console/zipball/9dc2299a016497f9ee620be94524e6c0af0280a9", + "reference": "9dc2299a016497f9ee620be94524e6c0af0280a9", "shasum": "" }, "require": { @@ -907,7 +907,7 @@ "symfony/lock": "", "symfony/process": "" }, - "time": "2019-01-25T14:35:16+00:00", + "time": "2019-02-23T15:17:42+00:00", "type": "library", "extra": { "branch-alias": { @@ -1012,23 +1012,23 @@ }, { "name": "symfony/finder", - "version": "v4.2.3", - "version_normalized": "4.2.3.0", + "version": "v4.2.4", + "version_normalized": "4.2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c" + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ef71816cbb264988bb57fe6a73f610888b9aa70c", - "reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c", + "url": "https://api.github.com/repos/symfony/finder/zipball/267b7002c1b70ea80db0833c3afe05f0fbde580a", + "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a", "shasum": "" }, "require": { "php": "^7.1.3" }, - "time": "2019-01-16T20:35:37+00:00", + "time": "2019-02-23T15:42:05+00:00", "type": "library", "extra": { "branch-alias": { diff --git a/vendor/nette/finder/composer.json b/vendor/nette/finder/composer.json index 86da82e7..76f0cbff 100644 --- a/vendor/nette/finder/composer.json +++ b/vendor/nette/finder/composer.json @@ -15,11 +15,11 @@ } ], "require": { - "php": ">=5.6.0", - "nette/utils": "~2.4" + "php": ">=7.1", + "nette/utils": "^2.4 || ~3.0.0" }, "require-dev": { - "nette/tester": "~2.0", + "nette/tester": "^2.0", "tracy/tracy": "^2.3" }, "conflict": { @@ -31,7 +31,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.5-dev" } } } diff --git a/vendor/nette/finder/readme.md b/vendor/nette/finder/readme.md index 87778d17..d30149fd 100644 --- a/vendor/nette/finder/readme.md +++ b/vendor/nette/finder/readme.md @@ -7,18 +7,31 @@ Nette Finder: Files Searching [![Latest Stable Version](https://poser.pugx.org/nette/finder/v/stable)](https://github.com/nette/finder/releases) [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/finder/blob/master/license.md) -Class `Nette\Utils\Finder` makes browsing the directory structure really easy. + +Introduction +------------ + +Nette Finder makes browsing the directory structure really easy. + +Documentation can be found on the [website](https://doc.nette.org/finder). + +If you like Nette, **[please make a donation now](https://nette.org/donate)**. Thank you! -All examples assume the following class alias is defined: +Installation +------------ -```php -use Nette\Utils\Finder; +The recommended way to install is via Composer: + +``` +composer require nette/finder ``` +It requires PHP version 5.6 and supports PHP up to 7.3. The dev-master version requires PHP 7.1. -Searching for Files -------------------- + +Usage +----- How to find all `*.txt` files in `$dir` directory without recursing subdirectories? @@ -104,13 +117,13 @@ Depth of search can be limited using the `limitDepth()` method. Searching for directories ----------------- +------------------------- In addition to files, it is possible to search for directories using `Finder::findDirectories('subdir*')`, or to search for files and directories: `Finder::find('file.txt')`. Filtering ----------- +--------- You can also filter results. For example by size. This way we will traverse the files of size between 100B and 200B: @@ -151,7 +164,7 @@ foreach (Finder::findFiles('*') Connection to Amazon S3 ----------------------- +----------------------- It's possible to use custom streams, for example Zend_Service_Amazon_S3: diff --git a/vendor/nette/finder/src/Utils/Finder.php b/vendor/nette/finder/src/Utils/Finder.php index f9d2145f..e5b77dab 100644 --- a/vendor/nette/finder/src/Utils/Finder.php +++ b/vendor/nette/finder/src/Utils/Finder.php @@ -5,6 +5,8 @@ * Copyright (c) 2004 David Grudl (https://davidgrudl.com) */ +declare(strict_types=1); + namespace Nette\Utils; use Nette; @@ -26,6 +28,9 @@ class Finder implements \IteratorAggregate, \Countable { use Nette\SmartObject; + /** @var callable extension methods */ + private static $extMethods = []; + /** @var array */ private $paths = []; @@ -47,10 +52,10 @@ class Finder implements \IteratorAggregate, \Countable /** * Begins search for files matching mask and all directories. - * @param mixed + * @param string|string[] $masks * @return static */ - public static function find(...$masks) + public static function find(...$masks): self { $masks = $masks && is_array($masks[0]) ? $masks[0] : $masks; return (new static)->select($masks, 'isDir')->select($masks, 'isFile'); @@ -59,10 +64,10 @@ class Finder implements \IteratorAggregate, \Countable /** * Begins search for files matching mask. - * @param mixed + * @param string|string[] $masks * @return static */ - public static function findFiles(...$masks) + public static function findFiles(...$masks): self { $masks = $masks && is_array($masks[0]) ? $masks[0] : $masks; return (new static)->select($masks, 'isFile'); @@ -71,10 +76,10 @@ class Finder implements \IteratorAggregate, \Countable /** * Begins search for directories matching mask. - * @param mixed + * @param string|string[] $masks * @return static */ - public static function findDirectories(...$masks) + public static function findDirectories(...$masks): self { $masks = $masks && is_array($masks[0]) ? $masks[0] : $masks; return (new static)->select($masks, 'isDir'); @@ -83,31 +88,27 @@ class Finder implements \IteratorAggregate, \Countable /** * Creates filtering group by mask & type selector. - * @param array - * @param string * @return static */ - private function select($masks, $type) + private function select(array $masks, string $type): self { $this->cursor = &$this->groups[]; $pattern = self::buildPattern($masks); - if ($type || $pattern) { - $this->filter(function (RecursiveDirectoryIterator $file) use ($type, $pattern) { - return !$file->isDot() - && (!$type || $file->$type()) - && (!$pattern || preg_match($pattern, '/' . strtr($file->getSubPathName(), '\\', '/'))); - }); - } + $this->filter(function (RecursiveDirectoryIterator $file) use ($type, $pattern): bool { + return !$file->isDot() + && $file->$type() + && (!$pattern || preg_match($pattern, '/' . strtr($file->getSubPathName(), '\\', '/'))); + }); return $this; } /** - * Searchs in the given folder(s). - * @param string|array + * Searches in the given folder(s). + * @param string|string[] $paths * @return static */ - public function in(...$paths) + public function in(...$paths): self { $this->maxDepth = 0; return $this->from(...$paths); @@ -115,11 +116,11 @@ class Finder implements \IteratorAggregate, \Countable /** - * Searchs recursively from the given folder(s). - * @param string|array + * Searches recursively from the given folder(s). + * @param string|string[] $paths * @return static */ - public function from(...$paths) + public function from(...$paths): self { if ($this->paths) { throw new Nette\InvalidStateException('Directory to search has already been specified.'); @@ -134,7 +135,7 @@ class Finder implements \IteratorAggregate, \Countable * Shows folder content prior to the folder. * @return static */ - public function childFirst() + public function childFirst(): self { $this->order = RecursiveIteratorIterator::CHILD_FIRST; return $this; @@ -143,10 +144,8 @@ class Finder implements \IteratorAggregate, \Countable /** * Converts Finder pattern to regular expression. - * @param array - * @return string|null */ - private static function buildPattern($masks) + private static function buildPattern(array $masks): ?string { $pattern = []; foreach ($masks as $mask) { @@ -174,9 +173,8 @@ class Finder implements \IteratorAggregate, \Countable /** * Get the number of found files and/or directories. - * @return int */ - public function count() + public function count(): int { return iterator_count($this->getIterator()); } @@ -184,23 +182,20 @@ class Finder implements \IteratorAggregate, \Countable /** * Returns iterator. - * @return \Iterator */ - public function getIterator() + public function getIterator(): \Iterator { if (!$this->paths) { throw new Nette\InvalidStateException('Call in() or from() to specify directory to search.'); } elseif (count($this->paths) === 1) { - return $this->buildIterator($this->paths[0]); + return $this->buildIterator((string) $this->paths[0]); } else { $iterator = new \AppendIterator(); - $iterator->append($workaround = new \ArrayIterator(['workaround PHP bugs #49104, #63077'])); foreach ($this->paths as $path) { - $iterator->append($this->buildIterator($path)); + $iterator->append($this->buildIterator((string) $path)); } - unset($workaround[0]); return $iterator; } } @@ -208,18 +203,16 @@ class Finder implements \IteratorAggregate, \Countable /** * Returns per-path iterator. - * @param string - * @return \Iterator */ - private function buildIterator($path) + private function buildIterator(string $path): \Iterator { $iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::FOLLOW_SYMLINKS); if ($this->exclude) { - $iterator = new \RecursiveCallbackFilterIterator($iterator, function ($foo, $bar, RecursiveDirectoryIterator $file) { + $iterator = new \RecursiveCallbackFilterIterator($iterator, function ($foo, $bar, RecursiveDirectoryIterator $file): bool { if (!$file->isDot() && !$file->isFile()) { foreach ($this->exclude as $filter) { - if (!call_user_func($filter, $file)) { + if (!$filter($file)) { return false; } } @@ -233,14 +226,14 @@ class Finder implements \IteratorAggregate, \Countable $iterator->setMaxDepth($this->maxDepth); } - $iterator = new \CallbackFilterIterator($iterator, function ($foo, $bar, \Iterator $file) { + $iterator = new \CallbackFilterIterator($iterator, function ($foo, $bar, \Iterator $file): bool { while ($file instanceof \OuterIterator) { $file = $file->getInnerIterator(); } foreach ($this->groups as $filters) { foreach ($filters as $filter) { - if (!call_user_func($filter, $file)) { + if (!$filter($file)) { continue 2; } } @@ -259,15 +252,15 @@ class Finder implements \IteratorAggregate, \Countable /** * Restricts the search using mask. * Excludes directories from recursive traversing. - * @param mixed + * @param string|string[] $masks * @return static */ - public function exclude(...$masks) + public function exclude(...$masks): self { $masks = $masks && is_array($masks[0]) ? $masks[0] : $masks; $pattern = self::buildPattern($masks); if ($pattern) { - $this->filter(function (RecursiveDirectoryIterator $file) use ($pattern) { + $this->filter(function (RecursiveDirectoryIterator $file) use ($pattern): bool { return !preg_match($pattern, '/' . strtr($file->getSubPathName(), '\\', '/')); }); } @@ -277,10 +270,10 @@ class Finder implements \IteratorAggregate, \Countable /** * Restricts the search using callback. - * @param callable function (RecursiveDirectoryIterator $file) + * @param callable $callback function (RecursiveDirectoryIterator $file): bool * @return static */ - public function filter($callback) + public function filter(callable $callback): self { $this->cursor[] = $callback; return $this; @@ -289,10 +282,9 @@ class Finder implements \IteratorAggregate, \Countable /** * Limits recursion level. - * @param int * @return static */ - public function limitDepth($depth) + public function limitDepth(int $depth): self { $this->maxDepth = $depth; return $this; @@ -301,22 +293,21 @@ class Finder implements \IteratorAggregate, \Countable /** * Restricts the search by size. - * @param string "[operator] [size] [unit]" example: >=10kB - * @param int + * @param string $operator "[operator] [size] [unit]" example: >=10kB * @return static */ - public function size($operator, $size = null) + public function size(string $operator, int $size = null): self { if (func_num_args() === 1) { // in $operator is predicate if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?((?:\d*\.)?\d+)\s*(K|M|G|)B?\z#i', $operator, $matches)) { throw new Nette\InvalidArgumentException('Invalid size predicate format.'); } - list(, $operator, $size, $unit) = $matches; + [, $operator, $size, $unit] = $matches; static $units = ['' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9]; $size *= $units[strtolower($unit)]; $operator = $operator ?: '='; } - return $this->filter(function (RecursiveDirectoryIterator $file) use ($operator, $size) { + return $this->filter(function (RecursiveDirectoryIterator $file) use ($operator, $size): bool { return self::compare($file->getSize(), $operator, $size); }); } @@ -324,21 +315,21 @@ class Finder implements \IteratorAggregate, \Countable /** * Restricts the search by modified time. - * @param string "[operator] [date]" example: >1978-01-23 - * @param mixed + * @param string $operator "[operator] [date]" example: >1978-01-23 + * @param string|int|\DateTimeInterface $date * @return static */ - public function date($operator, $date = null) + public function date(string $operator, $date = null): self { if (func_num_args() === 1) { // in $operator is predicate if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?(.+)\z#i', $operator, $matches)) { throw new Nette\InvalidArgumentException('Invalid date predicate format.'); } - list(, $operator, $date) = $matches; + [, $operator, $date] = $matches; $operator = $operator ?: '='; } $date = DateTime::from($date)->format('U'); - return $this->filter(function (RecursiveDirectoryIterator $file) use ($operator, $date) { + return $this->filter(function (RecursiveDirectoryIterator $file) use ($operator, $date): bool { return self::compare($file->getMTime(), $operator, $date); }); } @@ -346,11 +337,8 @@ class Finder implements \IteratorAggregate, \Countable /** * Compares two values. - * @param mixed - * @param mixed - * @return bool */ - public static function compare($l, $operator, $r) + public static function compare($l, string $operator, $r): bool { switch ($operator) { case '>': @@ -377,17 +365,16 @@ class Finder implements \IteratorAggregate, \Countable /********************* extension methods ****************d*g**/ - public function __call($name, $args) + public function __call(string $name, array $args) { - if ($callback = Nette\Utils\ObjectMixin::getExtensionMethod(__CLASS__, $name)) { - return $callback($this, ...$args); - } - Nette\Utils\ObjectMixin::strictCall(__CLASS__, $name); + return isset(self::$extMethods[$name]) + ? (self::$extMethods[$name])($this, ...$args) + : parent::__call($name, $args); } - public static function extensionMethod($name, $callback) + public static function extensionMethod(string $name, callable $callback): void { - Nette\Utils\ObjectMixin::setExtensionMethod(__CLASS__, $name, $callback); + self::$extMethods[$name] = $callback; } } diff --git a/vendor/nette/robot-loader/readme.md b/vendor/nette/robot-loader/readme.md index 7339da97..6d46626f 100644 --- a/vendor/nette/robot-loader/readme.md +++ b/vendor/nette/robot-loader/readme.md @@ -14,11 +14,13 @@ Introduction RobotLoader is a tool that gives you comfort of automated class loading for your entire application including third-party libraries. - get rid of all `require` -- only necessary scripts are loaded - requires no strict file naming conventions - allows more classes in single file +- extremely fast +- no manual cache updates, everything runs automatically +- highly mature, stable and widely used library -RobotLoader is extremely comfortable and addictive! +RobotLoader is incredibly comfortable and addictive! If you like Nette, **[please make a donation now](https://nette.org/donate)**. Thank you! @@ -46,7 +48,7 @@ The recommended way to install is via Composer: composer require nette/robot-loader ``` -It requires PHP version 5.6 and supports PHP up to 7.2. +It requires PHP version 5.6 and supports PHP up to 7.3. Usage @@ -76,3 +78,36 @@ This feature should be disabled on production server. If you want RobotLoader to skip some directory, use `$loader->excludeDirectory('temp')`. By default, RobotLoader reports errors in PHP files by throwing exception `ParseError` (since PHP 7.0). It can be disabled via `$loader->reportParseErrors(false)`. + + +PHP files analyzer +------------------ + +RobotLoader can also be used to find classes, interfaces, and trait in PHP files without using the autoloading feature: + +```php +$loader = new Nette\Loaders\RobotLoader; +$loader->addDirectory(__DIR__ . '/app'); + +// Scans directories for classes / intefaces / traits +$loader->rebuild(); + +// Returns array of class => filename pairs +$res = $loader->getIndexedClasses(); +``` + +When scanning files again, we can use the cache and unmodified files will not be analyzed repeatedly: + +```php +$loader = new Nette\Loaders\RobotLoader; +$loader->addDirectory(__DIR__ . '/app'); +$loader->setTempDirectory(__DIR__ . '/temp'); + +// Scans directories using a cache +$loader->refresh(); + +// Returns array of class => filename pairs +$res = $loader->getIndexedClasses(); +``` + +Enjoy RobotLoader! diff --git a/vendor/nette/robot-loader/src/RobotLoader/RobotLoader.php b/vendor/nette/robot-loader/src/RobotLoader/RobotLoader.php index 427ee59e..a6785188 100644 --- a/vendor/nette/robot-loader/src/RobotLoader/RobotLoader.php +++ b/vendor/nette/robot-loader/src/RobotLoader/RobotLoader.php @@ -28,10 +28,10 @@ class RobotLoader const RETRY_LIMIT = 3; - /** @var array comma separated wildcards */ + /** @var array */ public $ignoreDirs = ['.*', '*.old', '*.bak', '*.tmp', 'temp']; - /** @var array comma separated wildcards */ + /** @var array */ public $acceptFiles = ['*.php']; /** @var bool */ @@ -95,7 +95,7 @@ class RobotLoader $missing = &$this->missing[$type]; $missing++; if (!$this->refreshed && $missing <= self::RETRY_LIMIT) { - $this->refresh(); + $this->refreshClasses(); $this->saveCache(); } elseif ($info) { unset($this->classes[$type]); @@ -171,7 +171,8 @@ class RobotLoader */ public function rebuild() { - $this->refresh(); + $this->classes = $this->missing = []; + $this->refreshClasses(); if ($this->tempDirectory) { $this->saveCache(); } @@ -179,12 +180,26 @@ class RobotLoader /** - * Refreshes class list. + * Refreshes class list cache. * @return void */ - private function refresh() + public function refresh() { - $this->refreshed = true; // prevents calling refresh() or updateFile() in tryLoad() + $this->loadCache(); + if (!$this->refreshed) { + $this->refreshClasses(); + $this->saveCache(); + } + } + + + /** + * Refreshes $classes. + * @return void + */ + private function refreshClasses() + { + $this->refreshed = true; // prevents calling refreshClasses() or updateFile() in tryLoad() $files = []; foreach ($this->classes as $class => $info) { $files[$info['file']]['time'] = $info['time']; @@ -193,7 +208,8 @@ class RobotLoader $this->classes = []; foreach ($this->scanPaths as $path) { - foreach (is_file($path) ? [new SplFileInfo($path)] : $this->createFileIterator($path) as $file) { + $iterator = is_file($path) ? [new SplFileInfo($path)] : $this->createFileIterator($path); + foreach ($iterator as $file) { $file = $file->getPathname(); if (isset($files[$file]) && $files[$file]['time'] == filemtime($file)) { $classes = $files[$file]['classes']; @@ -234,7 +250,8 @@ class RobotLoader } } - $iterator = Nette\Utils\Finder::findFiles(is_array($this->acceptFiles) ? $this->acceptFiles : preg_split('#[,\s]+#', $this->acceptFiles)) + $acceptFiles = is_array($this->acceptFiles) ? $this->acceptFiles : preg_split('#[,\s]+#', $this->acceptFiles); + $iterator = Nette\Utils\Finder::findFiles($acceptFiles) ->filter(function (SplFileInfo $file) use (&$disallow) { return !isset($disallow[str_replace('\\', '/', $file->getRealPath())]); }) @@ -419,9 +436,7 @@ class RobotLoader list($this->classes, $this->missing) = @include $file; // @ file may not exist if (!is_array($this->classes)) { - $this->classes = []; - $this->refresh(); - $this->saveCache(); + $this->rebuild(); } flock($handle, LOCK_UN); diff --git a/vendor/ocramius/package-versions/src/PackageVersions/Versions.php b/vendor/ocramius/package-versions/src/PackageVersions/Versions.php index 00a5219e..a4f9072c 100644 --- a/vendor/ocramius/package-versions/src/PackageVersions/Versions.php +++ b/vendor/ocramius/package-versions/src/PackageVersions/Versions.php @@ -19,22 +19,22 @@ final class Versions 'jean85/pretty-package-versions' => '1.2@75c7effcf3f77501d0e0caa75111aff4daa0dd48', 'nette/bootstrap' => 'v2.4.6@268816e3f1bb7426c3a4ceec2bd38a036b532543', 'nette/di' => 'v2.4.15@d0561b8f77e8ef2ed6d83328860e16c81a5a8649', - 'nette/finder' => 'v2.4.2@ee951a656cb8ac622e5dd33474a01fd2470505a0', + 'nette/finder' => 'v2.5.0@6be1b83ea68ac558aff189d640abe242e0306fe2', 'nette/neon' => 'v3.0.0@cbff32059cbdd8720deccf9e9eace6ee516f02eb', 'nette/php-generator' => 'v3.2.1@9de4e093a130f7a1bd175198799ebc0efbac6924', - 'nette/robot-loader' => 'v3.1.0@fc76c70e740b10f091e502b2e393d0be912f38d4', + 'nette/robot-loader' => 'v3.1.1@3e8d75d6d976e191bdf46752ca40a286671219d2', 'nette/utils' => 'v2.5.3@17b9f76f2abd0c943adfb556e56f2165460b15ce', 'nikic/php-parser' => 'v4.2.1@5221f49a608808c1e4d436df32884cbc1b821ac0', 'ocramius/package-versions' => '1.4.0@a4d4b60d0e60da2487bd21a2c6ac089f85570dbb', 'phpstan/phpdoc-parser' => '0.3.1@2cc49f47c69b023eaf05b48e6529389893b13d74', - 'phpstan/phpstan' => '0.11.2@8e185a74004920419ee97bf9dc62e6a175e8dca5', + 'phpstan/phpstan' => '0.11.3@e4644b4a8fd393c346f1137305fb2f76a7dc20a7', 'psr/log' => '1.1.0@6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd', - 'symfony/console' => 'v4.2.3@1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4', + 'symfony/console' => 'v4.2.4@9dc2299a016497f9ee620be94524e6c0af0280a9', 'symfony/contracts' => 'v1.0.2@1aa7ab2429c3d594dd70689604b5cf7421254cdf', - 'symfony/finder' => 'v4.2.3@ef71816cbb264988bb57fe6a73f610888b9aa70c', + 'symfony/finder' => 'v4.2.4@267b7002c1b70ea80db0833c3afe05f0fbde580a', 'symfony/polyfill-mbstring' => 'v1.10.0@c79c051f5b3a46be09205c73b80b346e4153e494', 'thecodingmachine/phpstan-safe-rule' => 'v0.1.3@00f4845905feb5240ca62fb799e3c51ba85c9230', - '__root__' => 'dev-master@04a956886ab327ddbe5eec546b911b9e55a0e5ef', + '__root__' => 'dev-master@58cc098058143344a846f01a5d2252a45e2be9ba', ); private function __construct() diff --git a/vendor/phpstan/phpstan/README.md b/vendor/phpstan/phpstan/README.md index 6de8f5e7..6a9756c5 100644 --- a/vendor/phpstan/phpstan/README.md +++ b/vendor/phpstan/phpstan/README.md @@ -27,11 +27,12 @@ can be checked before you run the actual line. Mike Pretzlaw     TheCodingMachine -    - Check out [PHPStan's Patreon](https://www.patreon.com/phpstan) for sponsoring options. One-time donations [through PayPal](https://paypal.me/phpstan) are also accepted. To request an invoice, [contact me](mailto:ondrej@mirtes.cz) through e-mail. +BTC: bc1qd5s06wjtf8rzag08mk3s264aekn52jze9zeapt +
LTC: LSU5xLsWEfrVx1P9yJwmhziHAXikiE8xtC + ## Prerequisites PHPStan requires PHP >= 7.1. You have to run it in environment with PHP 7.x but the actual code does not have to use @@ -119,6 +120,7 @@ Unofficial extensions for other frameworks and libraries are also available: * [Yii2](https://github.com/proget-hq/phpstan-yii2) * [PhpSpec](https://github.com/proget-hq/phpstan-phpspec) * [TYPO3](https://github.com/sascha-egerer/phpstan-typo3) +* [moneyphp/money](https://github.com/JohnstonCode/phpstan-moneyphp) New extensions are becoming available on a regular basis! @@ -406,6 +408,8 @@ You can pass the following keywords to the `--error-format=X` parameter in order - `table`: Default. Grouped errors by file, colorized. For human consumption. - `raw`: Contains one error per line, with path to file, line number, and error description - `checkstyle`: Creates a checkstyle.xml compatible output. Note that you'd have to redirect output into a file in order to capture the results for later processing. +- `json`: Creates minified .json output without whitespaces. Note that you'd have to redirect output into a file in order to capture the results for later processing. +- `prettyJson`: Creates human readable .json output with whitespaces and indentations. Note that you'd have to redirect output into a file in order to capture the results for later processing. ## Class reflection extensions diff --git a/vendor/phpstan/phpstan/conf/config.level0.neon b/vendor/phpstan/phpstan/conf/config.level0.neon index d5f8608e..ecf36140 100644 --- a/vendor/phpstan/phpstan/conf/config.level0.neon +++ b/vendor/phpstan/phpstan/conf/config.level0.neon @@ -21,7 +21,8 @@ rules: - PHPStan\Rules\Functions\PrintfParametersRule - PHPStan\Rules\Functions\UnusedClosureUsesRule - PHPStan\Rules\Methods\ExistingClassesInTypehintsRule - - PHPStan\Rules\Properties\AccessStaticPropertiesRule + - PHPStan\Rules\Properties\AccessPropertiesInAssignRule + - PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule - PHPStan\Rules\Variables\ThisVariableRule services: @@ -90,6 +91,11 @@ services: arguments: reportMagicProperties: %reportMagicProperties% + - + class: PHPStan\Rules\Properties\AccessStaticPropertiesRule + tags: + - phpstan.rules.rule + - class: PHPStan\Rules\Properties\ExistingClassesInPropertiesRule tags: diff --git a/vendor/phpstan/phpstan/conf/config.neon b/vendor/phpstan/phpstan/conf/config.neon index a25a32b3..067e760e 100644 --- a/vendor/phpstan/phpstan/conf/config.neon +++ b/vendor/phpstan/phpstan/conf/config.neon @@ -379,6 +379,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\FilterVarDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension tags: diff --git a/vendor/phpstan/phpstan/src/Analyser/EnsuredNonNullabilityResult.php b/vendor/phpstan/phpstan/src/Analyser/EnsuredNonNullabilityResult.php new file mode 100644 index 00000000..7235e40e --- /dev/null +++ b/vendor/phpstan/phpstan/src/Analyser/EnsuredNonNullabilityResult.php @@ -0,0 +1,37 @@ +scope = $scope; + $this->specifiedExpressions = $specifiedExpressions; + } + + public function getScope(): Scope + { + return $this->scope; + } + + /** + * @return EnsuredNonNullabilityResultExpression[] + */ + public function getSpecifiedExpressions(): array + { + return $this->specifiedExpressions; + } + +} diff --git a/vendor/phpstan/phpstan/src/Analyser/EnsuredNonNullabilityResultExpression.php b/vendor/phpstan/phpstan/src/Analyser/EnsuredNonNullabilityResultExpression.php new file mode 100644 index 00000000..e182d54a --- /dev/null +++ b/vendor/phpstan/phpstan/src/Analyser/EnsuredNonNullabilityResultExpression.php @@ -0,0 +1,33 @@ +expression = $expression; + $this->originalType = $originalType; + } + + public function getExpression(): Expr + { + return $this->expression; + } + + public function getOriginalType(): Type + { + return $this->originalType; + } + +} diff --git a/vendor/phpstan/phpstan/src/Analyser/ExpressionContext.php b/vendor/phpstan/phpstan/src/Analyser/ExpressionContext.php new file mode 100644 index 00000000..e71da9f8 --- /dev/null +++ b/vendor/phpstan/phpstan/src/Analyser/ExpressionContext.php @@ -0,0 +1,69 @@ +isDeep = $isDeep; + $this->inAssignRightSideVariableName = $inAssignRightSideVariableName; + $this->inAssignRightSideType = $inAssignRightSideType; + } + + public static function createTopLevel(): self + { + return new self(false, null, null); + } + + public static function createDeep(): self + { + return new self(true, null, null); + } + + public function enterDeep(): self + { + if ($this->isDeep) { + return $this; + } + + return new self(true, $this->inAssignRightSideVariableName, $this->inAssignRightSideType); + } + + public function isDeep(): bool + { + return $this->isDeep; + } + + public function enterRightSideAssign(string $variableName, Type $type): self + { + return new self($this->isDeep, $variableName, $type); + } + + public function getInAssignRightSideVariableName(): ?string + { + return $this->inAssignRightSideVariableName; + } + + public function getInAssignRightSideType(): ?Type + { + return $this->inAssignRightSideType; + } + +} diff --git a/vendor/phpstan/phpstan/src/Analyser/ExpressionResult.php b/vendor/phpstan/phpstan/src/Analyser/ExpressionResult.php new file mode 100644 index 00000000..42e5c89c --- /dev/null +++ b/vendor/phpstan/phpstan/src/Analyser/ExpressionResult.php @@ -0,0 +1,74 @@ +scope = $scope; + $this->truthyScopeCallback = $truthyScopeCallback; + $this->falseyScopeCallback = $falseyScopeCallback; + } + + public function getScope(): Scope + { + return $this->scope; + } + + public function getTruthyScope(): Scope + { + if ($this->truthyScopeCallback === null) { + return $this->scope; + } + + if ($this->truthyScope !== null) { + return $this->truthyScope; + } + + $callback = $this->truthyScopeCallback; + $this->truthyScope = $callback(); + return $this->truthyScope; + } + + public function getFalseyScope(): Scope + { + if ($this->falseyScopeCallback === null) { + return $this->scope; + } + + if ($this->falseyScope !== null) { + return $this->falseyScope; + } + + $callback = $this->falseyScopeCallback; + $this->falseyScope = $callback(); + return $this->falseyScope; + } + +} diff --git a/vendor/phpstan/phpstan/src/Analyser/LookForAssignsSettings.php b/vendor/phpstan/phpstan/src/Analyser/LookForAssignsSettings.php deleted file mode 100644 index 488b6d4a..00000000 --- a/vendor/phpstan/phpstan/src/Analyser/LookForAssignsSettings.php +++ /dev/null @@ -1,119 +0,0 @@ -respectEarlyTermination = $respectEarlyTermination; - } - - public static function default(): self - { - return self::create(self::EARLY_TERMINATION_ALL); - } - - public static function insideLoop(): self - { - return self::create(self::EARLY_TERMINATION_STOP + self::EARLY_TERMINATION_BREAK + self::REPEAT_ANALYSIS); - } - - public static function afterLoop(): self - { - return self::create(self::EARLY_TERMINATION_STOP + self::REPEAT_ANALYSIS); - } - - public static function afterSwitch(): self - { - return self::create(self::EARLY_TERMINATION_STOP); - } - - public static function insideFinally(): self - { - return self::create(0); - } - - public static function insideClosure(): self - { - return self::create(self::EARLY_TERMINATION_CLOSURE); - } - - private static function create(int $value): self - { - self::$registry[$value] = self::$registry[$value] ?? new self($value); - return self::$registry[$value]; - } - - public function shouldRepeatAnalysis(): bool - { - return ($this->respectEarlyTermination & self::REPEAT_ANALYSIS) === self::REPEAT_ANALYSIS; - } - - public function shouldSkipBranch(\PhpParser\Node $earlyTerminationStatement): bool - { - return $this->isRespected($earlyTerminationStatement); - } - - private function isRespected(\PhpParser\Node $earlyTerminationStatement): bool - { - if ( - $earlyTerminationStatement instanceof Break_ - ) { - return ($this->respectEarlyTermination & self::EARLY_TERMINATION_BREAK) === self::EARLY_TERMINATION_BREAK; - } - - if ( - $earlyTerminationStatement instanceof Continue_ - ) { - return ($this->respectEarlyTermination & self::EARLY_TERMINATION_CONTINUE) === self::EARLY_TERMINATION_CONTINUE; - } - - return ($this->respectEarlyTermination & self::EARLY_TERMINATION_STOP) === self::EARLY_TERMINATION_STOP; - } - - public function shouldIntersectVariables(?\PhpParser\Node $earlyTerminationStatement): bool - { - if ($earlyTerminationStatement === null) { - return true; - } - - if ($this->shouldSkipBranch($earlyTerminationStatement)) { - throw new \PHPStan\ShouldNotHappenException(); - } - - return $earlyTerminationStatement instanceof Break_ - || $earlyTerminationStatement instanceof Continue_ - || ($this->respectEarlyTermination & self::EARLY_TERMINATION_STOP) === 0; - } - - public function shouldGeneralizeConstantTypesOfNonIdempotentOperations(): bool - { - return ( - ($this->respectEarlyTermination & self::EARLY_TERMINATION_STOP) === self::EARLY_TERMINATION_STOP - && $this->respectEarlyTermination !== self::EARLY_TERMINATION_ALL - ) || $this->respectEarlyTermination === self::EARLY_TERMINATION_CLOSURE; - } - -} diff --git a/vendor/phpstan/phpstan/src/Analyser/NodeScopeResolver.php b/vendor/phpstan/phpstan/src/Analyser/NodeScopeResolver.php index 29f509ee..cb910b26 100644 --- a/vendor/phpstan/phpstan/src/Analyser/NodeScopeResolver.php +++ b/vendor/phpstan/phpstan/src/Analyser/NodeScopeResolver.php @@ -3,7 +3,6 @@ namespace PHPStan\Analyser; use PhpParser\Node; -use PhpParser\Node\Arg; use PhpParser\Node\Expr; use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\ArrayDimFetch; @@ -20,11 +19,9 @@ use PhpParser\Node\Expr\ErrorSuppress; use PhpParser\Node\Expr\Exit_; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\Instanceof_; -use PhpParser\Node\Expr\Isset_; use PhpParser\Node\Expr\List_; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\New_; -use PhpParser\Node\Expr\Print_; use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Expr\StaticPropertyFetch; @@ -32,7 +29,6 @@ use PhpParser\Node\Expr\Ternary; use PhpParser\Node\Expr\Variable; use PhpParser\Node\Name; use PhpParser\Node\Stmt\Break_; -use PhpParser\Node\Stmt\Catch_; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Continue_; use PhpParser\Node\Stmt\Do_; @@ -54,21 +50,22 @@ use PHPStan\Node\InClassMethodNode; use PHPStan\Parser\Parser; use PHPStan\PhpDoc\PhpDocBlock; use PHPStan\PhpDoc\Tag\ParamTag; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\ParametersAcceptor; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; +use PHPStan\Type\ClosureType; use PHPStan\Type\CommentHelper; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; -use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ErrorType; use PHPStan\Type\FileTypeMapper; use PHPStan\Type\IntegerType; use PHPStan\Type\MixedType; -use PHPStan\Type\ObjectType; +use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -78,6 +75,9 @@ use PHPStan\Type\UnionType; class NodeScopeResolver { + private const LOOP_SCOPE_ITERATIONS = 3; + private const GENERALIZE_AFTER_ITERATION = 1; + /** @var \PHPStan\Broker\Broker */ private $broker; @@ -105,9 +105,6 @@ class NodeScopeResolver /** @var string[][] className(string) => methods(string[]) */ private $earlyTerminatingMethodCalls; - /** @var \PHPStan\Reflection\ClassReflection|null */ - private $anonymousClassReflection; - /** @var bool[] filePath(string) => bool(true) */ private $analysedFiles; @@ -157,1037 +154,1010 @@ class NodeScopeResolver * @param \PhpParser\Node[] $nodes * @param \PHPStan\Analyser\Scope $scope * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param \PHPStan\Analyser\Scope $closureBindScope */ public function processNodes( array $nodes, Scope $scope, - \Closure $nodeCallback, - ?Scope $closureBindScope = null + \Closure $nodeCallback ): void { - $nodesCount = count($nodes); - /** @var \PhpParser\Node|string $node */ - foreach ($nodes as $i => $node) { - if (!($node instanceof \PhpParser\Node)) { + foreach ($nodes as $node) { + if (!$node instanceof Node\Stmt) { continue; } - if ($scope->getInFunctionCall() !== null && $node instanceof Arg) { - $functionCall = $scope->getInFunctionCall(); - $value = $node->value; - $parametersAcceptor = $this->findParametersAcceptorInFunctionCall($functionCall, $scope); + $statementResult = $this->processStmtNode($node, $scope, $nodeCallback); + /*if ($statementResult->isAlwaysTerminating()) { + // todo virtual dead code node + //break; + }*/ - if ($parametersAcceptor !== null) { - $parameters = $parametersAcceptor->getParameters(); - $assignByReference = false; - if (isset($parameters[$i])) { - $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable(); - } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { - $lastParameter = $parameters[count($parameters) - 1]; - $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); - } - if ($assignByReference && $value instanceof Variable && is_string($value->name)) { - $scope = $scope->assignVariable($value->name, new MixedType(), TrinaryLogic::createYes()); - } - } - } - - $nodeScope = $scope; - if ($i === 0 && $closureBindScope !== null) { - $nodeScope = $closureBindScope; - } - - $this->processNode($node, $nodeScope, $nodeCallback); - - if ($i === $nodesCount - 1) { - break; - } - $scope = $this->lookForAssigns( - $scope, - $node, - TrinaryLogic::createYes(), - LookForAssignsSettings::default() - ); - - if ($node instanceof If_) { - if ($this->findEarlyTermination($node->stmts, $scope) !== null) { - $scope = $scope->filterByFalseyValue($node->cond); - $this->processNode($node->cond, $scope, function (Node $node, Scope $inScope) use (&$scope): void { - $this->specifyFetchedPropertyForInnerScope($node, $inScope, true, $scope); - }); - } - } elseif ($node instanceof Node\Stmt\Declare_) { - foreach ($node->declares as $declare) { - if ( - $declare->key->name === 'strict_types' - && $declare->value instanceof Node\Scalar\LNumber - && $declare->value->value === 1 - ) { - $scope = $scope->enterDeclareStrictTypes(); - break; - } - } - } elseif ($node instanceof Node\Stmt\Expression) { - $scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition( - $scope, - $node->expr, - TypeSpecifierContext::createNull() - )); - } + $scope = $statementResult->getScope(); } } - private function specifyProperty(Scope $scope, Expr $expr): Scope - { - if ( - $expr instanceof PropertyFetch - && $expr->name instanceof Node\Identifier - ) { - return $scope->filterByTruthyValue( - new FuncCall( - new Node\Name('property_exists'), - [ - new Arg($expr->var), - new Arg(new Node\Scalar\String_($expr->name->name)), - ] - ) - ); - } elseif ( - $expr instanceof Expr\StaticPropertyFetch - ) { - if ( - $expr->class instanceof Name - ) { - if ((string) $expr->class === 'static') { - return $scope->specifyFetchedStaticPropertyFromIsset($expr); - } - } elseif ($expr->name instanceof Node\VarLikeIdentifier) { - return $scope->filterByTruthyValue( - new FuncCall( - new Node\Name('property_exists'), - [ - new Arg($expr->class), - new Arg(new Node\Scalar\String_($expr->name->name)), - ] - ) - ); - } - } - - return $scope; - } - - private function specifyFetchedPropertyForInnerScope(Node $node, Scope $inScope, bool $inEarlyTermination, Scope &$scope): void - { - if ($inEarlyTermination === $inScope->isNegated()) { - if ($node instanceof Isset_) { - foreach ($node->vars as $var) { - $scope = $this->specifyProperty($scope, $var); - } - } - } else { - if ($node instanceof Expr\Empty_) { - $scope = $this->specifyProperty($scope, $node->expr); - } - } - } - - private function lookForArrayDestructuringArray(Scope $scope, Node $node, Type $valueType): Scope - { - if ($node instanceof Array_ || $node instanceof List_) { - foreach ($node->items as $key => $item) { - /** @var \PhpParser\Node\Expr\ArrayItem|null $itemValue */ - $itemValue = $item; - if ($itemValue === null) { - continue; - } - - $keyType = $itemValue->key === null ? new ConstantIntegerType($key) : $scope->getType($itemValue->key); - $scope = $this->specifyItemFromArrayDestructuring($scope, $itemValue, $valueType, $keyType); - } - } elseif ($node instanceof Variable && is_string($node->name)) { - $scope = $scope->assignVariable($node->name, new MixedType(), TrinaryLogic::createYes()); - } elseif ($node instanceof ArrayDimFetch && $node->var instanceof Variable && is_string($node->var->name)) { - $scope = $scope->assignVariable( - $node->var->name, - new MixedType(), - TrinaryLogic::createYes() - ); - } - - return $scope; - } - - private function specifyItemFromArrayDestructuring(Scope $scope, ArrayItem $arrayItem, Type $valueType, Type $keyType): Scope - { - $type = $valueType->getOffsetValueType($keyType); - - $itemNode = $arrayItem->value; - if ($itemNode instanceof Variable && is_string($itemNode->name)) { - $scope = $scope->assignVariable($itemNode->name, $type, TrinaryLogic::createYes()); - } elseif ($itemNode instanceof ArrayDimFetch && $itemNode->var instanceof Variable && is_string($itemNode->var->name)) { - $currentType = $scope->hasVariableType($itemNode->var->name)->no() - ? new ConstantArrayType([], []) - : $scope->getVariableType($itemNode->var->name); - $dimType = null; - if ($itemNode->dim !== null) { - $dimType = $scope->getType($itemNode->dim); - } - $scope = $scope->assignVariable( - $itemNode->var->name, - $currentType->setOffsetValueType($dimType, $type), - TrinaryLogic::createYes() - ); - } else { - $scope = $this->lookForArrayDestructuringArray($scope, $itemNode, $type); - } - - return $scope; - } - - private function enterForeach(Scope $scope, Foreach_ $node): Scope - { - if ($node->keyVar !== null && $node->keyVar instanceof Variable && is_string($node->keyVar->name)) { - $scope = $scope->assignVariable($node->keyVar->name, new MixedType(), TrinaryLogic::createYes()); - } - - $comment = CommentHelper::getDocComment($node); - if ($node->valueVar instanceof Variable && is_string($node->valueVar->name)) { - $scope = $scope->enterForeach( - $node->expr, - $node->valueVar->name, - $node->keyVar !== null - && $node->keyVar instanceof Variable - && is_string($node->keyVar->name) - ? $node->keyVar->name - : null - ); - if ($comment !== null) { - $scope = $this->processVarAnnotation($scope, $node->valueVar->name, $comment, true); - } - } - - if ( - $node->keyVar instanceof Variable && is_string($node->keyVar->name) - && $comment !== null - ) { - $scope = $this->processVarAnnotation($scope, $node->keyVar->name, $comment, true); - } - - if ($node->valueVar instanceof List_ || $node->valueVar instanceof Array_) { - $itemTypes = []; - $exprType = $scope->getType($node->expr); - $arrayTypes = TypeUtils::getArrays($exprType); - foreach ($arrayTypes as $arrayType) { - $itemTypes[] = $arrayType->getItemType(); - } - - $itemType = count($itemTypes) > 0 ? TypeCombinator::union(...$itemTypes) : new MixedType(); - $scope = $this->lookForArrayDestructuringArray($scope, $node->valueVar, $itemType); - } - - return $this->lookForAssigns($scope, $node->valueVar, TrinaryLogic::createYes(), LookForAssignsSettings::default()); - } - /** - * @param \PhpParser\Node $node - * @param Scope $scope + * @param \PhpParser\Node\Stmt[] $stmts + * @param \PHPStan\Analyser\Scope $scope * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback - * @param bool $stopImmediately + * @return StatementResult */ - private function processNode(\PhpParser\Node $node, Scope $scope, \Closure $nodeCallback, bool $stopImmediately = false): void + public function processStmtNodes( + array $stmts, + Scope $scope, + \Closure $nodeCallback + ): StatementResult { - $nodeCallback($node, $scope); - if ($stopImmediately) { - return; + $exitPoints = []; + $alwaysTerminatingStatements = []; + $alreadyTerminated = false; + foreach ($stmts as $stmt) { + $statementResult = $this->processStmtNode($stmt, $scope, $nodeCallback); + $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints()); + + if ($statementResult->isAlwaysTerminating() && !$alreadyTerminated) { + $alwaysTerminatingStatements = array_merge($alwaysTerminatingStatements, $statementResult->getAlwaysTerminatingStatements()); + $alreadyTerminated = true; + // todo break; + } + + $scope = $statementResult->getScope(); } - if ( - $node instanceof \PhpParser\Node\Stmt\ClassLike - ) { - if ($node instanceof Node\Stmt\Trait_) { - return; + return new StatementResult($scope, $alwaysTerminatingStatements, $exitPoints); + } + + /** + * @param \PhpParser\Node\Stmt $stmt + * @param \PHPStan\Analyser\Scope $scope + * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @return StatementResult + */ + private function processStmtNode( + Node\Stmt $stmt, + Scope $scope, + \Closure $nodeCallback + ): StatementResult + { + $nodeCallback($stmt, $scope); + + // todo handle all stmt descendants + if ($stmt instanceof Node\Stmt\Declare_) { + foreach ($stmt->declares as $declare) { + $nodeCallback($declare, $scope); + $nodeCallback($declare->value, $scope); + if ( + $declare->key->name !== 'strict_types' + || !($declare->value instanceof Node\Scalar\LNumber) + || $declare->value->value !== 1 + ) { + continue; + } + + $scope = $scope->enterDeclareStrictTypes(); } - if (isset($node->namespacedName)) { - $scope = $scope->enterClass($this->broker->getClass((string) $node->namespacedName)); - } elseif ($this->anonymousClassReflection !== null) { - $scope = $scope->enterAnonymousClass($this->anonymousClassReflection); + } elseif ($stmt instanceof Node\Stmt\Function_) { + [$phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $isDeprecated, $isInternal, $isFinal] = $this->getPhpDocs($scope, $stmt); + + foreach ($stmt->params as $param) { + $this->processParamNode($param, $scope, $nodeCallback); + } + + if ($stmt->returnType !== null) { + $nodeCallback($stmt->returnType, $scope); + } + + $functionScope = $scope->enterFunction( + $stmt, + $phpDocParameterTypes, + $phpDocReturnType, + $phpDocThrowType, + $isDeprecated, + $isInternal, + $isFinal + ); + $this->processStmtNodes($stmt->stmts, $functionScope, $nodeCallback); + } elseif ($stmt instanceof Node\Stmt\ClassMethod) { + [$phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $isDeprecated, $isInternal, $isFinal] = $this->getPhpDocs($scope, $stmt); + + foreach ($stmt->params as $param) { + $this->processParamNode($param, $scope, $nodeCallback); + } + + if ($stmt->returnType !== null) { + $nodeCallback($stmt->returnType, $scope); + } + + $methodScope = $scope->enterClassMethod( + $stmt, + $phpDocParameterTypes, + $phpDocReturnType, + $phpDocThrowType, + $isDeprecated, + $isInternal, + $isFinal + ); + $nodeCallback(new InClassMethodNode($stmt), $methodScope); + + if ($stmt->stmts !== null) { + $this->processStmtNodes($stmt->stmts, $methodScope, $nodeCallback); + } + } elseif ($stmt instanceof Echo_) { + foreach ($stmt->exprs as $echoExpr) { + $scope = $this->processExprNode($echoExpr, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); + } + } elseif ($stmt instanceof Return_) { + if ($stmt->expr !== null) { + $scope = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); + } + + return new StatementResult($scope, [$stmt], [ + new StatementExitPoint($stmt, $scope), + ]); + } elseif ($stmt instanceof Continue_) { + if ($stmt->num !== null) { + $scope = $this->processExprNode($stmt->num, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); + } + + return new StatementResult($scope, [$stmt], [ + new StatementExitPoint($stmt, $scope), + ]); + } elseif ($stmt instanceof Break_) { + if ($stmt->num !== null) { + $scope = $this->processExprNode($stmt->num, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); + } + + return new StatementResult($scope, [$stmt], [ + new StatementExitPoint($stmt, $scope), + ]); + } elseif ($stmt instanceof Node\Stmt\Expression) { + $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope); + $scope = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); + $scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition( + $scope, + $stmt->expr, + TypeSpecifierContext::createNull() + )); + if ($earlyTerminationExpr !== null) { + return new StatementResult($scope, [$stmt], [ + new StatementExitPoint($stmt, $scope), + ]); + } + } elseif ($stmt instanceof Node\Stmt\Namespace_) { + if ($stmt->name !== null) { + $scope = $scope->enterNamespace($stmt->name->toString()); + } + + $scope = $this->processStmtNodes($stmt->stmts, $scope, $nodeCallback)->getScope(); + } elseif ($stmt instanceof Node\Stmt\Trait_) { + return new StatementResult($scope, [], []); + } elseif ($stmt instanceof Node\Stmt\ClassLike) { + if (isset($stmt->namespacedName)) { + $classScope = $scope->enterClass($this->broker->getClass((string) $stmt->namespacedName)); + } elseif ($stmt instanceof Class_) { + if ($stmt->name === null) { + throw new \PHPStan\ShouldNotHappenException(); + } + $classScope = $scope->enterClass($this->broker->getClass($stmt->name->toString())); } else { throw new \PHPStan\ShouldNotHappenException(); } - } elseif ($node instanceof Node\Stmt\TraitUse) { - $this->processTraitUse($node, $scope, $nodeCallback); - } elseif ($node instanceof \PhpParser\Node\Stmt\Function_) { - $scope = $this->enterFunction($scope, $node); - } elseif ($node instanceof \PhpParser\Node\Stmt\ClassMethod) { - $scope = $this->enterClassMethod($scope, $node); - $nodeCallback(new InClassMethodNode($node), $scope); - } elseif ($node instanceof \PhpParser\Node\Stmt\Namespace_) { - $scope = $scope->enterNamespace((string) $node->name); - } elseif ( - $node instanceof \PhpParser\Node\Expr\StaticCall - && $node->class instanceof \PhpParser\Node\Name - && $node->name instanceof Node\Identifier - && (string) $node->class === 'Closure' - && $node->name->name === 'bind' - ) { - $thisType = null; - if (isset($node->args[1])) { - $argValue = $node->args[1]->value; - if ($argValue instanceof Expr\ConstFetch && ((string) $argValue->name === 'null')) { - $thisType = null; - } else { - $thisType = $scope->getType($argValue); + + $this->processStmtNodes($stmt->stmts, $classScope, $nodeCallback); + } elseif ($stmt instanceof Node\Stmt\Property) { + foreach ($stmt->props as $prop) { + $this->processStmtNode($prop, $scope, $nodeCallback); + } + } elseif ($stmt instanceof Node\Stmt\PropertyProperty) { + if ($stmt->default !== null) { + $this->processExprNode($stmt->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + } + } elseif ($stmt instanceof Throw_) { + $scope = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); + return new StatementResult($scope, [$stmt], [ + new StatementExitPoint($stmt, $scope), + ]); + } elseif ($stmt instanceof If_) { + $conditionType = $scope->getType($stmt->cond)->toBoolean(); + $ifAlwaysTrue = $conditionType instanceof ConstantBooleanType && $conditionType->getValue(); + $condResult = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep()); + $exitPoints = []; + $finalScope = null; + $alwaysTerminatingStatements = []; + $alwaysTerminating = true; + + $branchScopeStatementResult = $this->processStmtNodes($stmt->stmts, $condResult->getTruthyScope(), $nodeCallback); + + if (!$conditionType instanceof ConstantBooleanType || $conditionType->getValue()) { + $exitPoints = $branchScopeStatementResult->getExitPoints(); + $branchScope = $branchScopeStatementResult->getScope(); + $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? null : $branchScope; + $alwaysTerminating = $branchScopeStatementResult->isAlwaysTerminating(); + if ($branchScopeStatementResult->isAlwaysTerminating()) { + $alwaysTerminatingStatements = array_merge($alwaysTerminatingStatements, $branchScopeStatementResult->getAlwaysTerminatingStatements()); } } - $scopeClass = 'static'; - if (isset($node->args[2])) { - $argValue = $node->args[2]->value; - $argValueType = $scope->getType($argValue); - $directClassNames = TypeUtils::getDirectClassNames($argValueType); - if (count($directClassNames) === 1) { - $scopeClass = $directClassNames[0]; - } elseif ( - $argValue instanceof Expr\ClassConstFetch - && $argValue->name instanceof Node\Identifier - && strtolower($argValue->name->name) === 'class' - && $argValue->class instanceof Name + $scope = $condResult->getFalseyScope(); + $lastElseIfConditionIsTrue = false; + + $condScope = $scope; + foreach ($stmt->elseifs as $elseif) { + $nodeCallback($elseif, $scope); + $elseIfConditionType = $condScope->getType($elseif->cond)->toBoolean(); + $condResult = $this->processExprNode($elseif->cond, $condScope, $nodeCallback, ExpressionContext::createDeep()); + $condScope = $condResult->getScope(); + $branchScopeStatementResult = $this->processStmtNodes($elseif->stmts, $condResult->getTruthyScope(), $nodeCallback); + + if ( + !$ifAlwaysTrue + && ( + !$lastElseIfConditionIsTrue + && ( + !$elseIfConditionType instanceof ConstantBooleanType + || $elseIfConditionType->getValue() + ) + ) ) { - $scopeClass = $scope->resolveName($argValue->class); - } elseif ($argValueType instanceof ConstantStringType) { - $scopeClass = $argValueType->getValue(); + $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); + $branchScope = $branchScopeStatementResult->getScope(); + $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope); + $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating(); + if ($branchScopeStatementResult->isAlwaysTerminating()) { + $alwaysTerminatingStatements = array_merge($alwaysTerminatingStatements, $branchScopeStatementResult->getAlwaysTerminatingStatements()); + } + } + + if ( + $elseIfConditionType instanceof ConstantBooleanType + && $elseIfConditionType->getValue() + ) { + $lastElseIfConditionIsTrue = true; + } + + $condScope = $condScope->filterByFalseyValue($elseif->cond); + $scope = $condScope; + } + + if ($stmt->else === null) { + if (!$ifAlwaysTrue) { + $finalScope = $scope->mergeWith($finalScope); + $alwaysTerminating = false; + } + } else { + $nodeCallback($stmt->else, $scope); + $branchScopeStatementResult = $this->processStmtNodes($stmt->else->stmts, $scope, $nodeCallback); + + if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) { + $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints()); + $branchScope = $branchScopeStatementResult->getScope(); + $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope); + $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating(); + if ($branchScopeStatementResult->isAlwaysTerminating()) { + $alwaysTerminatingStatements = array_merge($alwaysTerminatingStatements, $branchScopeStatementResult->getAlwaysTerminatingStatements()); + } } } - $closureBindScope = $scope->enterClosureBind($thisType, $scopeClass); - } elseif ($node instanceof Foreach_) { - $scope = $scope->exitFirstLevelStatements(); - $this->processNode($node->expr, $scope, $nodeCallback); - $scope = $this->lookForAssigns($scope, $node->expr, TrinaryLogic::createYes(), LookForAssignsSettings::default()); - if ($node->keyVar !== null) { - $this->processNode($node->keyVar, $scope->enterExpressionAssign($node->keyVar), $nodeCallback); + + if ($finalScope === null) { + $finalScope = $scope; } - $this->processNode( - $node->valueVar, - $this->lookForEnterVariableAssign($scope, $node->valueVar), - $nodeCallback - ); - $scope = $this->lookForAssignsInBranches($scope, [ - new StatementList($scope, $node->stmts, false, function (Scope $scope) use ($node): Scope { - return $this->enterForeach($scope, $node); - }), - new StatementList($scope, []), - ], LookForAssignsSettings::insideLoop()); - $scope = $this->enterForeach($scope, $node); + return new StatementResult($finalScope, $alwaysTerminating ? $alwaysTerminatingStatements : [], $exitPoints); + } elseif ($stmt instanceof Node\Stmt\TraitUse) { + $this->processTraitUse($stmt, $scope, $nodeCallback); + } elseif ($stmt instanceof Foreach_) { + $scope = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); + $bodyScope = $this->enterForeach($scope, $stmt); + $count = 0; + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($scope); + $bodyScope = $this->enterForeach($bodyScope, $stmt); + $bodyScopeResult = $this->processStmtNodes($stmt->stmts, $bodyScope, static function (): void { + })->filterOutLoopTerminationStatements(); + $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + if ($bodyScope->equals($prevScope)) { + break; + } - $this->processNodes($node->stmts, $scope->enterFirstLevelStatements(), $nodeCallback); + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $bodyScope->generalizeWith($prevScope); + } + $count++; + } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); - return; - } elseif ($node instanceof For_) { - $this->processNodes($node->init, $scope, $nodeCallback); - - foreach ($node->init as $initExpr) { - $scope = $this->lookForAssigns($scope, $initExpr, TrinaryLogic::createYes(), LookForAssignsSettings::insideLoop()); + $bodyScope = $bodyScope->mergeWith($scope); + $bodyScope = $this->enterForeach($bodyScope, $stmt); + $finalScopeResult = $this->processStmtNodes($stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopTerminationStatements(); + $alwaysTerminatingStatements = $finalScopeResult->getAlwaysTerminatingStatements(); + $finalScope = $finalScopeResult->getScope(); + foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope); + } + foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { + $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); } - $scopeLoopMightHaveRun = $this->lookForAssignsInBranches($scope, [ - new StatementList($scope, $node->cond), - new StatementList($scope, $node->stmts), - new StatementList($scope, $node->loop), - new StatementList($scope, []), - ], LookForAssignsSettings::insideLoop()); - $this->processNodes($node->cond, $scopeLoopMightHaveRun, $nodeCallback); + $isIterableAtLeastOnce = $scope->getType($stmt->expr)->isIterableAtLeastOnce(); + if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) { + $finalScope = $scope; + } elseif ($isIterableAtLeastOnce->maybe()) { + $finalScope = $finalScope->mergeWith($scope); + } elseif (!$this->polluteScopeWithAlwaysIterableForeach) { + $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope); + // get types from finalScope, but don't create new variables + } - $scopeLoopDefinitelyRan = $this->lookForAssignsInBranches($scope, [ - new StatementList($scope, $node->stmts, false, static function (Scope $scope) use ($node): Scope { - foreach ($node->cond as $condExpr) { - $scope = $scope->filterByTruthyValue($condExpr); + return new StatementResult($finalScope, $alwaysTerminating ? $alwaysTerminatingStatements : [], []); + } elseif ($stmt instanceof While_) { + $condResult = $this->processExprNode($stmt->cond, $scope, static function (): void { + }, ExpressionContext::createDeep()); + $bodyScope = $condResult->getTruthyScope(); + $count = 0; + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($scope); + $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope(); + $bodyScopeResult = $this->processStmtNodes($stmt->stmts, $bodyScope, static function (): void { + })->filterOutLoopTerminationStatements(); + $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); + $alwaysTerminatingStatements = $bodyScopeResult->getAlwaysTerminatingStatements(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + if ($bodyScope->equals($prevScope)) { + break; + } + + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $bodyScope->generalizeWith($prevScope); + } + $count++; + } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); + + $bodyScope = $bodyScope->mergeWith($scope); + $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + $finalScopeResult = $this->processStmtNodes($stmt->stmts, $bodyScope, $nodeCallback); + $finalScope = $finalScopeResult->getScope(); + foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $finalScope = $finalScope->mergeWith($continueExitPoint->getScope()); + } + foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { + $finalScope = $finalScope->mergeWith($breakExitPoint->getScope()); + } + + $condBooleanType = $scope->getType($stmt->cond)->toBoolean(); + $isIterableAtLeastOnce = $condBooleanType instanceof ConstantBooleanType && $condBooleanType->getValue(); + // todo for all loops - is not falsey when the loop is exited via break + $condScope = $condResult->getFalseyScope(); + if (!$isIterableAtLeastOnce) { + if (!$this->polluteScopeWithLoopInitialAssignments) { + $condScope = $condScope->mergeWith($scope); + } + $finalScope = $finalScope->mergeWith($condScope); + } + + return new StatementResult($finalScope, $alwaysTerminatingStatements, []); + } elseif ($stmt instanceof Do_) { + $finalScope = null; + $bodyScope = $scope; + $count = 0; + do { + $prevScope = $bodyScope; + $bodyScopeResult = $this->processStmtNodes($stmt->stmts, $bodyScope, static function (): void { + })->filterOutLoopTerminationStatements(); + $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope); + foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { + $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); + } + $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope(); + if ($bodyScope->equals($prevScope)) { + break; + } + + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $bodyScope->generalizeWith($prevScope); + } + $count++; + } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); + + $bodyScope = $bodyScope->mergeWith($scope); + + $bodyScopeResult = $this->processStmtNodes($stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopTerminationStatements(); + $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); + $alwaysTerminatingStatements = $bodyScopeResult->getAlwaysTerminatingStatements(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope); + if ($finalScope === null) { + $finalScope = $scope; + } + if (!$alwaysTerminating) { + $finalScope = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getFalseyScope(); + // todo not falsey if it breaks out of the loop using break; + } + foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { + $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); + } + + return new StatementResult($finalScope, $alwaysTerminatingStatements, []); + } elseif ($stmt instanceof For_) { + $initScope = $scope; + foreach ($stmt->init as $initExpr) { + $initScope = $this->processExprNode($initExpr, $initScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); + } + + $bodyScope = $initScope; + foreach ($stmt->cond as $condExpr) { + $bodyScope = $this->processExprNode($condExpr, $bodyScope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope(); + } + + $count = 0; + do { + $prevScope = $bodyScope; + $bodyScope = $bodyScope->mergeWith($initScope); + foreach ($stmt->cond as $condExpr) { + $bodyScope = $this->processExprNode($condExpr, $bodyScope, static function (): void { + }, ExpressionContext::createDeep())->getTruthyScope(); + } + $bodyScopeResult = $this->processStmtNodes($stmt->stmts, $bodyScope, static function (): void { + })->filterOutLoopTerminationStatements(); + $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating(); + $alwaysTerminatingStatements = $bodyScopeResult->getAlwaysTerminatingStatements(); + $bodyScope = $bodyScopeResult->getScope(); + foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope()); + } + foreach ($stmt->loop as $loopExpr) { + $bodyScope = $this->processExprNode($loopExpr, $bodyScope, static function (): void { + }, ExpressionContext::createTopLevel())->getScope(); + } + + if ($bodyScope->equals($prevScope)) { + break; + } + + if ($count >= self::GENERALIZE_AFTER_ITERATION) { + $bodyScope = $bodyScope->generalizeWith($prevScope); + } + $count++; + } while (!$alwaysTerminating && $count < self::LOOP_SCOPE_ITERATIONS); + + $bodyScope = $bodyScope->mergeWith($initScope); + foreach ($stmt->cond as $condExpr) { + $bodyScope = $this->processExprNode($condExpr, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope(); + } + + $finalScopeResult = $this->processStmtNodes($stmt->stmts, $bodyScope, $nodeCallback); + $finalScope = $finalScopeResult->getScope(); + foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope); + } + foreach ($stmt->loop as $loopExpr) { + $finalScope = $this->processExprNode($loopExpr, $finalScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope(); + } + foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { + $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope); + } + + if ($this->polluteScopeWithLoopInitialAssignments) { + $scope = $initScope; + } + + $finalScope = $finalScope->mergeWith($scope); + + /*foreach ($stmt->cond as $condExpr) { + // todo not if breaks out of the loop using break; + //$finalScope = $finalScope->filterByFalseyValue($condExpr); + }*/ + + return new StatementResult($finalScope, $alwaysTerminatingStatements, []); + } elseif ($stmt instanceof Switch_) { + $scope = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); + $scopeForBranches = $scope; + $finalScope = null; + $prevScope = null; + $hasDefaultCase = false; + $alwaysTerminating = true; + $alwaysTerminatingStatements = []; + foreach ($stmt->cases as $i => $caseNode) { + if ($caseNode->cond !== null) { + $condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond); + $scopeForBranches = $this->processExprNode($caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep())->getScope(); + if ($prevScope === null) { + $branchScope = $scopeForBranches->filterByTruthyValue($condExpr); + $scopeForBranches = $scopeForBranches->filterByFalseyValue($condExpr); + } else { + $branchScope = $scopeForBranches; } + } else { + $hasDefaultCase = true; + $branchScope = $scopeForBranches; + } - return $scope; - }), - ], LookForAssignsSettings::insideLoop()); + $branchScope = $branchScope->mergeWith($prevScope); + $branchScopeResult = $this->processStmtNodes($caseNode->stmts, $branchScope, $nodeCallback); + $branchScope = $branchScopeResult->getScope(); + $branchFinalScopeResult = $branchScopeResult->filterOutLoopTerminationStatements(); + $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating(); + if ($branchFinalScopeResult->isAlwaysTerminating()) { + $alwaysTerminatingStatements = array_merge($alwaysTerminatingStatements, $branchFinalScopeResult->getAlwaysTerminatingStatements()); + } + $isLastCase = ($i === count($stmt->cases) - 1); + if ( + $branchScopeResult->areAllAlwaysTerminatingStatementsLoopTerminationStatements() + || ( + $isLastCase + && !$branchFinalScopeResult->isAlwaysTerminating() + ) + ) { + $finalScope = $branchScope->mergeWith($finalScope); + foreach ($branchScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) { + $finalScope = $finalScope->mergeWith($breakExitPoint->getScope()); + } + foreach ($branchScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) { + $finalScope = $finalScope->mergeWith($continueExitPoint->getScope()); + } + } - $this->processNodes($node->loop, $scopeLoopDefinitelyRan, $nodeCallback); - - foreach ($node->cond as $condExpr) { - $scopeLoopMightHaveRun = $this->lookForAssigns($scopeLoopMightHaveRun, $condExpr, TrinaryLogic::createYes(), LookForAssignsSettings::insideLoop()); + $prevScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope; } - foreach ($node->cond as $condExpr) { - $scopeLoopMightHaveRun = $scopeLoopMightHaveRun->filterByTruthyValue($condExpr); + if (!$hasDefaultCase) { + $alwaysTerminating = false; } - $this->processNodes($node->stmts, $scopeLoopMightHaveRun, $nodeCallback); - return; - } elseif ($node instanceof While_) { - $bodyScope = $this->lookForAssigns( - $scope, - $node->cond, - TrinaryLogic::createYes(), - LookForAssignsSettings::default() - ); - $condScope = $this->lookForAssignsInBranches($scope, [ - new StatementList($bodyScope, $node->stmts, false, static function (Scope $scope) use ($node): Scope { - return $scope->filterByTruthyValue($node->cond); - }), - new StatementList($scope, []), - ], LookForAssignsSettings::insideLoop()); - $this->processNode($node->cond, $condScope, $nodeCallback); + if (!$hasDefaultCase || $finalScope === null) { + $finalScope = $scope->mergeWith($finalScope); + } - $bodyScope = $this->lookForAssignsInBranches($bodyScope, [ - new StatementList($bodyScope, $node->stmts, false, static function (Scope $scope) use ($node): Scope { - return $scope->filterByTruthyValue($node->cond); - }), - new StatementList($bodyScope, []), - ], LookForAssignsSettings::insideLoop()); - $bodyScope = $this->lookForAssigns($bodyScope, $node->cond, TrinaryLogic::createYes(), LookForAssignsSettings::insideLoop()); - $bodyScope = $bodyScope->filterByTruthyValue($node->cond); - $this->processNodes($node->stmts, $bodyScope, $nodeCallback); - return; - } elseif ($node instanceof Catch_) { - if (!is_string($node->var->name)) { + return new StatementResult($finalScope, $alwaysTerminating ? $alwaysTerminatingStatements : [], []); + } elseif ($stmt instanceof TryCatch) { + $branchScopeResult = $this->processStmtNodes($stmt->stmts, $scope, $nodeCallback); + $branchScope = $branchScopeResult->getScope(); + $tryScope = $branchScope; + $exitPoints = []; + $alwaysTerminatingStatements = []; + $finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope; + $alwaysTerminating = $branchScopeResult->isAlwaysTerminating(); + if ($branchScopeResult->isAlwaysTerminating()) { + $alwaysTerminatingStatements = array_merge($alwaysTerminatingStatements, $branchScopeResult->getAlwaysTerminatingStatements()); + } + + if ($stmt->finally !== null) { + $finallyScope = $branchScope; + } else { + $finallyScope = null; + } + foreach ($branchScopeResult->getExitPoints() as $exitPoint) { + if ($finallyScope !== null) { + $finallyScope = $finallyScope->mergeWith($exitPoint->getScope()); + } + $exitPoints[] = $exitPoint; + } + + foreach ($stmt->catches as $catchNode) { + $nodeCallback($catchNode, $scope); + if (!is_string($catchNode->var->name)) { + throw new \PHPStan\ShouldNotHappenException(); + } + + if (!$this->polluteCatchScopeWithTryAssignments) { + $catchScopeResult = $this->processCatchNode($catchNode, $scope->mergeWith($tryScope), $nodeCallback); + $catchScopeForFinally = $catchScopeResult->getScope(); + } else { + $catchScopeForFinally = $this->processCatchNode($catchNode, $tryScope, $nodeCallback)->getScope(); + $catchScopeResult = $this->processCatchNode($catchNode, $scope->mergeWith($tryScope), static function (): void { + }); + } + + $finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope); + $alwaysTerminating = $alwaysTerminating && $catchScopeResult->isAlwaysTerminating(); + + if ($catchScopeResult->isAlwaysTerminating()) { + $alwaysTerminatingStatements = array_merge($alwaysTerminatingStatements, $catchScopeResult->getAlwaysTerminatingStatements()); + } + + if ($finallyScope !== null) { + $finallyScope = $finallyScope->mergeWith($catchScopeForFinally); + } + foreach ($catchScopeResult->getExitPoints() as $exitPoint) { + if ($finallyScope !== null) { + $finallyScope = $finallyScope->mergeWith($exitPoint->getScope()); + } + $exitPoints[] = $exitPoint; + } + } + + if ($finalScope === null) { + $finalScope = $scope; + } + + if ($finallyScope !== null && $stmt->finally !== null) { + $originalFinallyScope = $finallyScope; + $finallyResult = $this->processStmtNodes($stmt->finally->stmts, $finallyScope, $nodeCallback); + $finallyScope = $finallyResult->getScope(); + $finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope); + if ($finallyResult->isAlwaysTerminating()) { + $alwaysTerminatingStatements = array_merge($alwaysTerminatingStatements, $finallyResult->getAlwaysTerminatingStatements()); + } + } + + return new StatementResult($finalScope, $alwaysTerminating ? $alwaysTerminatingStatements : [], $exitPoints); + } elseif ($stmt instanceof Unset_) { + foreach ($stmt->vars as $var) { + $scope = $this->lookForEnterVariableAssign($scope, $var); + $scope = $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope(); + $scope = $this->lookForExitVariableAssign($scope, $var); + $scope = $scope->unsetExpression($var); + } + } elseif ($stmt instanceof Node\Stmt\Use_) { + foreach ($stmt->uses as $use) { + $this->processStmtNode($use, $scope, $nodeCallback); + } + } elseif ($stmt instanceof Node\Stmt\Global_) { + foreach ($stmt->vars as $var) { + if (!$var instanceof Variable) { + throw new \PHPStan\ShouldNotHappenException(); + } + if (!is_string($var->name)) { + throw new \PHPStan\ShouldNotHappenException(); + } + $scope = $this->lookForEnterVariableAssign($scope, $var); + $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $scope = $this->lookForExitVariableAssign($scope, $var); + $scope = $scope->assignVariable($var->name, new MixedType()); + } + } elseif ($stmt instanceof Static_) { + foreach ($stmt->vars as $var) { + $scope = $this->processStmtNode($var, $scope, $nodeCallback)->getScope(); + } + } elseif ($stmt instanceof StaticVar) { + if (!is_string($stmt->var->name)) { throw new \PHPStan\ShouldNotHappenException(); } - $scope = $scope->enterCatch( - $node->types, - $node->var->name - ); - } elseif ($node instanceof Array_) { - $scope = $scope->exitFirstLevelStatements(); - foreach ($node->items as $item) { + if ($stmt->default !== null) { + $this->processExprNode($stmt->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + } + $scope = $scope->enterExpressionAssign($stmt->var); + $this->processExprNode($stmt->var, $scope, $nodeCallback, ExpressionContext::createDeep()); + $scope = $scope->exitExpressionAssign($stmt->var); + $scope = $scope->assignVariable($stmt->var->name, new MixedType()); + } elseif ($stmt instanceof Node\Stmt\Const_ || $stmt instanceof Node\Stmt\ClassConst) { + foreach ($stmt->consts as $const) { + $nodeCallback($const, $scope); + $this->processExprNode($const->value, $scope, $nodeCallback, ExpressionContext::createDeep()); + } + } + + return new StatementResult($scope, [], []); + } + + /** + * @param Node\Stmt\Catch_ $catchNode + * @param Scope $catchScope + * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @return StatementResult + */ + private function processCatchNode( + Node\Stmt\Catch_ $catchNode, + Scope $catchScope, + \Closure $nodeCallback + ): StatementResult + { + if (!is_string($catchNode->var->name)) { + throw new \PHPStan\ShouldNotHappenException(); + } + + $catchScope = $catchScope->enterCatch($catchNode->types, $catchNode->var->name); + return $this->processStmtNodes($catchNode->stmts, $catchScope, $nodeCallback); + } + + private function lookForEnterVariableAssign(Scope $scope, Expr $expr): Scope + { + if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { + $scope = $scope->enterExpressionAssign($expr); + } + if (!$expr instanceof Variable) { + return $this->lookForVariableAssignCallback($scope, $expr, static function (Scope $scope, Expr $expr): Scope { + return $scope->enterExpressionAssign($expr); + }); + } + + return $scope; + } + + private function lookForExitVariableAssign(Scope $scope, Expr $expr): Scope + { + $scope = $scope->exitExpressionAssign($expr); + if (!$expr instanceof Variable) { + return $this->lookForVariableAssignCallback($scope, $expr, static function (Scope $scope, Expr $expr): Scope { + return $scope->exitExpressionAssign($expr); + }); + } + + return $scope; + } + + /** + * @param Scope $scope + * @param Expr $expr + * @param \Closure(Scope $scope, Expr $expr): Scope $callback + * @return Scope + */ + private function lookForVariableAssignCallback(Scope $scope, Expr $expr, \Closure $callback): Scope + { + if ($expr instanceof Variable) { + $scope = $callback($scope, $expr); + } elseif ($expr instanceof ArrayDimFetch) { + while ($expr instanceof ArrayDimFetch) { + $expr = $expr->var; + } + + $scope = $this->lookForVariableAssignCallback($scope, $expr, $callback); + } elseif ($expr instanceof PropertyFetch) { + $scope = $this->lookForVariableAssignCallback($scope, $expr->var, $callback); + } elseif ($expr instanceof StaticPropertyFetch) { + if ($expr->class instanceof Expr) { + $scope = $this->lookForVariableAssignCallback($scope, $expr->class, $callback); + } + } elseif ($expr instanceof Array_ || $expr instanceof List_) { + foreach ($expr->items as $item) { if ($item === null) { continue; } - $this->processNode($item, $scope, $nodeCallback); - if ($item->key !== null) { - $scope = $this->lookForAssigns($scope, $item->key, TrinaryLogic::createYes(), LookForAssignsSettings::default()); - } - $scope = $this->lookForAssigns($scope, $item->value, TrinaryLogic::createYes(), LookForAssignsSettings::default()); - } - return; - } elseif ($node instanceof Expr\Closure) { - $this->processNodes($node->uses, $scope, $nodeCallback); - $usesByRef = []; - foreach ($node->uses as $closureUse) { - if (!$closureUse->byRef) { - continue; - } - - $usesByRef[] = $closureUse; - } - - if (count($usesByRef) > 0) { - $closureScope = $this->lookForAssignsInBranches($scope, [ - new StatementList($scope, $node->stmts), - new StatementList($scope, []), - ], LookForAssignsSettings::insideClosure()); - /** @var Expr\ClosureUse $closureUse */ - foreach ($usesByRef as $closureUse) { - if (!is_string($closureUse->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - $variableCertainty = $closureScope->hasVariableType($closureUse->var->name); - if ($variableCertainty->no()) { - continue; - } - $scope = $scope->assignVariable( - $closureUse->var->name, - $closureScope->getVariableType($closureUse->var->name), - $variableCertainty - ); - } - } - - $scope = $scope->enterAnonymousFunction($node); - $this->processNodes($node->stmts, $scope, $nodeCallback); - - return; - } elseif ($node instanceof If_) { - $this->processNode($node->cond, $scope->exitFirstLevelStatements(), $nodeCallback); - $scope = $this->lookForAssigns( - $scope, - $node->cond, - TrinaryLogic::createYes(), - LookForAssignsSettings::default() - ); - $ifScope = $scope; - $scope = $scope->filterByTruthyValue($node->cond); - - $specifyFetchedProperty = function (Node $node, Scope $inScope) use (&$scope): void { - $this->specifyFetchedPropertyForInnerScope($node, $inScope, false, $scope); - }; - $this->processNode($node->cond, $scope, $specifyFetchedProperty); - $this->processNodes($node->stmts, $scope->enterFirstLevelStatements(), $nodeCallback); - - if (count($node->elseifs) > 0 || $node->else !== null) { - $elseifScope = $ifScope->filterByFalseyValue($node->cond); - foreach ($node->elseifs as $elseif) { - $scope = $elseifScope; - $this->processNode($elseif, $scope, $nodeCallback, true); - $this->processNode($elseif->cond, $scope->exitFirstLevelStatements(), $nodeCallback); - $scope = $this->lookForAssigns( - $scope, - $elseif->cond, - TrinaryLogic::createYes(), - LookForAssignsSettings::default() - ); - $scope = $scope->filterByTruthyValue($elseif->cond); - $this->processNode($elseif->cond, $scope, $specifyFetchedProperty); - $this->processNodes($elseif->stmts, $scope->enterFirstLevelStatements(), $nodeCallback); - $elseifScope = $this->lookForAssigns( - $elseifScope, - $elseif->cond, - TrinaryLogic::createYes(), - LookForAssignsSettings::default() - )->filterByFalseyValue($elseif->cond); - } - if ($node->else !== null) { - $this->processNode($node->else, $elseifScope, $nodeCallback); - } - } - - return; - } elseif ($node instanceof Switch_) { - $scope = $scope->exitFirstLevelStatements(); - $this->processNode($node->cond, $scope, $nodeCallback); - $scope = $this->lookForAssigns( - $scope, - $node->cond, - TrinaryLogic::createYes(), - LookForAssignsSettings::default() - ); - $switchScope = $scope; - $switchConditionType = $scope->getType($node->cond)->toBoolean(); - $switchConditionIsTrue = $switchConditionType instanceof ConstantBooleanType && $switchConditionType->getValue(); - $switchConditionGetClassExpression = null; - if ( - $node->cond instanceof FuncCall - && $node->cond->name instanceof Name - && strtolower((string) $node->cond->name) === 'get_class' - && isset($node->cond->args[0]) - ) { - $switchConditionGetClassExpression = $node->cond->args[0]->value; - } - foreach ($node->cases as $caseNode) { - $this->processNode($caseNode, $scope, $nodeCallback, true); - if ($caseNode->cond !== null) { - $this->processNode($caseNode->cond, $switchScope, $nodeCallback); - $switchScope = $this->lookForAssigns( - $switchScope, - $caseNode->cond, - TrinaryLogic::createYes(), - LookForAssignsSettings::default() - ); - $scope = $this->lookForAssigns( - $scope, - $caseNode->cond, - TrinaryLogic::createYes(), - LookForAssignsSettings::default() - ); - - $caseScope = $switchScope; - if ($switchConditionIsTrue) { - $caseScope = $caseScope->filterByTruthyValue($caseNode->cond); - } elseif ( - $switchConditionGetClassExpression !== null - && $caseNode->cond instanceof Expr\ClassConstFetch - && $caseNode->cond->class instanceof Name - && $caseNode->cond->name instanceof Node\Identifier - && strtolower($caseNode->cond->name->name) === 'class' - ) { - $caseScope = $caseScope->specifyExpressionType( - $switchConditionGetClassExpression, - new ObjectType($scope->resolveName($caseNode->cond->class)) - ); - } - } else { - $caseScope = $switchScope; - } - $this->processNodes( - $caseNode->stmts, - $caseScope->enterFirstLevelStatements(), - $nodeCallback - ); - if ($this->findEarlyTermination($caseNode->stmts, $switchScope) === null) { - foreach ($caseNode->stmts as $statement) { - $switchScope = $this->lookForAssigns($switchScope, $statement, TrinaryLogic::createMaybe(), LookForAssignsSettings::default()); - } - } else { - $switchScope = $scope; - } - } - return; - } elseif ($node instanceof TryCatch) { - $statements = []; - $this->processNodes($node->stmts, $scope->enterFirstLevelStatements(), $nodeCallback); - - $scopeForLookForAssignsInBranches = $scope; - $tryAssignmentsCertainty = $this->polluteCatchScopeWithTryAssignments ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe(); - foreach ($node->stmts as $statement) { - $scope = $this->lookForAssigns($scope, $statement, $tryAssignmentsCertainty, LookForAssignsSettings::default()); - } - - if ($node->finally !== null) { - $statements[] = new StatementList($scopeForLookForAssignsInBranches, $node->stmts); - $statements[] = new StatementList($scopeForLookForAssignsInBranches, []); - } - - foreach ($node->catches as $catch) { - $this->processNode($catch, $scope, $nodeCallback); - if ($node->finally === null) { - continue; - } - - if (!is_string($catch->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - $statements[] = new StatementList($scope->enterCatch( - $catch->types, - $catch->var->name - ), $catch->stmts); - } - - if ($node->finally !== null) { - $finallyScope = $this->lookForAssignsInBranches($scopeForLookForAssignsInBranches, $statements, LookForAssignsSettings::insideFinally()); - - $this->processNode($node->finally, $finallyScope, $nodeCallback); - } - - return; - } elseif ($node instanceof FuncCall) { - $scope = $scope->enterFunctionCall($node); - } elseif ($node instanceof Expr\StaticCall) { - $scope = $scope->enterFunctionCall($node); - } elseif ($node instanceof MethodCall) { - if ( - $node->var instanceof Expr\Closure - && $node->name instanceof Node\Identifier - && $node->name->name === 'call' - && isset($node->args[0]) - ) { - $closureCallScope = $scope->enterClosureCall($scope->getType($node->args[0]->value)); - } - $scope = $scope->enterFunctionCall($node); - } elseif ($node instanceof New_ && $node->class instanceof Class_) { - $this->anonymousClassReflection = $this->broker->getAnonymousClassReflection($node, $scope); - } elseif ($node instanceof BooleanNot) { - $scope = $scope->enterNegation(); - } elseif ($node instanceof Unset_ || $node instanceof Isset_) { - foreach ($node->vars as $unsetVar) { - if ($unsetVar instanceof ArrayDimFetch && $unsetVar->dim === null) { - continue; - } - - while ( - $unsetVar instanceof ArrayDimFetch - || $unsetVar instanceof PropertyFetch - || ( - $unsetVar instanceof StaticPropertyFetch - && $unsetVar->class instanceof Expr - ) - ) { - $scope = $scope->enterExpressionAssign($unsetVar); - if ($unsetVar instanceof StaticPropertyFetch) { - /** @var Expr $unsetVar */ - $unsetVar = $unsetVar->class; - } else { - $unsetVar = $unsetVar->var; - } - } - - $scope = $scope->enterExpressionAssign($unsetVar); - } - } elseif ($node instanceof Node\Stmt\Global_) { - foreach ($node->vars as $var) { - $scope = $scope->enterExpressionAssign($var); + $scope = $this->lookForVariableAssignCallback($scope, $item->value, $callback); } } - $originalScope = $scope; - foreach ($node->getSubNodeNames() as $subNodeName) { - $scope = $originalScope; - $subNode = $node->{$subNodeName}; - - if (is_array($subNode)) { - $argClosureBindScope = null; - if (isset($closureBindScope) && $subNodeName === 'args') { - $argClosureBindScope = $closureBindScope; - } - if ($subNodeName === 'stmts') { - $scope = $scope->enterFirstLevelStatements(); - } else { - $scope = $scope->exitFirstLevelStatements(); - } - - if ($node instanceof Isset_ && $subNodeName === 'vars') { - foreach ($node->vars as $issetVar) { - $scope = $this->specifyProperty($scope, $issetVar); - $scope = $this->ensureNonNullability($scope, $issetVar, true); - } - } - - if ($node instanceof MethodCall && $subNodeName === 'args') { - $scope = $this->lookForAssigns($scope, $node->var, TrinaryLogic::createYes(), LookForAssignsSettings::default()); - } - - if ($node instanceof Do_ && $subNodeName === 'stmts') { - $scope = $this->lookForAssignsInBranches($scope, [ - new StatementList($scope, $node->stmts), - new StatementList($scope, [$node->cond], true), - new StatementList($scope, []), - ], LookForAssignsSettings::insideLoop()); - } - - $this->processNodes($subNode, $scope, $nodeCallback, $argClosureBindScope); - } elseif ($subNode instanceof \PhpParser\Node) { - if ($node instanceof Coalesce && $subNodeName === 'left') { - $scope = $this->ensureNonNullability($scope, $subNode, false); - - if (!($node->left instanceof ArrayDimFetch) || $node->left->dim !== null) { - $scope = $this->lookForEnterVariableAssign($scope, $node->left); - } - } - - if ( - ($node instanceof BooleanAnd || $node instanceof BinaryOp\LogicalAnd) - && $subNodeName === 'right') { - $scope = $scope->filterByTruthyValue($node->left); - } - if ( - ($node instanceof BooleanOr || $node instanceof BinaryOp\LogicalOr) - && $subNodeName === 'right') { - $scope = $scope->filterByFalseyValue($node->left); - } - - if ($node instanceof Assign || $node instanceof AssignRef) { - if ($subNodeName === 'var') { - $scope = $this->lookForEnterVariableAssign($scope, $node->var); - } elseif ($subNodeName === 'expr') { - $scope = $this->lookForEnterVariableAssign($scope, $node); - } - } - - if ($node instanceof BinaryOp && $subNodeName === 'right') { - $scope = $this->lookForAssigns($scope, $node->left, TrinaryLogic::createYes(), LookForAssignsSettings::default()); - } - - if ($node instanceof Expr\Empty_ && $subNodeName === 'expr') { - $scope = $this->specifyProperty($scope, $node->expr); - - if (!($node->expr instanceof ArrayDimFetch) || $node->expr->dim !== null) { - $scope = $this->lookForEnterVariableAssign($scope, $node->expr); - } - } - - if ( - $node instanceof ArrayItem - && $subNodeName === 'value' - && $node->key !== null - ) { - $scope = $this->lookForAssigns($scope, $node->key, TrinaryLogic::createYes(), LookForAssignsSettings::default()); - } - - if ( - $node instanceof Ternary - && $subNodeName !== 'cond' - ) { - $scope = $this->lookForAssigns($scope, $node->cond, TrinaryLogic::createYes(), LookForAssignsSettings::default()); - if ($subNodeName === 'if') { - $scope = $scope->filterByTruthyValue($node->cond); - $this->processNode($node->cond, $scope, function (Node $node, Scope $inScope) use (&$scope): void { - $this->specifyFetchedPropertyForInnerScope($node, $inScope, false, $scope); - }); - } elseif ($subNodeName === 'else') { - $scope = $scope->filterByFalseyValue($node->cond); - $this->processNode($node->cond, $scope, function (Node $node, Scope $inScope) use (&$scope): void { - $this->specifyFetchedPropertyForInnerScope($node, $inScope, true, $scope); - }); - } - } - - if ($node instanceof Do_ && $subNodeName === 'cond') { - foreach ($node->stmts as $statement) { - $scope = $this->lookForAssigns($scope, $statement, TrinaryLogic::createYes(), LookForAssignsSettings::default()); - } - } - - if ($node instanceof Expr\Empty_ && $subNodeName === 'expr') { - $scope = $this->ensureNonNullability($scope, $subNode, true); - } - - if ($node instanceof StaticVar && $subNodeName === 'var') { - $scope = $scope->enterExpressionAssign($node->var); - } elseif ($node instanceof Expr\ClosureUse && $subNodeName === 'var') { - $scope = $scope->enterExpressionAssign($node->var); - } - - $nodeScope = $scope; - if (!$node instanceof ErrorSuppress && !$node instanceof Node\Stmt\Expression) { - $nodeScope = $nodeScope->exitFirstLevelStatements(); - } - if ($scope->isInFirstLevelStatement()) { - if ($node instanceof Ternary && $subNodeName !== 'cond') { - $nodeScope = $scope->enterFirstLevelStatements(); - } elseif ( - ($node instanceof BooleanAnd || $node instanceof BinaryOp\BooleanOr) - && $subNodeName === 'right' - ) { - $nodeScope = $scope->enterFirstLevelStatements(); - } - } - - if ($node instanceof MethodCall && $subNodeName === 'var' && isset($closureCallScope)) { - $nodeScope = $closureCallScope->exitFirstLevelStatements(); - } - - $this->processNode($subNode, $nodeScope, $nodeCallback); - } - } + return $scope; } - private function ensureNonNullability( - Scope $scope, - Node $node, - bool $findMethods - ): Scope + private function ensureNonNullability(Scope $scope, Expr $expr, bool $findMethods): EnsuredNonNullabilityResult { - $nodeToSpecify = $node; + $exprToSpecify = $expr; + $specifiedExpressions = []; while ( - $nodeToSpecify instanceof PropertyFetch - || $nodeToSpecify instanceof StaticPropertyFetch + $exprToSpecify instanceof PropertyFetch + || $exprToSpecify instanceof StaticPropertyFetch || ( $findMethods && ( - $nodeToSpecify instanceof MethodCall - || $nodeToSpecify instanceof StaticCall + $exprToSpecify instanceof MethodCall + || $exprToSpecify instanceof StaticCall ) ) ) { if ( - $nodeToSpecify instanceof PropertyFetch - || $nodeToSpecify instanceof MethodCall + $exprToSpecify instanceof PropertyFetch + || $exprToSpecify instanceof MethodCall ) { - $nodeToSpecify = $nodeToSpecify->var; - } elseif ($nodeToSpecify->class instanceof Expr) { - $nodeToSpecify = $nodeToSpecify->class; + $exprToSpecify = $exprToSpecify->var; + } elseif ($exprToSpecify->class instanceof Expr) { + $exprToSpecify = $exprToSpecify->class; } else { break; } - $scope = $scope->specifyExpressionType( - $nodeToSpecify, - TypeCombinator::removeNull($scope->getType($nodeToSpecify)) - ); + $exprType = $scope->getType($exprToSpecify); + $exprTypeWithoutNull = TypeCombinator::removeNull($exprType); + if ($exprType->equals($exprTypeWithoutNull)) { + continue; + } + + $specifiedExpressions[] = new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType); + + $scope = $scope->specifyExpressionType($exprToSpecify, $exprTypeWithoutNull); + } + + return new EnsuredNonNullabilityResult($scope, $specifiedExpressions); + } + + /** + * @param Scope $scope + * @param EnsuredNonNullabilityResultExpression[] $specifiedExpressions + * @return Scope + */ + private function revertNonNullability(Scope $scope, array $specifiedExpressions): Scope + { + foreach ($specifiedExpressions as $specifiedExpressionResult) { + $scope = $scope->specifyExpressionType($specifiedExpressionResult->getExpression(), $specifiedExpressionResult->getOriginalType()); } return $scope; } - private function lookForEnterVariableAssign(Scope $scope, Expr $node): Scope + private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr { - if ($node instanceof Variable) { - $scope = $scope->enterExpressionAssign($node); - } elseif ($node instanceof ArrayDimFetch) { - while ($node instanceof ArrayDimFetch) { - $scope = $scope->enterExpressionAssign($node); - $node = $node->var; + if (($expr instanceof MethodCall || $expr instanceof Expr\StaticCall) && count($this->earlyTerminatingMethodCalls) > 0) { + if ($expr->name instanceof Expr) { + return null; } - if ($node instanceof Variable) { - $scope = $scope->enterExpressionAssign($node); + if ($expr instanceof MethodCall) { + $methodCalledOnType = $scope->getType($expr->var); + } else { + if ($expr->class instanceof Name) { + $methodCalledOnType = $scope->getFunctionType($expr->class, false, false); + } else { + $methodCalledOnType = $scope->getType($expr->class); + } } - } elseif ($node instanceof List_ || $node instanceof Array_) { - foreach ($node->items as $listItem) { - if ($listItem === null) { + + $directClassNames = TypeUtils::getDirectClassNames($methodCalledOnType); + foreach ($directClassNames as $referencedClass) { + if (!$this->broker->hasClass($referencedClass)) { continue; } - $scope = $this->lookForEnterVariableAssign($scope, $listItem->value); - } - } elseif ($node instanceof AssignRef) { - $scope = $scope->enterExpressionAssign($node->expr); - } elseif ($node instanceof Assign && $node->expr instanceof Expr\Closure) { - foreach ($node->expr->uses as $closureUse) { - if ( - !$closureUse->byRef - || !$node->var instanceof Variable - || !is_string($closureUse->var->name) - || $node->var->name !== $closureUse->var->name - ) { - continue; - } - - $scope = $scope->enterExpressionAssign(new Variable($closureUse->var->name)); - } - } else { - $scope = $scope->enterExpressionAssign($node); - } - - return $scope; - } - - private function lookForAssigns( - Scope $scope, - \PhpParser\Node $node, - TrinaryLogic $certainty, - LookForAssignsSettings $lookForAssignsSettings - ): Scope - { - if ($node instanceof Node\Stmt\Expression) { - $node = $node->expr; - } - - if ($node instanceof StaticVar) { - if (!is_string($node->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - $scope = $scope->assignVariable( - $node->var->name, - new MixedType(), - $certainty - ); - } elseif ($node instanceof Static_) { - foreach ($node->vars as $var) { - $scope = $this->lookForAssigns($scope, $var, $certainty, $lookForAssignsSettings); - } - } elseif ($node instanceof If_) { - $conditionType = $scope->getType($node->cond)->toBoolean(); - $scope = $this->lookForAssigns($scope, $node->cond, $certainty, $lookForAssignsSettings); - $statements = []; - - if (!$conditionType instanceof ConstantBooleanType || $conditionType->getValue()) { - $statements[] = new StatementList( - $scope, - array_merge([$node->cond], $node->stmts), - false, - static function (Scope $scope) use ($node): Scope { - return $scope->filterByTruthyValue($node->cond); + $classReflection = $this->broker->getClass($referencedClass); + foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames()) as $className) { + if (!isset($this->earlyTerminatingMethodCalls[$className])) { + continue; } + + if (in_array((string) $expr->name, $this->earlyTerminatingMethodCalls[$className], true)) { + return $expr; + } + } + } + } + + if ($expr instanceof Exit_) { + return $expr; + } + + return null; + } + + /** + * @param \PhpParser\Node\Expr $expr + * @param \PHPStan\Analyser\Scope $scope + * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param \PHPStan\Analyser\ExpressionContext $context + * @return \PHPStan\Analyser\ExpressionResult + */ + private function processExprNode(Expr $expr, Scope $scope, \Closure $nodeCallback, ExpressionContext $context): ExpressionResult + { + $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $context); + + // todo handle all expr descendants + if ($expr instanceof Variable) { + if ($expr->name instanceof Expr) { + return $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep()); + } + } elseif ($expr instanceof Assign || $expr instanceof AssignRef) { + if (!$expr->var instanceof Array_ && !$expr->var instanceof List_) { + $scope = $this->processAssignVar( + $scope, + $expr->var, + $expr->expr, + $nodeCallback, + $context, + function (Scope $scope) use ($expr, $nodeCallback, $context): Scope { + if ($expr instanceof AssignRef) { + $scope = $scope->enterExpressionAssign($expr->expr); + } + + if ($expr->var instanceof Variable && is_string($expr->var->name)) { + $context = $context->enterRightSideAssign( + $expr->var->name, + $scope->getType($expr->expr) + ); + } + + if ($expr->expr instanceof Expr\Closure) { + $nodeCallback($expr->expr, $scope); + $scope = $this->processClosureNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()); + } else { + $scope = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } + + if ($expr instanceof AssignRef) { + $scope = $scope->exitExpressionAssign($expr->expr); + } + + return $scope; + }, + true + ); + } else { + $scope = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep())->getScope(); + foreach ($expr->var->items as $arrayItem) { + if ($arrayItem === null) { + continue; + } + + $itemScope = $scope; + if ($arrayItem->value instanceof ArrayDimFetch && $arrayItem->value->dim === null) { + $itemScope = $itemScope->enterExpressionAssign($arrayItem->value); + } + $itemScope = $this->lookForEnterVariableAssign($itemScope, $arrayItem->value); + + $this->processExprNode($arrayItem, $itemScope, $nodeCallback, $context->enterDeep()); + } + $scope = $this->lookForArrayDestructuringArray($scope, $expr->var, $scope->getType($expr->expr)); + } + + if ($expr->var instanceof Variable && is_string($expr->var->name)) { + $comment = CommentHelper::getDocComment($expr); + if ($comment !== null) { + $scope = $this->processVarAnnotation($scope, $expr->var->name, $comment, false); + } + } + } elseif ($expr instanceof Expr\AssignOp) { + $scope = $this->processAssignVar( + $scope, + $expr->var, + $expr, + $nodeCallback, + $context, + function (Scope $scope) use ($expr, $nodeCallback, $context): Scope { + return $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep())->getScope(); + }, + false + ); + } elseif ($expr instanceof FuncCall) { + $parametersAcceptor = null; + if ($expr->name instanceof Expr) { + $scope = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } elseif ($this->broker->hasFunction($expr->name, $scope)) { + $function = $this->broker->getFunction($expr->name, $scope); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->args, + $function->getVariants() ); } - - if (!$conditionType instanceof ConstantBooleanType || !$conditionType->getValue()) { - $lastElseIfConditionIsTrue = false; - $elseIfScope = $scope; - $lastCond = $node->cond; - foreach ($node->elseifs as $elseIf) { - $elseIfScope = $elseIfScope->filterByFalseyValue($lastCond); - $lastCond = $elseIf->cond; - $elseIfConditionType = $elseIfScope->getType($elseIf->cond)->toBoolean(); - if ( - $elseIfConditionType instanceof ConstantBooleanType - && !$elseIfConditionType->getValue() - ) { - continue; - } - $statements[] = new StatementList( - $elseIfScope, - array_merge([$elseIf->cond], $elseIf->stmts), - false, - static function (Scope $scope) use ($elseIf): Scope { - return $scope->filterByTruthyValue($elseIf->cond); - } - ); - if ( - $elseIfConditionType instanceof ConstantBooleanType - && $elseIfConditionType->getValue() - ) { - $lastElseIfConditionIsTrue = true; - break; - } - } - - if (!$lastElseIfConditionIsTrue) { - $statements[] = new StatementList( - $elseIfScope, - $node->else !== null ? $node->else->stmts : [], - false, - static function (Scope $scope) use ($lastCond): Scope { - return $scope->filterByFalseyValue($lastCond); - } - ); - } - } - - $scope = $this->lookForAssignsInBranches($scope, $statements, $lookForAssignsSettings); - } elseif ($node instanceof TryCatch) { - $statements = [ - new StatementList($scope, $node->stmts), - ]; - foreach ($node->catches as $catch) { - if (!is_string($catch->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - $statements[] = new StatementList($scope->enterCatch( - $catch->types, - $catch->var->name - ), array_merge([new Node\Stmt\Nop()], $catch->stmts)); - } - - $scope = $this->lookForAssignsInBranches($scope, $statements, $lookForAssignsSettings); - if ($node->finally !== null) { - foreach ($node->finally->stmts as $statement) { - $scope = $this->lookForAssigns($scope, $statement, $certainty, $lookForAssignsSettings); - } - } - } elseif ($node instanceof MethodCall || $node instanceof FuncCall || $node instanceof Expr\StaticCall) { - if ($node instanceof MethodCall) { - $scope = $this->lookForAssigns($scope, $node->var, $certainty, $lookForAssignsSettings); - } - foreach ($node->args as $argument) { - $scope = $this->lookForAssigns($scope, $argument, $certainty, $lookForAssignsSettings); - } - - $parametersAcceptor = $this->findParametersAcceptorInFunctionCall($node, $scope); - - if ($parametersAcceptor !== null) { - $parameters = $parametersAcceptor->getParameters(); - foreach ($node->args as $i => $arg) { - $assignByReference = false; - if (isset($parameters[$i])) { - $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable(); - } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { - $lastParameter = $parameters[count($parameters) - 1]; - $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); - } - - if (!$assignByReference) { - continue; - } - - $arg = $node->args[$i]->value; - if (!($arg instanceof Variable) || !is_string($arg->name)) { - continue; - } - - $scope = $scope->assignVariable($arg->name, new MixedType(), $certainty); - } - } + $scope = $this->processArgs($parametersAcceptor, $expr->args, $scope, $nodeCallback, $context); if ( - $node instanceof FuncCall - && $node->name instanceof Name - && in_array((string) $node->name, [ - 'fopen', - 'file_get_contents', - ], true) + isset($function) + && in_array($function->getName(), ['array_pop', 'array_shift'], true) + && count($expr->args) >= 1 ) { - $scope = $scope->assignVariable('http_response_header', new ArrayType(new IntegerType(), new StringType()), $certainty); + $arrayArg = $expr->args[0]->value; + $constantArrays = TypeUtils::getConstantArrays($scope->getType($arrayArg)); + if (count($constantArrays) > 0) { + $resultArrayTypes = []; + + foreach ($constantArrays as $constantArray) { + if ($function->getName() === 'array_pop') { + $resultArrayTypes[] = $constantArray->removeLast(); + } else { + $resultArrayTypes[] = $constantArray->removeFirst(); + } + } + + $scope = $scope->specifyExpressionType( + $arrayArg, + TypeCombinator::union(...$resultArrayTypes) + ); + } } if ( - $node instanceof FuncCall - && $node->name instanceof Name - && in_array(strtolower((string) $node->name), [ - 'array_push', - 'array_unshift', - ], true) - && count($node->args) >= 2 + isset($function) + && in_array($function->getName(), ['array_push', 'array_unshift'], true) + && count($expr->args) >= 2 ) { $argumentTypes = []; - foreach (array_slice($node->args, 1) as $callArg) { + foreach (array_slice($expr->args, 1) as $callArg) { $callArgType = $scope->getType($callArg->value); if ($callArg->unpack) { $iterableValueType = $callArgType->getIterableValueType(); @@ -1204,12 +1174,11 @@ class NodeScopeResolver $argumentTypes[] = $callArgType; } - $arrayArg = $node->args[0]->value; + $arrayArg = $expr->args[0]->value; $originalArrayType = $scope->getType($arrayArg); - $functionName = strtolower((string) $node->name); $constantArrays = TypeUtils::getConstantArrays($originalArrayType); if ( - $functionName === 'array_push' + $function->getName() === 'array_push' || ($originalArrayType instanceof ArrayType && count($constantArrays) === 0) ) { $arrayType = $originalArrayType; @@ -1245,378 +1214,592 @@ class NodeScopeResolver ); } } + if ( - $node instanceof FuncCall - && $node->name instanceof Name - && in_array(strtolower((string) $node->name), [ - 'array_pop', - 'array_shift', - ], true) - && count($node->args) >= 1 + isset($function) + && in_array($function->getName(), ['fopen', 'file_get_contents'], true) ) { - $arrayArg = $node->args[0]->value; - $constantArrays = TypeUtils::getConstantArrays($scope->getType($arrayArg)); - $functionName = strtolower((string) $node->name); - if (count($constantArrays) > 0) { - $resultArrayTypes = []; + $scope = $scope->assignVariable('http_response_header', new ArrayType(new IntegerType(), new StringType())); + } + } elseif ($expr instanceof MethodCall) { + $originalScope = $scope; + if ( + $expr->var instanceof Expr\Closure + && $expr->name instanceof Node\Identifier + && strtolower($expr->name->name) === 'call' + && isset($expr->args[0]) + ) { + $closureCallScope = $scope->enterClosureCall($scope->getType($expr->args[0]->value)); + } - foreach ($constantArrays as $constantArray) { - if ($functionName === 'array_pop') { - $resultArrayTypes[] = $constantArray->removeLast(); - } else { - $resultArrayTypes[] = $constantArray->removeFirst(); - } - } - - $scope = $scope->specifyExpressionType( - $arrayArg, - TypeCombinator::union(...$resultArrayTypes) + $scope = $this->processExprNode($expr->var, $closureCallScope ?? $scope, $nodeCallback, $context->enterDeep())->getScope(); + if (isset($closureCallScope)) { + $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); + } + $parametersAcceptor = null; + if ($expr->name instanceof Expr) { + $scope = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } else { + $calledOnType = $scope->getType($expr->var); + $methodName = $expr->name->name; + if ($calledOnType->hasMethod($methodName)->yes()) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->args, + $calledOnType->getMethod($methodName, $scope)->getVariants() ); } } - } elseif ($node instanceof BinaryOp) { - $scope = $this->lookForAssigns($scope, $node->left, $certainty, $lookForAssignsSettings); - $scope = $this->lookForAssigns($scope, $node->right, $certainty, $lookForAssignsSettings); - } elseif ($node instanceof Arg) { - $scope = $this->lookForAssigns($scope, $node->value, $certainty, $lookForAssignsSettings); - } elseif ($node instanceof BooleanNot) { - $scope = $this->lookForAssigns($scope, $node->expr, $certainty, $lookForAssignsSettings); - } elseif ($node instanceof Ternary) { - $scope = $this->lookForAssigns($scope, $node->cond, $certainty, $lookForAssignsSettings); - $statements = []; - if ($node->if !== null) { - $statements[] = new StatementList( - $scope->filterByTruthyValue($node->cond), - [$node->if] - ); - } else { - $statements[] = new StatementList( - $scope->filterByTruthyValue($node->cond), - [$node->cond] - ); + $scope = $this->processArgs($parametersAcceptor, $expr->args, $scope, $nodeCallback, $context); + } elseif ($expr instanceof StaticCall) { + if ($expr->class instanceof Expr) { + $scope = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep())->getScope(); } - $statements[] = new StatementList( - $scope->filterByFalseyValue($node->cond), - [$node->else] - ); - $scope = $this->lookForAssignsInBranches($scope, $statements, $lookForAssignsSettings); - } elseif ($node instanceof Array_) { - foreach ($node->items as $item) { - if ($item === null) { - continue; - } - if ($item->key !== null) { - $scope = $this->lookForAssigns($scope, $item->key, $certainty, $lookForAssignsSettings); - } - $scope = $this->lookForAssigns($scope, $item->value, $certainty, $lookForAssignsSettings); - } - } elseif ($node instanceof New_) { - foreach ($node->args as $arg) { - $scope = $this->lookForAssigns($scope, $arg, $certainty, $lookForAssignsSettings); - } - } elseif ($node instanceof Do_) { - $scope = $this->lookForAssignsInBranches($scope, [ - new StatementList($scope, $node->stmts), - ], LookForAssignsSettings::afterLoop()); - $scope = $this->lookForAssigns($scope, $node->cond, TrinaryLogic::createYes(), LookForAssignsSettings::afterLoop()); - $scope = $scope->filterByFalseyValue($node->cond); - } elseif ($node instanceof Switch_) { - $scope = $this->lookForAssigns( - $scope, - $node->cond, - TrinaryLogic::createYes(), - LookForAssignsSettings::default() - ); - $statementLists = []; - $tmpStatements = []; - $hasDefault = false; - foreach ($node->cases as $case) { - if ($case->cond === null) { - $hasDefault = true; - } + $parametersAcceptor = null; + if ($expr->name instanceof Expr) { + $scope = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } elseif ($expr->class instanceof Name) { + $className = $scope->resolveName($expr->class); + if ($this->broker->hasClass($className)) { + $classReflection = $this->broker->getClass($className); + if (is_string($expr->name)) { + $methodName = $expr->name; + } else { + $methodName = $expr->name->name; + } + if ($classReflection->hasMethod($methodName)) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->args, + $classReflection->getMethod($methodName, $scope)->getVariants() + ); + if ( + $classReflection->getName() === 'Closure' + && strtolower($methodName) === 'bind' + ) { + $thisType = null; + if (isset($expr->args[1])) { + $argType = $scope->getType($expr->args[1]->value); + if ($argType instanceof NullType) { + $thisType = null; + } else { + $thisType = $argType; + } + } + $scopeClass = 'static'; + if (isset($expr->args[2])) { + $argValue = $expr->args[2]->value; + $argValueType = $scope->getType($argValue); - foreach ($case->stmts as $statement) { - $tmpStatements[] = $statement; - if ($this->findStatementEarlyTermination($statement, $scope) !== null) { - $statementLists[] = new StatementList($scope, $tmpStatements); - $tmpStatements = []; - break; + $directClassNames = TypeUtils::getDirectClassNames($argValueType); + if (count($directClassNames) === 1) { + $scopeClass = $directClassNames[0]; + } elseif ( + $argValue instanceof Expr\ClassConstFetch + && $argValue->name instanceof Node\Identifier + && strtolower($argValue->name->name) === 'class' + && $argValue->class instanceof Name + ) { + $scopeClass = $scope->resolveName($argValue->class); + } elseif ($argValueType instanceof ConstantStringType) { + $scopeClass = $argValueType->getValue(); + } + } + $closureBindScope = $scope->enterClosureBind($thisType, $scopeClass); + } } } } - - if (count($tmpStatements) > 0) { - $statementLists[] = new StatementList($scope, $tmpStatements); + $scope = $this->processArgs($parametersAcceptor, $expr->args, $scope, $nodeCallback, $context, $closureBindScope ?? null); + } elseif ($expr instanceof PropertyFetch) { + $scope = $this->processExprNode($expr->var, $scope, $nodeCallback, $context->enterDeep())->getScope(); + if ($expr->name instanceof Expr) { + $scope = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } + } elseif ($expr instanceof StaticPropertyFetch) { + if ($expr->class instanceof Expr) { + $scope = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } + if ($expr->name instanceof Expr) { + $scope = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } + } elseif ($expr instanceof Expr\Closure) { + $scope = $this->processClosureNode($expr, $scope, $nodeCallback, $context); + } elseif ($expr instanceof Expr\ClosureUse) { + $this->processExprNode($expr->var, $scope, $nodeCallback, $context)->getScope(); + } elseif ($expr instanceof ErrorSuppress) { + $scope = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context)->getScope(); + } elseif ($expr instanceof Exit_) { + if ($expr->expr !== null) { + $scope = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } + } elseif ($expr instanceof Node\Scalar\Encapsed) { + foreach ($expr->parts as $part) { + $scope = $this->processExprNode($part, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } + } elseif ($expr instanceof ArrayDimFetch) { + if ($expr->dim !== null) { + $scope = $this->processExprNode($expr->dim, $scope, $nodeCallback, $context->enterDeep())->getScope(); } - if (!$hasDefault) { - $statementLists[] = new StatementList($scope, []); + $scope = $this->processExprNode($expr->var, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } elseif ($expr instanceof Array_) { + foreach ($expr->items as $arrayItem) { + $scope = $this->processExprNode($arrayItem, $scope, $nodeCallback, $context->enterDeep())->getScope(); } + } elseif ($expr instanceof ArrayItem) { + if ($expr->key !== null) { + $scope = $this->processExprNode($expr->key, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } + $scope = $this->processExprNode($expr->value, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } elseif ($expr instanceof BooleanAnd || $expr instanceof BinaryOp\LogicalAnd) { + $leftResult = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); + $rightResult = $this->processExprNode($expr->right, $leftResult->getTruthyScope(), $nodeCallback, $context); + $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope()); - $scope = $this->lookForAssignsInBranches($scope, $statementLists, LookForAssignsSettings::afterSwitch()); - } elseif ($node instanceof Cast) { - $scope = $this->lookForAssigns($scope, $node->expr, $certainty, $lookForAssignsSettings); - } elseif ($node instanceof For_) { - $forAssignmentsCertainty = $this->polluteScopeWithLoopInitialAssignments ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe(); - foreach ($node->init as $initExpr) { - $scope = $this->lookForAssigns($scope, $initExpr, $forAssignmentsCertainty, LookForAssignsSettings::afterLoop()); - } - - foreach ($node->cond as $condExpr) { - $scope = $this->lookForAssigns($scope, $condExpr, $forAssignmentsCertainty, LookForAssignsSettings::afterLoop()); - } - - $statements = [ - new StatementList($scope, $node->stmts), - new StatementList($scope, []), // in order not to add variables existing only inside the for loop - ]; - $scope = $this->lookForAssignsInBranches($scope, $statements, LookForAssignsSettings::afterLoop()); - foreach ($node->loop as $loopExpr) { - $scope = $this->lookForAssigns($scope, $loopExpr, TrinaryLogic::createMaybe(), LookForAssignsSettings::afterLoop()); - } - } elseif ($node instanceof While_) { - $whileAssignmentsCertainty = $this->polluteScopeWithLoopInitialAssignments ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe(); - $scope = $this->lookForAssigns($scope, $node->cond, $whileAssignmentsCertainty, LookForAssignsSettings::afterLoop()); - - $statements = [ - new StatementList($scope, $node->stmts, false, static function (Scope $scope) use ($node): Scope { - return $scope->filterByTruthyValue($node->cond); - }), - new StatementList($scope, []), // in order not to add variables existing only inside the for loop - ]; - $scope = $this->lookForAssignsInBranches($scope, $statements, LookForAssignsSettings::afterLoop()); - } elseif ($node instanceof ErrorSuppress) { - $scope = $this->lookForAssigns($scope, $node->expr, $certainty, $lookForAssignsSettings); - } elseif ($node instanceof \PhpParser\Node\Stmt\Unset_) { - foreach ($node->vars as $var) { - $scope = $scope->unsetExpression($var); - } - } elseif ($node instanceof Echo_) { - foreach ($node->exprs as $echoedExpr) { - $scope = $this->lookForAssigns($scope, $echoedExpr, $certainty, $lookForAssignsSettings); - } - } elseif ($node instanceof Print_) { - $scope = $this->lookForAssigns($scope, $node->expr, $certainty, $lookForAssignsSettings); - } elseif ($node instanceof Foreach_) { - $iterableAtLeastOnce = $scope->getType($node->expr)->isIterableAtLeastOnce(); - $scope = $this->lookForAssigns($scope, $node->expr, $certainty, $lookForAssignsSettings); - if (!$iterableAtLeastOnce->no()) { - $statements = [ - new StatementList($scope, array_merge( - [new Node\Stmt\Nop()], - $node->stmts - ), false, function (Scope $scope) use ($node): Scope { - return $this->enterForeach($scope, $node); - }), - ]; - - if (!$iterableAtLeastOnce->yes() || !$this->polluteScopeWithAlwaysIterableForeach) { - $statements[] = new StatementList($scope, []); + return new ExpressionResult( + $leftMergedWithRightScope, + static function () use ($expr, $rightResult): Scope { + return $rightResult->getScope()->filterByTruthyValue($expr); + }, + static function () use ($leftMergedWithRightScope, $expr): Scope { + return $leftMergedWithRightScope->filterByFalseyValue($expr); } + ); + // todo do not execute right side if the left is false + } elseif ($expr instanceof BooleanOr || $expr instanceof BinaryOp\LogicalOr) { + $leftResult = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep()); + $rightResult = $this->processExprNode($expr->right, $leftResult->getFalseyScope(), $nodeCallback, $context); + $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope()); - $scope = $this->lookForAssignsInBranches($scope, $statements, LookForAssignsSettings::afterLoop()); - } - } elseif ($node instanceof Isset_) { - foreach ($node->vars as $var) { - $scope = $this->lookForAssigns($scope, $var, $certainty, $lookForAssignsSettings); - } - } elseif ($node instanceof Expr\Empty_ && (!($node->expr instanceof ArrayDimFetch) || $node->expr->dim !== null)) { - $scope = $this->lookForAssigns($scope, $node->expr, $certainty, $lookForAssignsSettings); - } elseif ($node instanceof ArrayDimFetch && $node->dim !== null) { - $scope = $this->lookForAssigns($scope, $node->dim, $certainty, $lookForAssignsSettings); - } elseif ($node instanceof Expr\Closure) { - $closureScope = $scope->enterAnonymousFunction($node); - $statements = [ - new StatementList($closureScope, array_merge( - [new Node\Stmt\Nop()], - $node->stmts - )), - new StatementList($closureScope, []), - ]; - $closureScope = $this->lookForAssignsInBranches($scope, $statements, LookForAssignsSettings::insideClosure()); - foreach ($node->uses as $closureUse) { - if (!$closureUse->byRef) { - continue; + return new ExpressionResult( + $leftMergedWithRightScope, + static function () use ($leftMergedWithRightScope, $expr): Scope { + return $leftMergedWithRightScope->filterByTruthyValue($expr); + }, + static function () use ($expr, $rightResult): Scope { + return $rightResult->getScope()->filterByFalseyValue($expr); } + ); + // todo do not execute right side if the left is true + } elseif ($expr instanceof Coalesce) { + $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left, false); - if (!is_string($closureUse->var->name)) { - throw new \PHPStan\ShouldNotHappenException(); - } - $variableCertainty = $closureScope->hasVariableType($closureUse->var->name); - if ($variableCertainty->no()) { - continue; - } - - $scope = $scope->assignVariable( - $closureUse->var->name, - $closureScope->getVariableType($closureUse->var->name), - $variableCertainty - ); + if ($expr->left instanceof PropertyFetch) { + $scope = $nonNullabilityResult->getScope(); + } else { + $scope = $this->lookForEnterVariableAssign($nonNullabilityResult->getScope(), $expr->left); } - } elseif ($node instanceof Instanceof_) { - $scope = $this->lookForAssigns($scope, $node->expr, $certainty, $lookForAssignsSettings); - } elseif ($node instanceof Expr\Include_) { - $scope = $this->lookForAssigns($scope, $node->expr, $certainty, $lookForAssignsSettings); + $scope = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep())->getScope(); + $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); + + if (!$expr->left instanceof PropertyFetch) { + $scope = $this->lookForExitVariableAssign($scope, $expr->left); + } + $scope = $this->processExprNode($expr->right, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } elseif ($expr instanceof BinaryOp) { + $scope = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep())->getScope(); + $scope = $this->processExprNode($expr->right, $scope, $nodeCallback, $context->enterDeep())->getScope(); } elseif ( - ( - $node instanceof Expr\PostInc - || $node instanceof Expr\PostDec - || $node instanceof Expr\PreInc - || $node instanceof Expr\PreDec - ) && ( - $node->var instanceof Variable - || $node->var instanceof ArrayDimFetch - || $node->var instanceof PropertyFetch - || $node->var instanceof StaticPropertyFetch - ) + $expr instanceof Expr\BitwiseNot + || $expr instanceof Cast + || $expr instanceof Expr\Clone_ + || $expr instanceof Expr\Eval_ + || $expr instanceof Expr\Include_ + || $expr instanceof Expr\Print_ + || $expr instanceof Expr\UnaryMinus + || $expr instanceof Expr\UnaryPlus + || $expr instanceof Expr\YieldFrom ) { - $expressionType = $scope->getType($node); - if ($expressionType instanceof ConstantScalarType) { - $afterValue = $expressionType->getValue(); - if ($node instanceof Expr\PostInc) { - $afterValue++; - } elseif ($node instanceof Expr\PostDec) { - $afterValue--; + $scope = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } elseif ($expr instanceof BooleanNot) { + $scope = $scope->enterNegation(); + $scope = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep())->getScope(); + $scope = $scope->enterNegation(); + } elseif ($expr instanceof Expr\ClassConstFetch) { + if ($expr->class instanceof Expr) { + $scope = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } + } elseif ($expr instanceof Expr\Empty_) { + $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->expr, true); + $scope = $this->lookForEnterVariableAssign($nonNullabilityResult->getScope(), $expr->expr); + $scope = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep())->getScope(); + $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); + $scope = $this->lookForExitVariableAssign($scope, $expr->expr); + } elseif ($expr instanceof Expr\Isset_) { + foreach ($expr->vars as $var) { + $nonNullabilityResult = $this->ensureNonNullability($scope, $var, true); + $scope = $this->lookForEnterVariableAssign($nonNullabilityResult->getScope(), $var); + $scope = $this->processExprNode($var, $scope, $nodeCallback, $context->enterDeep())->getScope(); + $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions()); + $scope = $this->lookForExitVariableAssign($scope, $var); + } + } elseif ($expr instanceof Instanceof_) { + $scope = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep())->getScope(); + if ($expr->class instanceof Expr) { + $scope = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } + } elseif ($expr instanceof List_) { + // only in assign and foreach, processed elsewhere + return new ExpressionResult($scope); + } elseif ($expr instanceof New_) { + $parametersAcceptor = null; + if ($expr->class instanceof Expr) { + $scope = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep())->getScope(); + } elseif ($expr->class instanceof Class_) { + $this->broker->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name + $this->processStmtNode($expr->class, $scope, $nodeCallback); + } elseif ($this->broker->hasClass($expr->class->toString())) { + $classReflection = $this->broker->getClass($expr->class->toString()); + if ($classReflection->hasConstructor()) { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->args, + $classReflection->getConstructor()->getVariants() + ); } + } + $scope = $this->processArgs($parametersAcceptor, $expr->args, $scope, $nodeCallback, $context); + } elseif ( + $expr instanceof Expr\PreInc + || $expr instanceof Expr\PostInc + || $expr instanceof Expr\PreDec + || $expr instanceof Expr\PostDec + ) { + $scope = $this->processExprNode($expr->var, $scope, $nodeCallback, $context->enterDeep())->getScope(); + if ( + $expr->var instanceof Variable + || $expr->var instanceof ArrayDimFetch + || $expr->var instanceof PropertyFetch + || $expr->var instanceof StaticPropertyFetch + ) { + $expressionType = $scope->getType($expr); + if (count(TypeUtils::getConstantScalars($expressionType)) > 0) { + $newExpr = $expr; + if ($expr instanceof Expr\PostInc) { + $newExpr = new Expr\PreInc($expr->var); + } elseif ($expr instanceof Expr\PostDec) { + $newExpr = new Expr\PreDec($expr->var); + } - $newExpressionType = $scope->getTypeFromValue($afterValue); - if ($lookForAssignsSettings->shouldGeneralizeConstantTypesOfNonIdempotentOperations()) { - $newExpressionType = TypeUtils::generalizeType($newExpressionType); + $scope = $this->processAssignVar( + $scope, + $expr->var, + $newExpr, + static function (): void { + }, + $context, + static function (Scope $scope): Scope { + return $scope; + }, + false + ); } - - $scope = $this->assignVariable( - $scope, - $node->var, - $certainty, - $newExpressionType - ); } - } elseif ($node instanceof Expr\Yield_) { - if ($node->key !== null) { - $scope = $this->lookForAssigns($scope, $node->key, $certainty, $lookForAssignsSettings); - } - if ($node->value !== null) { - $scope = $this->lookForAssigns($scope, $node->value, $certainty, $lookForAssignsSettings); - } - } elseif ($node instanceof Expr\YieldFrom) { - $scope = $this->lookForAssigns($scope, $node->expr, $certainty, $lookForAssignsSettings); - } + } elseif ($expr instanceof Ternary) { + $ternaryCondResult = $this->processExprNode($expr->cond, $scope, $nodeCallback, $context->enterDeep()); + $ifTrueScope = $ternaryCondResult->getTruthyScope(); + $ifFalseScope = $ternaryCondResult->getFalseyScope(); - $scope = $this->updateScopeForVariableAssign($scope, $node, $certainty, $lookForAssignsSettings); - - return $scope; - } - - private function updateScopeForVariableAssign( - Scope $scope, - \PhpParser\Node $node, - TrinaryLogic $certainty, - LookForAssignsSettings $lookForAssignsSettings - ): Scope - { - if ($node instanceof Assign || $node instanceof AssignRef || $node instanceof Expr\AssignOp || $node instanceof Node\Stmt\Global_) { - if ($node instanceof Assign || $node instanceof AssignRef || $node instanceof Expr\AssignOp) { - $scope = $this->lookForAssigns($scope, $node->var, TrinaryLogic::createYes(), $lookForAssignsSettings); - $vars = [$node->var]; + if ($expr->if !== null) { + $ifTrueScope = $this->processExprNode($expr->if, $ifTrueScope, $nodeCallback, $context)->getScope(); + $ifFalseScope = $this->processExprNode($expr->else, $ifFalseScope, $nodeCallback, $context)->getScope(); } else { - $vars = $node->vars; + $ifFalseScope = $this->processExprNode($expr->else, $ifFalseScope, $nodeCallback, $context)->getScope(); } - foreach ($vars as $var) { - if ( - !$var instanceof Variable - && !$var instanceof ArrayDimFetch - && !$var instanceof PropertyFetch - && !$var instanceof StaticPropertyFetch - ) { - continue; - } + $finalScope = $ifTrueScope->mergeWith($ifFalseScope); - $type = null; - if ($node instanceof Assign || $node instanceof AssignRef) { - $type = $scope->getType($node->expr); - } elseif ($node instanceof Expr\AssignOp) { - $type = $scope->getType($node); - if ($lookForAssignsSettings->shouldGeneralizeConstantTypesOfNonIdempotentOperations()) { - $type = TypeUtils::generalizeType($type); - } + return new ExpressionResult( + $finalScope, + static function () use ($finalScope, $expr): Scope { + return $finalScope->filterByTruthyValue($expr); + }, + static function () use ($finalScope, $expr): Scope { + return $finalScope->filterByFalseyValue($expr); } + ); - $scope = $this->assignVariable($scope, $var, $certainty, $type); - if ( - (!$node instanceof Assign && !$node instanceof AssignRef) - || !$lookForAssignsSettings->shouldGeneralizeConstantTypesOfNonIdempotentOperations() - || !($var instanceof ArrayDimFetch) - || $var->dim !== null - ) { - continue; - } - - $type = $scope->getType($var->var); - $scope = $this->assignVariable($scope, $var->var, $certainty, TypeUtils::generalizeType($type)); + // todo do not run else if cond is always true + // todo do not run if branch if cond is always false + } elseif ($expr instanceof Expr\Yield_) { + if ($expr->key !== null) { + $scope = $this->processExprNode($expr->key, $scope, $nodeCallback, $context->enterDeep())->getScope(); } - - if ($node instanceof Assign || $node instanceof AssignRef) { - if ($node->var instanceof Array_ || $node->var instanceof List_) { - $scope = $this->lookForArrayDestructuringArray($scope, $node->var, $scope->getType($node->expr)); - } - } - - if (!$node instanceof Node\Stmt\Global_) { - $scope = $this->lookForAssigns($scope, $node->expr, TrinaryLogic::createYes(), $lookForAssignsSettings); - } - - if ($node instanceof Assign || $node instanceof AssignRef) { - if ($node->var instanceof Variable && is_string($node->var->name)) { - $comment = CommentHelper::getDocComment($node); - if ($comment !== null) { - $scope = $this->processVarAnnotation($scope, $node->var->name, $comment, false); - } - } + if ($expr->value !== null) { + $scope = $this->processExprNode($expr->value, $scope, $nodeCallback, $context->enterDeep())->getScope(); } } - return $scope; - } - - private function processVarAnnotation(Scope $scope, string $variableName, string $comment, bool $strict): Scope - { - $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( - $scope->getFile(), - $scope->isInClass() ? $scope->getClassReflection()->getName() : null, - $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, - $comment + return new ExpressionResult( + $scope, + static function () use ($scope, $expr): Scope { + return $scope->filterByTruthyValue($expr); + }, + static function () use ($scope, $expr): Scope { + return $scope->filterByFalseyValue($expr); + } ); - $varTags = $resolvedPhpDoc->getVarTags(); - - if (isset($varTags[$variableName])) { - $variableType = $varTags[$variableName]->getType(); - return $scope->assignVariable($variableName, $variableType, TrinaryLogic::createYes()); - - } - - if (!$strict && count($varTags) === 1 && isset($varTags[0])) { - $variableType = $varTags[0]->getType(); - return $scope->assignVariable($variableName, $variableType, TrinaryLogic::createYes()); - - } - - return $scope; } - private function assignVariable( + /** + * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param Expr $expr + * @param Scope $scope + * @param ExpressionContext $context + */ + private function callNodeCallbackWithExpression( + \Closure $nodeCallback, + Expr $expr, Scope $scope, - Node $var, - TrinaryLogic $certainty, - ?Type $subNodeType = null + ExpressionContext $context + ): void + { + if ($context->isDeep()) { + $scope = $scope->exitFirstLevelStatements(); + } + $nodeCallback($expr, $scope); + } + + /** + * @param \PhpParser\Node\Expr\Closure $expr + * @param \PHPStan\Analyser\Scope $scope + * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param ExpressionContext $context + * @return \PHPStan\Analyser\Scope $scope + */ + private function processClosureNode( + Expr\Closure $expr, + Scope $scope, + \Closure $nodeCallback, + ExpressionContext $context ): Scope { - if ($var instanceof Variable && is_string($var->name)) { - $scope = $scope->assignVariable($var->name, $subNodeType ?? new MixedType(), $certainty); - } elseif ($var instanceof ArrayDimFetch) { - $subNodeType = $subNodeType ?? new MixedType(); + foreach ($expr->params as $param) { + $this->processParamNode($param, $scope, $nodeCallback); + } + $byRefUses = []; + + $useScope = $scope; + foreach ($expr->uses as $use) { + if ($use->byRef) { + $byRefUses[] = $use; + $useScope = $useScope->enterExpressionAssign($use->var); + + $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName(); + $inAssignRightSideType = $context->getInAssignRightSideType(); + if ( + $inAssignRightSideVariableName === $use->var->name + && $inAssignRightSideType !== null + ) { + if ($inAssignRightSideType instanceof ClosureType) { + $variableType = $inAssignRightSideType; + } else { + $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName); + if ($alreadyHasVariableType->no()) { + $variableType = TypeCombinator::union(new NullType(), $inAssignRightSideType); + } else { + $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType); + } + } + $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType); + } + } + $this->processExprNode($use, $useScope, $nodeCallback, $context); + if (!$use->byRef) { + continue; + } + + $useScope = $useScope->exitExpressionAssign($use->var); + } + + if ($expr->returnType !== null) { + $nodeCallback($expr->returnType, $scope); + } + + $closureScope = $scope->enterAnonymousFunction($expr); + $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses); + if (count($byRefUses) === 0) { + $this->processStmtNodes($expr->stmts, $closureScope, $nodeCallback); + return $scope; + } + + $count = 0; + do { + $prevScope = $closureScope; + + $intermediaryClosureScopeResult = $this->processStmtNodes($expr->stmts, $closureScope, static function (): void { + }); + $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope(); + foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) { + $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); + } + $closureScope = $scope->enterAnonymousFunction($expr); + $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses); + if ($closureScope->equals($prevScope)) { + break; + } + $count++; + } while ($count < self::LOOP_SCOPE_ITERATIONS); + + $this->processStmtNodes($expr->stmts, $closureScope, $nodeCallback); + + return $scope->processClosureScope($closureScope, null, $byRefUses); + } + + private function lookForArrayDestructuringArray(Scope $scope, Expr $expr, Type $valueType): Scope + { + if ($expr instanceof Array_ || $expr instanceof List_) { + foreach ($expr->items as $key => $item) { + /** @var \PhpParser\Node\Expr\ArrayItem|null $itemValue */ + $itemValue = $item; + if ($itemValue === null) { + continue; + } + + $keyType = $itemValue->key === null ? new ConstantIntegerType($key) : $scope->getType($itemValue->key); + $scope = $this->specifyItemFromArrayDestructuring($scope, $itemValue, $valueType, $keyType); + } + } elseif ($expr instanceof Variable && is_string($expr->name)) { + $scope = $scope->assignVariable($expr->name, new MixedType()); + } elseif ($expr instanceof ArrayDimFetch && $expr->var instanceof Variable && is_string($expr->var->name)) { + $scope = $scope->assignVariable($expr->var->name, new MixedType()); + } + + return $scope; + } + + private function specifyItemFromArrayDestructuring(Scope $scope, ArrayItem $arrayItem, Type $valueType, Type $keyType): Scope + { + $type = $valueType->getOffsetValueType($keyType); + + $itemNode = $arrayItem->value; + if ($itemNode instanceof Variable && is_string($itemNode->name)) { + $scope = $scope->assignVariable($itemNode->name, $type); + } elseif ($itemNode instanceof ArrayDimFetch && $itemNode->var instanceof Variable && is_string($itemNode->var->name)) { + $currentType = $scope->hasVariableType($itemNode->var->name)->no() + ? new ConstantArrayType([], []) + : $scope->getVariableType($itemNode->var->name); + $dimType = null; + if ($itemNode->dim !== null) { + $dimType = $scope->getType($itemNode->dim); + } + $scope = $scope->assignVariable($itemNode->var->name, $currentType->setOffsetValueType($dimType, $type)); + } else { + $scope = $this->lookForArrayDestructuringArray($scope, $itemNode, $type); + } + + return $scope; + } + + /** + * @param \PhpParser\Node\Param $param + * @param \PHPStan\Analyser\Scope $scope + * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback + */ + private function processParamNode( + Node\Param $param, + Scope $scope, + \Closure $nodeCallback + ): void + { + if ($param->type !== null) { + $nodeCallback($param->type, $scope); + } + if ($param->default === null) { + return; + } + + $this->processExprNode($param->default, $scope, $nodeCallback, ExpressionContext::createDeep()); + } + + /** + * @param ParametersAcceptor|null $parametersAcceptor + * @param \PhpParser\Node\Arg[] $args + * @param \PHPStan\Analyser\Scope $scope + * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param ExpressionContext $context + * @param \PHPStan\Analyser\Scope|null $closureBindScope + * @return \PHPStan\Analyser\Scope + */ + private function processArgs( + ?ParametersAcceptor $parametersAcceptor, + array $args, + Scope $scope, + \Closure $nodeCallback, + ExpressionContext $context, + ?Scope $closureBindScope = null + ): Scope + { + // todo $scope = $scope->enterFunctionCall(); + if ($parametersAcceptor !== null) { + $parameters = $parametersAcceptor->getParameters(); + } + + foreach ($args as $i => $arg) { + $nodeCallback($arg, $scope); + if (isset($parameters) && $parametersAcceptor !== null) { + $assignByReference = false; + if (isset($parameters[$i])) { + $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable(); + } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) { + $lastParameter = $parameters[count($parameters) - 1]; + $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); + } + + if ($assignByReference) { + $argValue = $arg->value; + if ($argValue instanceof Variable && is_string($argValue->name)) { + $scope = $scope->assignVariable($argValue->name, new MixedType()); + } + } + } + + $originalScope = $scope; + $scopeToPass = $scope; + if ($i === 0 && $closureBindScope !== null) { + $scopeToPass = $closureBindScope; + } + $scope = $this->processExprNode($arg->value, $scopeToPass, $nodeCallback, $context->enterDeep())->getScope(); + if ($i !== 0 || $closureBindScope === null) { + continue; + } + + $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); + } + + // todo $scope = $scope->exitFunctionCall(); + + return $scope; + } + + /** + * @param \PHPStan\Analyser\Scope $scope + * @param \PhpParser\Node\Expr $var + * @param \PhpParser\Node\Expr $assignedExpr + * @param \Closure(\PhpParser\Node $node, Scope $scope): void $nodeCallback + * @param ExpressionContext $context + * @param \Closure(Scope $scope): Scope $processExprCallback + * @param bool $enterExpressionAssign + * @return Scope + */ + private function processAssignVar( + Scope $scope, + Expr $var, + Expr $assignedExpr, + \Closure $nodeCallback, + ExpressionContext $context, + \Closure $processExprCallback, + bool $enterExpressionAssign + ): Scope + { + $nodeCallback($var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope); + if ($var instanceof Variable && is_string($var->name)) { + $scope = $processExprCallback($scope); + $scope = $scope->assignVariable($var->name, $scope->getType($assignedExpr)); + } elseif ($var instanceof ArrayDimFetch) { $dimExprStack = []; while ($var instanceof ArrayDimFetch) { $dimExprStack[] = $var->dim; @@ -1624,7 +1807,13 @@ class NodeScopeResolver } // 1. eval root expr - $scope = $this->lookForAssigns($scope, $var, TrinaryLogic::createYes(), LookForAssignsSettings::default()); + if ($enterExpressionAssign && $var instanceof Variable) { + $scope = $scope->enterExpressionAssign($var); + } + $scope = $this->processExprNode($var, $scope, $nodeCallback, $context->enterDeep())->getScope(); + if ($enterExpressionAssign && $var instanceof Variable) { + $scope = $scope->exitExpressionAssign($var); + } // 2. eval dimensions $offsetTypes = []; @@ -1633,15 +1822,23 @@ class NodeScopeResolver $offsetTypes[] = null; } else { - if ($dimExpr instanceof Expr\PreInc || $dimExpr instanceof Expr\PreDec) { - $dimExpr = $dimExpr->var; - } - $scope = $this->lookForAssigns($scope, $dimExpr, TrinaryLogic::createYes(), LookForAssignsSettings::default()); $offsetTypes[] = $scope->getType($dimExpr); + + if ($enterExpressionAssign) { + $scope->enterExpressionAssign($dimExpr); + } + $scope = $this->processExprNode($dimExpr, $scope, $nodeCallback, $context->enterDeep())->getScope(); + + if ($enterExpressionAssign) { + $scope = $scope->exitExpressionAssign($dimExpr); + } } } - // 3. eval assigned expr, unfortunately this was already done + $valueToWrite = $scope->getType($assignedExpr); + + // 3. eval assigned expr + $scope = $processExprCallback($scope); // 4. compose types $varType = $scope->getType($var); @@ -1664,248 +1861,98 @@ class NodeScopeResolver $offsetValueTypeStack[] = $offsetValueType; } - $valueToWrite = $subNodeType; foreach (array_reverse($offsetTypes) as $offsetType) { /** @var Type $offsetValueType */ $offsetValueType = array_pop($offsetValueTypeStack); $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite); } - if ($valueToWrite instanceof ErrorType) { - $valueToWrite = new ArrayType(new MixedType(), new MixedType()); - } - if ($var instanceof Variable && is_string($var->name)) { - $scope = $scope->assignVariable( - $var->name, - $valueToWrite, - $certainty - ); + $scope = $scope->assignVariable($var->name, $valueToWrite); } else { $scope = $scope->specifyExpressionType( $var, $valueToWrite ); } - } elseif ($var instanceof PropertyFetch && $subNodeType !== null) { - $scope = $scope->specifyExpressionType($var, $subNodeType); - } elseif ($var instanceof Expr\StaticPropertyFetch && $subNodeType !== null) { - $scope = $scope->specifyExpressionType($var, $subNodeType); - } else { - $scope = $this->lookForAssigns($scope, $var, TrinaryLogic::createYes(), LookForAssignsSettings::default()); + } elseif ($var instanceof PropertyFetch) { + $scope = $processExprCallback($scope); + $scope = $scope->specifyExpressionType($var, $scope->getType($assignedExpr)); + } elseif ($var instanceof Expr\StaticPropertyFetch) { + $scope = $processExprCallback($scope); + $scope = $scope->specifyExpressionType($var, $scope->getType($assignedExpr)); } return $scope; } - /** - * @param \PHPStan\Analyser\Scope $initialScope - * @param \PHPStan\Analyser\StatementList[] $statementsLists - * @param \PHPStan\Analyser\LookForAssignsSettings $lookForAssignsSettings - * @param int $counter - * @return Scope - */ - private function lookForAssignsInBranches( - Scope $initialScope, - array $statementsLists, - LookForAssignsSettings $lookForAssignsSettings, - int $counter = 0 - ): Scope + private function processVarAnnotation(Scope $scope, string $variableName, string $comment, bool $strict): Scope { - /** @var \PHPStan\Analyser\Scope|null $intersectedScope */ - $intersectedScope = null; - foreach ($statementsLists as $statementList) { - $statements = $statementList->getStatements(); - $branchScope = $statementList->getScope(); - $branchScopeWithInitialScopeRemoved = $branchScope->removeVariables($initialScope, true); + $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( + $scope->getFile(), + $scope->isInClass() ? $scope->getClassReflection()->getName() : null, + $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, + $comment + ); + $varTags = $resolvedPhpDoc->getVarTags(); - $earlyTerminationStatement = null; - foreach ($statements as $statement) { - $branchScope = $this->lookForAssigns($branchScope, $statement, TrinaryLogic::createYes(), $lookForAssignsSettings); - $branchScopeWithInitialScopeRemoved = $branchScope->removeVariables($initialScope, false); - $earlyTerminationStatement = $this->findStatementEarlyTermination($statement, $branchScope); - if ($earlyTerminationStatement !== null) { - if ($lookForAssignsSettings->shouldSkipBranch($earlyTerminationStatement)) { - continue 2; - } - $branchScopeWithInitialScopeRemoved = $branchScopeWithInitialScopeRemoved->removeSpecified($initialScope); - break; - } - } + if (isset($varTags[$variableName])) { + $variableType = $varTags[$variableName]->getType(); + return $scope->assignVariable($variableName, $variableType); - if (!$lookForAssignsSettings->shouldIntersectVariables($earlyTerminationStatement)) { - continue; - } - - if ($intersectedScope === null) { - $intersectedScope = $initialScope->createIntersectedScope($branchScopeWithInitialScopeRemoved); - } else { - $intersectedScope = $intersectedScope->intersectVariables($branchScopeWithInitialScopeRemoved); - } - - if (!$statementList->shouldFilterByTruthyValue()) { - continue; - } - - /** @var \PhpParser\Node\Expr $statement */ - foreach ($statements as $statement) { - $intersectedScope = $intersectedScope->filterByTruthyValue($statement); - } } - if ($intersectedScope !== null) { - $scope = $initialScope->mergeWithIntersectedScope($intersectedScope); - if ($counter === 0 && $lookForAssignsSettings->shouldRepeatAnalysis()) { - $newStatementLists = []; - foreach ($statementsLists as $statementList) { - $newStatementLists[] = StatementList::fromList( - $scope, - $statementList - ); - } - return $this->lookForAssignsInBranches( - $scope, - $newStatementLists, - $lookForAssignsSettings, - $counter + 1 - ); - } + if (!$strict && count($varTags) === 1 && isset($varTags[0])) { + $variableType = $varTags[0]->getType(); + return $scope->assignVariable($variableName, $variableType); - return $scope; } - return $initialScope; + return $scope; } - /** - * @param \PhpParser\Node[] $statements - * @param \PHPStan\Analyser\Scope $scope - * @return \PhpParser\Node|null - */ - private function findEarlyTermination(array $statements, Scope $scope): ?\PhpParser\Node + private function enterForeach(Scope $scope, Foreach_ $stmt): Scope { - foreach ($statements as $statement) { - $statement = $this->findStatementEarlyTermination($statement, $scope); - if ($statement !== null) { - return $statement; - } + if ($stmt->keyVar !== null && $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) { + $scope = $scope->assignVariable($stmt->keyVar->name, new MixedType()); } - return null; - } - - private function findStatementEarlyTermination(Node $statement, Scope $scope): ?\PhpParser\Node - { - if ($statement instanceof Node\Stmt\Expression) { - $statement = $statement->expr; + $comment = CommentHelper::getDocComment($stmt); + if ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) { + $scope = $scope->enterForeach( + $stmt->expr, + $stmt->valueVar->name, + $stmt->keyVar !== null + && $stmt->keyVar instanceof Variable + && is_string($stmt->keyVar->name) + ? $stmt->keyVar->name + : null + ); + if ($comment !== null) { + $scope = $this->processVarAnnotation($scope, $stmt->valueVar->name, $comment, true); + } } if ( - $statement instanceof Throw_ - || $statement instanceof Return_ - || $statement instanceof Continue_ - || $statement instanceof Break_ - || $statement instanceof Exit_ + $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) + && $comment !== null ) { - return $statement; - } elseif (($statement instanceof MethodCall || $statement instanceof Expr\StaticCall) && count($this->earlyTerminatingMethodCalls) > 0) { - if ($statement->name instanceof Expr) { - return null; - } - - if ($statement instanceof MethodCall) { - $methodCalledOnType = $scope->getType($statement->var); - } else { - if ($statement->class instanceof Name) { - $methodCalledOnType = $scope->getFunctionType($statement->class, false, false); - } else { - $methodCalledOnType = $scope->getType($statement->class); - } - } - - $directClassNames = TypeUtils::getDirectClassNames($methodCalledOnType); - foreach ($directClassNames as $referencedClass) { - if (!$this->broker->hasClass($referencedClass)) { - continue; - } - - $classReflection = $this->broker->getClass($referencedClass); - foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames()) as $className) { - if (!isset($this->earlyTerminatingMethodCalls[$className])) { - continue; - } - - if (in_array((string) $statement->name, $this->earlyTerminatingMethodCalls[$className], true)) { - return $statement; - } - } - } - - return null; - } elseif ($statement instanceof If_) { - if ($statement->else === null) { - return null; - } - - if ($this->findEarlyTermination($statement->stmts, $scope) === null) { - return null; - } - - foreach ($statement->elseifs as $elseIfStatement) { - if ($this->findEarlyTermination($elseIfStatement->stmts, $scope) === null) { - return null; - } - } - - if ($this->findEarlyTermination($statement->else->stmts, $scope) === null) { - return null; - } - - return $statement; + $scope = $this->processVarAnnotation($scope, $stmt->keyVar->name, $comment, true); } - return null; - } + if ($stmt->valueVar instanceof List_ || $stmt->valueVar instanceof Array_) { + $itemTypes = []; + $exprType = $scope->getType($stmt->expr); + $arrayTypes = TypeUtils::getArrays($exprType); + foreach ($arrayTypes as $arrayType) { + $itemTypes[] = $arrayType->getItemType(); + } - private function findParametersAcceptorInFunctionCall(Expr $functionCall, Scope $scope): ?\PHPStan\Reflection\ParametersAcceptor - { - if ($functionCall instanceof FuncCall && $functionCall->name instanceof Name) { - if ($this->broker->hasFunction($functionCall->name, $scope)) { - return ParametersAcceptorSelector::selectFromArgs( - $scope, - $functionCall->args, - $this->broker->getFunction($functionCall->name, $scope)->getVariants() - ); - } - } elseif ($functionCall instanceof MethodCall && $functionCall->name instanceof Node\Identifier) { - $type = $scope->getType($functionCall->var); - $methodName = $functionCall->name->name; - if ($type->hasMethod($methodName)->yes()) { - return ParametersAcceptorSelector::selectFromArgs( - $scope, - $functionCall->args, - $type->getMethod($methodName, $scope)->getVariants() - ); - } - } elseif ( - $functionCall instanceof Expr\StaticCall - && $functionCall->class instanceof Name - && $functionCall->name instanceof Node\Identifier - ) { - $className = $scope->resolveName($functionCall->class); - if ($this->broker->hasClass($className)) { - $classReflection = $this->broker->getClass($className); - if ($classReflection->hasMethod($functionCall->name->name)) { - return ParametersAcceptorSelector::selectFromArgs( - $scope, - $functionCall->args, - $classReflection->getMethod($functionCall->name->name, $scope)->getVariants() - ); - } - } + $itemType = count($itemTypes) > 0 ? TypeCombinator::union(...$itemTypes) : new MixedType(); + $scope = $this->lookForArrayDestructuringArray($scope, $stmt->valueVar, $itemType); } - return null; + return $scope; } private function processTraitUse(Node\Stmt\TraitUse $node, Scope $classScope, \Closure $nodeCallback): void @@ -1918,61 +1965,47 @@ class NodeScopeResolver $traitReflection = $this->broker->getClass($traitName); $traitFileName = $traitReflection->getFileName(); if ($traitFileName === false) { - throw new \PHPStan\ShouldNotHappenException(); + continue; // trait from eval or from PHP itself } $fileName = $this->fileHelper->normalizePath($traitFileName); if (!isset($this->analysedFiles[$fileName])) { - return; + continue; } $parserNodes = $this->parser->parseFile($fileName); - $classScope = $classScope->enterTrait($traitReflection); - - $this->processNodesForTraitUse($parserNodes, $traitName, $classScope, $nodeCallback); + $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $nodeCallback); } } /** * @param \PhpParser\Node[]|\PhpParser\Node|scalar $node - * @param string $traitName - * @param \PHPStan\Analyser\Scope $classScope + * @param ClassReflection $traitReflection + * @param \PHPStan\Analyser\Scope $scope * @param \Closure(\PhpParser\Node $node): void $nodeCallback */ - private function processNodesForTraitUse($node, string $traitName, Scope $classScope, \Closure $nodeCallback): void + private function processNodesForTraitUse($node, ClassReflection $traitReflection, Scope $scope, \Closure $nodeCallback): void { if ($node instanceof Node) { - if ($node instanceof Node\Stmt\Trait_ && $traitName === (string) $node->namespacedName) { - $this->processNodes($node->stmts, $classScope->enterFirstLevelStatements(), $nodeCallback); + if ($node instanceof Node\Stmt\Trait_ && $traitReflection->getName() === (string) $node->namespacedName) { + $this->processStmtNodes($node->stmts, $scope->enterTrait($traitReflection), $nodeCallback); return; } if ($node instanceof Node\Stmt\ClassLike) { return; } + if ($node instanceof Node\FunctionLike) { + return; + } foreach ($node->getSubNodeNames() as $subNodeName) { $subNode = $node->{$subNodeName}; - $this->processNodesForTraitUse($subNode, $traitName, $classScope, $nodeCallback); + $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $nodeCallback); } } elseif (is_array($node)) { foreach ($node as $subNode) { - $this->processNodesForTraitUse($subNode, $traitName, $classScope, $nodeCallback); + $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $nodeCallback); } } } - private function enterClassMethod(Scope $scope, Node\Stmt\ClassMethod $classMethod): Scope - { - [$phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $isDeprecated, $isInternal, $isFinal] = $this->getPhpDocs($scope, $classMethod); - - return $scope->enterClassMethod( - $classMethod, - $phpDocParameterTypes, - $phpDocReturnType, - $phpDocThrowType, - $isDeprecated, - $isInternal, - $isFinal - ); - } - /** * @param Scope $scope * @param Node\FunctionLike $functionLike @@ -2046,19 +2079,4 @@ class NodeScopeResolver return [$phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $isDeprecated, $isInternal, $isFinal]; } - private function enterFunction(Scope $scope, Node\Stmt\Function_ $function): Scope - { - [$phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $isDeprecated, $isInternal, $isFinal] = $this->getPhpDocs($scope, $function); - - return $scope->enterFunction( - $function, - $phpDocParameterTypes, - $phpDocReturnType, - $phpDocThrowType, - $isDeprecated, - $isInternal, - $isFinal - ); - } - } diff --git a/vendor/phpstan/phpstan/src/Analyser/Scope.php b/vendor/phpstan/phpstan/src/Analyser/Scope.php index 9ca404cf..b583acb1 100644 --- a/vendor/phpstan/phpstan/src/Analyser/Scope.php +++ b/vendor/phpstan/phpstan/src/Analyser/Scope.php @@ -39,6 +39,7 @@ use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; use PHPStan\Type\ClosureType; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; @@ -119,7 +120,7 @@ class Scope implements ClassMemberAccessAnswerer /** @var bool */ private $inFirstLevelStatement; - /** @var string[] */ + /** @var array */ private $currentlyAssignedExpressions = []; /** @var string[] */ @@ -141,7 +142,7 @@ class Scope implements ClassMemberAccessAnswerer * @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|null $inFunctionCall * @param bool $negated * @param bool $inFirstLevelStatement - * @param string[] $currentlyAssignedExpressions + * @param array $currentlyAssignedExpressions * @param string[] $dynamicConstantNames */ public function __construct( @@ -356,6 +357,10 @@ class Scope implements ClassMemberAccessAnswerer private function resolveType(Expr $node): Type { + if ($node instanceof Expr\Exit_) { + return new NeverType(); + } + if ( $node instanceof Expr\BinaryOp\Greater || $node instanceof Expr\BinaryOp\GreaterOrEqual @@ -860,22 +865,31 @@ class Scope implements ClassMemberAccessAnswerer return TypeCombinator::union(...$resultTypes); } + $arrayType = new ArrayType(new MixedType(), new MixedType()); - $leftArrays = TypeUtils::getArrays($leftType); - $rightArrays = TypeUtils::getArrays($rightType); + if ($arrayType->isSuperTypeOf($leftType)->yes() && $arrayType->isSuperTypeOf($rightType)->yes()) { + if ($leftType->getIterableKeyType()->equals($rightType->getIterableKeyType())) { + // to preserve BenevolentUnionType + $keyType = $leftType->getIterableKeyType(); + } else { + $keyTypes = []; + foreach ([ + $leftType->getIterableKeyType(), + $rightType->getIterableKeyType(), + ] as $keyType) { + if ($keyType instanceof BenevolentUnionType) { + $keyTypes[] = new MixedType(); + continue; + } - if (count($leftArrays) > 0 && count($rightArrays) > 0) { - $resultTypes = []; - foreach ($rightArrays as $rightArray) { - foreach ($leftArrays as $leftArray) { - $resultTypes[] = new ArrayType( - TypeCombinator::union($leftArray->getKeyType(), $rightArray->getKeyType()), - TypeCombinator::union($leftArray->getItemType(), $rightArray->getItemType()) - ); + $keyTypes[] = $keyType; } + $keyType = TypeCombinator::union(...$keyTypes); } - - return TypeCombinator::union(...$resultTypes); + return new ArrayType( + $keyType, + TypeCombinator::union($leftType->getIterableValueType(), $rightType->getIterableValueType()) + ); } if ($leftType instanceof MixedType && $rightType instanceof MixedType) { @@ -1025,7 +1039,7 @@ class Scope implements ClassMemberAccessAnswerer return new ObjectType((string) $node->class); } if ($node->class instanceof Node\Stmt\Class_) { - $anonymousClassReflection = $this->broker->getAnonymousClassReflection($node, $this); + $anonymousClassReflection = $this->broker->getAnonymousClassReflection($node->class, $this); return new ObjectType($anonymousClassReflection->getName()); } @@ -1128,14 +1142,21 @@ class Scope implements ClassMemberAccessAnswerer return $this->getType($node->var); } elseif ($node instanceof Expr\PreInc || $node instanceof Expr\PreDec) { $varType = $this->getType($node->var); - if ($varType instanceof ConstantScalarType) { - $varValue = $varType->getValue(); - if ($node instanceof Expr\PreInc) { - ++$varValue; - } else { - --$varValue; + $varScalars = TypeUtils::getConstantScalars($varType); + if (count($varScalars) > 0) { + $newTypes = []; + + foreach ($varScalars as $scalar) { + $varValue = $scalar->getValue(); + if ($node instanceof Expr\PreInc) { + ++$varValue; + } else { + --$varValue; + } + + $newTypes[] = $this->getTypeFromValue($varValue); } - return $this->getTypeFromValue($varValue); + return TypeCombinator::union(...$newTypes); } $stringType = new StringType(); @@ -1147,7 +1168,7 @@ class Scope implements ClassMemberAccessAnswerer } $exprString = $this->printer->prettyPrintExpr($node); - if (isset($this->moreSpecificTypes[$exprString])) { + if (isset($this->moreSpecificTypes[$exprString]) && $this->moreSpecificTypes[$exprString]->getCertainty()->yes()) { return $this->moreSpecificTypes[$exprString]->getType(); } @@ -1714,7 +1735,8 @@ class Scope implements ClassMemberAccessAnswerer { $exprString = $this->printer->prettyPrintExpr($node); - return isset($this->moreSpecificTypes[$exprString]); + return isset($this->moreSpecificTypes[$exprString]) + && $this->moreSpecificTypes[$exprString]->getCertainty()->yes(); } public function enterClass(ClassReflection $classReflection): self @@ -1903,6 +1925,29 @@ class Scope implements ClassMemberAccessAnswerer ); } + public function restoreOriginalScopeAfterClosureBind(self $originalScope): self + { + $variableTypes = $this->getVariableTypes(); + if (isset($originalScope->variableTypes['this'])) { + $variableTypes['this'] = $originalScope->variableTypes['this']; + } else { + unset($variableTypes['this']); + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $variableTypes, + $this->moreSpecificTypes, + $originalScope->inClosureBindScopeClass, + $this->getAnonymousFunctionReturnType(), + $this->getInFunctionCall(), + $this->isNegated() + ); + } + public function enterClosureCall(Type $thisType): self { $variableTypes = $this->getVariableTypes(); @@ -1927,11 +1972,6 @@ class Scope implements ClassMemberAccessAnswerer return $this->inClosureBindScopeClass !== null; } - public function enterAnonymousClass(ClassReflection $anonymousClass): self - { - return $this->enterClass($anonymousClass); - } - public function enterAnonymousFunction( Expr\Closure $closure ): self @@ -1952,22 +1992,18 @@ class Scope implements ClassMemberAccessAnswerer if (!is_string($use->var->name)) { throw new \PHPStan\ShouldNotHappenException(); } - if ($this->hasVariableType($use->var->name)->no()) { - if ($use->byRef) { - if ($this->isInExpressionAssign(new Variable($use->var->name))) { - $variableTypes[$use->var->name] = VariableTypeHolder::createYes( - $this->getType($closure) - ); - continue; - } - $variableTypes[$use->var->name] = VariableTypeHolder::createYes(new NullType()); - } + if ($use->byRef) { continue; } - $variableTypes[$use->var->name] = VariableTypeHolder::createYes($this->getVariableType($use->var->name)); + if ($this->hasVariableType($use->var->name)->no()) { + $variableType = new ErrorType(); + } else { + $variableType = $this->getVariableType($use->var->name); + } + $variableTypes[$use->var->name] = VariableTypeHolder::createYes($variableType); } - if ($this->hasVariableType('this')->yes()) { + if ($this->hasVariableType('this')->yes() && !$closure->static) { $variableTypes['this'] = VariableTypeHolder::createYes($this->getVariableType('this')); } @@ -2063,10 +2099,10 @@ class Scope implements ClassMemberAccessAnswerer public function enterForeach(Expr $iteratee, string $valueName, ?string $keyName): self { $iterateeType = $this->getType($iteratee); - $scope = $this->assignVariable($valueName, $iterateeType->getIterableValueType(), TrinaryLogic::createYes()); + $scope = $this->assignVariable($valueName, $iterateeType->getIterableValueType()); if ($keyName !== null) { - $scope = $scope->assignVariable($keyName, $iterateeType->getIterableKeyType(), TrinaryLogic::createYes()); + $scope = $scope->assignVariable($keyName, $iterateeType->getIterableKeyType()); } return $scope; @@ -2085,8 +2121,7 @@ class Scope implements ClassMemberAccessAnswerer return $this->assignVariable( $variableName, - TypeCombinator::intersect($type, new ObjectType(\Throwable::class)), - TrinaryLogic::createYes() + TypeCombinator::intersect($type, new ObjectType(\Throwable::class)) ); } @@ -2113,8 +2148,31 @@ class Scope implements ClassMemberAccessAnswerer public function enterExpressionAssign(Expr $expr): self { + $exprString = $this->printer->prettyPrintExpr($expr); $currentlyAssignedExpressions = $this->currentlyAssignedExpressions; - $currentlyAssignedExpressions[] = $this->printer->prettyPrintExpr($expr); + $currentlyAssignedExpressions[$exprString] = true; + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $this->getVariableTypes(), + $this->moreSpecificTypes, + $this->inClosureBindScopeClass, + $this->getAnonymousFunctionReturnType(), + $this->getInFunctionCall(), + $this->isNegated(), + $this->isInFirstLevelStatement(), + $currentlyAssignedExpressions + ); + } + + public function exitExpressionAssign(Expr $expr): self + { + $exprString = $this->printer->prettyPrintExpr($expr); + $currentlyAssignedExpressions = $this->currentlyAssignedExpressions; + unset($currentlyAssignedExpressions[$exprString]); return $this->scopeFactory->create( $this->context, @@ -2135,26 +2193,13 @@ class Scope implements ClassMemberAccessAnswerer public function isInExpressionAssign(Expr $expr): bool { $exprString = $this->printer->prettyPrintExpr($expr); - return in_array($exprString, $this->currentlyAssignedExpressions, true); + return array_key_exists($exprString, $this->currentlyAssignedExpressions); } - public function assignVariable( - string $variableName, - Type $type, - TrinaryLogic $certainty - ): self + public function assignVariable(string $variableName, Type $type): self { - if ($certainty->no()) { - throw new \PHPStan\ShouldNotHappenException(); - } - - $existingCertainty = $this->hasVariableType($variableName); - if (!$existingCertainty->no()) { - $certainty = $certainty->or($existingCertainty); - } - $variableTypes = $this->getVariableTypes(); - $variableTypes[$variableName] = new VariableTypeHolder($type, $certainty); + $variableTypes[$variableName] = new VariableTypeHolder($type, TrinaryLogic::createYes()); $variableString = $this->printer->prettyPrintExpr(new Variable($variableName)); $moreSpecificTypeHolders = $this->moreSpecificTypes; @@ -2227,272 +2272,6 @@ class Scope implements ClassMemberAccessAnswerer return $this; } - public function intersectVariables(Scope $otherScope): self - { - $ourVariableTypeHolders = $this->getVariableTypes(); - $theirVariableTypeHolders = $otherScope->getVariableTypes(); - $intersectedVariableTypeHolders = []; - foreach ($theirVariableTypeHolders as $name => $variableTypeHolder) { - if (isset($ourVariableTypeHolders[$name])) { - $intersectedVariableTypeHolders[$name] = $ourVariableTypeHolders[$name]->and($variableTypeHolder); - } else { - $intersectedVariableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); - } - } - - foreach ($ourVariableTypeHolders as $name => $variableTypeHolder) { - $variableNode = new Variable($name); - if ($otherScope->isSpecified($variableNode)) { - $intersectedVariableTypeHolders[$name] = VariableTypeHolder::createYes( - TypeCombinator::union( - $otherScope->getType($variableNode), - $variableTypeHolder->getType() - ) - ); - continue; - } - if (isset($theirVariableTypeHolders[$name])) { - continue; - } - - $intersectedVariableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); - } - - $ourSpecifiedTypeHolders = $this->moreSpecificTypes; - $theirSpecifiedTypeHolders = $otherScope->moreSpecificTypes; - $intersectedSpecifiedTypes = []; - - foreach ($theirSpecifiedTypeHolders as $exprString => $theirSpecifiedTypeHolder) { - $matches = \Nette\Utils\Strings::match((string) $exprString, '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)$#'); - if ($matches !== null) { - continue; - } - if (isset($ourSpecifiedTypeHolders[$exprString])) { - $intersectedSpecifiedTypes[$exprString] = $ourSpecifiedTypeHolders[$exprString]->and($theirSpecifiedTypeHolder); - } else { - $intersectedSpecifiedTypes[$exprString] = VariableTypeHolder::createMaybe($theirSpecifiedTypeHolder->getType()); - } - } - - foreach ($this->moreSpecificTypes as $exprString => $specificTypeHolder) { - $matches = \Nette\Utils\Strings::match((string) $exprString, '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)$#'); - if ($matches !== null) { - continue; - } - if (isset($otherScope->moreSpecificTypes[$exprString])) { - $intersectedSpecifiedTypes[$exprString] = VariableTypeHolder::createYes( - TypeCombinator::union( - $otherScope->moreSpecificTypes[$exprString]->getType(), - $specificTypeHolder->getType() - ) - ); - continue; - } - if (isset($theirVariableTypeHolders[$exprString])) { - continue; - } - - $intersectedSpecifiedTypes[$exprString] = VariableTypeHolder::createMaybe($specificTypeHolder->getType()); - } - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), - $intersectedVariableTypeHolders, - $intersectedSpecifiedTypes, - $this->inClosureBindScopeClass, - $this->getAnonymousFunctionReturnType(), - $this->getInFunctionCall(), - $this->isNegated(), - $this->inFirstLevelStatement - ); - } - - public function createIntersectedScope(self $otherScope): self - { - $variableTypes = []; - foreach ($otherScope->getVariableTypes() as $name => $variableTypeHolder) { - $variableTypes[$name] = $variableTypeHolder; - } - - $specifiedTypes = []; - foreach ($otherScope->moreSpecificTypes as $exprString => $specificTypeHolder) { - $matches = \Nette\Utils\Strings::match((string) $exprString, '#^\$([a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*)$#'); - if ($matches !== null) { - $variableTypes[$matches[1]] = $specificTypeHolder; - continue; - } - $specifiedTypes[$exprString] = $specificTypeHolder; - } - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), - $variableTypes, - $specifiedTypes, - $this->inClosureBindScopeClass, - $this->getAnonymousFunctionReturnType(), - $this->getInFunctionCall(), - $this->isNegated(), - $this->inFirstLevelStatement - ); - } - - public function mergeWithIntersectedScope(self $intersectedScope): self - { - $variableTypeHolders = $this->variableTypes; - $specifiedTypeHolders = $this->moreSpecificTypes; - foreach ($intersectedScope->getVariableTypes() as $name => $theirVariableTypeHolder) { - if (isset($variableTypeHolders[$name])) { - $type = $theirVariableTypeHolder->getType(); - if ($theirVariableTypeHolder->getCertainty()->maybe()) { - $type = TypeCombinator::union($type, $variableTypeHolders[$name]->getType()); - } - $theirVariableTypeHolder = new VariableTypeHolder( - $type, - $theirVariableTypeHolder->getCertainty()->or($variableTypeHolders[$name]->getCertainty()) - ); - } - - $variableTypeHolders[$name] = $theirVariableTypeHolder; - - $exprString = $this->printer->prettyPrintExpr(new Variable($name)); - unset($specifiedTypeHolders[$exprString]); - } - - foreach ($intersectedScope->moreSpecificTypes as $exprString => $theirTypeHolder) { - if (isset($specifiedTypeHolders[$exprString])) { - $type = $theirTypeHolder->getType(); - if ($theirTypeHolder->getCertainty()->maybe()) { - $type = TypeCombinator::union($type, $specifiedTypeHolders[$exprString]->getType()); - } - $theirTypeHolder = new VariableTypeHolder( - $type, - $theirTypeHolder->getCertainty()->or($specifiedTypeHolders[$exprString]->getCertainty()) - ); - } - - if (!$theirTypeHolder->getCertainty()->yes()) { - continue; - } - - $specifiedTypeHolders[$exprString] = $theirTypeHolder; - } - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), - $variableTypeHolders, - $specifiedTypeHolders, - $this->inClosureBindScopeClass, - $this->getAnonymousFunctionReturnType(), - $this->getInFunctionCall(), - $this->isNegated(), - $this->inFirstLevelStatement - ); - } - - public function removeSpecified(self $initialScope): self - { - $variableTypeHolders = $this->variableTypes; - foreach ($variableTypeHolders as $name => $holder) { - if (!$holder->getCertainty()->yes()) { - continue; - } - $node = new Variable($name); - if ($this->isSpecified($node) && !$initialScope->hasVariableType($name)->no()) { - $variableTypeHolders[$name] = VariableTypeHolder::createYes(TypeCombinator::remove($initialScope->getVariableType($name), $this->getType($node))); - continue; - } - } - - $moreSpecificTypeHolders = $this->moreSpecificTypes; - foreach (array_keys($moreSpecificTypeHolders) as $exprString) { - if (isset($initialScope->moreSpecificTypes[$exprString])) { - continue; - } - - unset($moreSpecificTypeHolders[$exprString]); - } - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), - $variableTypeHolders, - $moreSpecificTypeHolders, - $this->inClosureBindScopeClass, - $this->getAnonymousFunctionReturnType(), - $this->getInFunctionCall(), - $this->isNegated(), - $this->inFirstLevelStatement - ); - } - - public function removeVariables(self $otherScope, bool $all): self - { - $ourVariableTypeHolders = $this->getVariableTypes(); - foreach ($otherScope->getVariableTypes() as $name => $theirVariableTypeHolder) { - if ($all) { - if ( - isset($ourVariableTypeHolders[$name]) - && $ourVariableTypeHolders[$name]->getCertainty()->equals($theirVariableTypeHolder->getCertainty()) - ) { - unset($ourVariableTypeHolders[$name]); - } - } else { - if ( - isset($ourVariableTypeHolders[$name]) - && $theirVariableTypeHolder->getType()->equals($ourVariableTypeHolders[$name]->getType()) - && $ourVariableTypeHolders[$name]->getCertainty()->equals($theirVariableTypeHolder->getCertainty()) - ) { - unset($ourVariableTypeHolders[$name]); - } - } - } - - $ourTypeHolders = $this->moreSpecificTypes; - foreach ($otherScope->moreSpecificTypes as $exprString => $theirTypeHolder) { - if ($all) { - if ( - isset($ourTypeHolders[$exprString]) - && $ourTypeHolders[$exprString]->getCertainty()->equals($theirTypeHolder->getCertainty()) - ) { - unset($ourVariableTypeHolders[$exprString]); - } - } else { - if ( - isset($ourTypeHolders[$exprString]) - && $theirTypeHolder->getType()->equals($ourTypeHolders[$exprString]->getType()) - && $ourTypeHolders[$exprString]->getCertainty()->equals($theirTypeHolder->getCertainty()) - ) { - unset($ourVariableTypeHolders[$exprString]); - } - } - } - - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), - $ourVariableTypeHolders, - $ourTypeHolders, - $this->inClosureBindScopeClass, - $this->getAnonymousFunctionReturnType(), - $this->getInFunctionCall(), - $this->isNegated(), - $this->inFirstLevelStatement - ); - } - public function specifyExpressionType(Expr $expr, Type $type): self { if ($expr instanceof Node\Scalar || $expr instanceof Array_) { @@ -2509,21 +2288,19 @@ class Scope implements ClassMemberAccessAnswerer $variableTypes = $this->getVariableTypes(); $variableTypes[$variableName] = VariableTypeHolder::createYes($type); - $moreSpecificTypes = $this->moreSpecificTypes; - $moreSpecificTypes[$exprString] = $variableTypes[$variableName]; - return $this->scopeFactory->create( $this->context, $this->isDeclareStrictTypes(), $this->getFunction(), $this->getNamespace(), $variableTypes, - $moreSpecificTypes, + $this->moreSpecificTypes, $this->inClosureBindScopeClass, $this->getAnonymousFunctionReturnType(), $this->getInFunctionCall(), $this->isNegated(), - $this->inFirstLevelStatement + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions ); } elseif ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { $constantArrays = TypeUtils::getConstantArrays($this->getType($expr->var)); @@ -2549,7 +2326,7 @@ class Scope implements ClassMemberAccessAnswerer { $exprString = $this->printer->prettyPrintExpr($expr); $moreSpecificTypeHolders = $this->moreSpecificTypes; - if (isset($moreSpecificTypeHolders[$exprString]) && !$moreSpecificTypeHolders[$exprString]->getType() instanceof MixedType) { + if (isset($moreSpecificTypeHolders[$exprString])) { unset($moreSpecificTypeHolders[$exprString]); return $this->scopeFactory->create( $this->context, @@ -2633,15 +2410,6 @@ class Scope implements ClassMemberAccessAnswerer return $scope; } - public function specifyFetchedStaticPropertyFromIsset(Expr\StaticPropertyFetch $expr): self - { - $exprString = $this->printer->prettyPrintExpr($expr); - - return $this->addMoreSpecificTypes([ - $exprString => new MixedType(), - ]); - } - public function enterNegation(): self { return $this->scopeFactory->create( @@ -2659,24 +2427,6 @@ class Scope implements ClassMemberAccessAnswerer ); } - public function enterFirstLevelStatements(): self - { - return $this->scopeFactory->create( - $this->context, - $this->isDeclareStrictTypes(), - $this->getFunction(), - $this->getNamespace(), - $this->getVariableTypes(), - $this->moreSpecificTypes, - $this->inClosureBindScopeClass, - $this->getAnonymousFunctionReturnType(), - $this->getInFunctionCall(), - $this->isNegated(), - true, - $this->currentlyAssignedExpressions - ); - } - public function exitFirstLevelStatements(): self { return $this->scopeFactory->create( @@ -2706,6 +2456,7 @@ class Scope implements ClassMemberAccessAnswerer } /** + * @phpcsSuppress SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod * @param Type[] $types * @return self */ @@ -2727,10 +2478,457 @@ class Scope implements ClassMemberAccessAnswerer $this->getAnonymousFunctionReturnType(), $this->getInFunctionCall(), $this->isNegated(), + $this->inFirstLevelStatement, + $this->currentlyAssignedExpressions + ); + } + + public function mergeWith(?self $otherScope): self + { + if ($otherScope === null) { + return $this; + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $this->mergeVariableHolders($this->getVariableTypes(), $otherScope->getVariableTypes()), + $this->mergeVariableHolders($this->moreSpecificTypes, $otherScope->moreSpecificTypes), + $this->inClosureBindScopeClass, + $this->getAnonymousFunctionReturnType(), + $this->getInFunctionCall(), + $this->isNegated(), $this->inFirstLevelStatement ); } + /** + * @param VariableTypeHolder[] $ourVariableTypeHolders + * @param VariableTypeHolder[] $theirVariableTypeHolders + * @return VariableTypeHolder[] + */ + private function mergeVariableHolders(array $ourVariableTypeHolders, array $theirVariableTypeHolders): array + { + $intersectedVariableTypeHolders = []; + foreach ($ourVariableTypeHolders as $name => $variableTypeHolder) { + if (isset($theirVariableTypeHolders[$name])) { + $intersectedVariableTypeHolders[$name] = $variableTypeHolder->and($theirVariableTypeHolders[$name]); + } else { + $intersectedVariableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + } + } + + foreach ($theirVariableTypeHolders as $name => $variableTypeHolder) { + if (isset($intersectedVariableTypeHolders[$name])) { + continue; + } + + $intersectedVariableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + } + + return $intersectedVariableTypeHolders; + } + + public function processFinallyScope(self $finallyScope, self $originalFinallyScope): self + { + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $this->processFinallyScopeVariableTypeHolders( + $this->getVariableTypes(), + $finallyScope->getVariableTypes(), + $originalFinallyScope->getVariableTypes() + ), + $this->processFinallyScopeVariableTypeHolders( + $this->moreSpecificTypes, + $finallyScope->moreSpecificTypes, + $originalFinallyScope->moreSpecificTypes + ), + $this->inClosureBindScopeClass, + $this->getAnonymousFunctionReturnType(), + $this->getInFunctionCall(), + $this->isNegated(), + $this->inFirstLevelStatement + ); + } + + /** + * @param VariableTypeHolder[] $ourVariableTypeHolders + * @param VariableTypeHolder[] $finallyVariableTypeHolders + * @param VariableTypeHolder[] $originalVariableTypeHolders + * @return VariableTypeHolder[] + */ + private function processFinallyScopeVariableTypeHolders( + array $ourVariableTypeHolders, + array $finallyVariableTypeHolders, + array $originalVariableTypeHolders + ): array + { + foreach ($finallyVariableTypeHolders as $name => $variableTypeHolder) { + if ( + isset($originalVariableTypeHolders[$name]) + && !$originalVariableTypeHolders[$name]->getType()->equals($variableTypeHolder->getType()) + ) { + $ourVariableTypeHolders[$name] = $variableTypeHolder; + continue; + } + + if (isset($originalVariableTypeHolders[$name])) { + continue; + } + + $ourVariableTypeHolders[$name] = $variableTypeHolder; + } + + return $ourVariableTypeHolders; + } + + /** + * @param self $closureScope + * @param self|null $prevScope + * @param Expr\ClosureUse[] $byRefUses + * @return self + */ + public function processClosureScope( + self $closureScope, + ?self $prevScope, + array $byRefUses + ): self + { + $variableTypes = $this->variableTypes; + foreach ($byRefUses as $use) { + if (!is_string($use->var->name)) { + throw new \PHPStan\ShouldNotHappenException(); + } + + $variableName = $use->var->name; + + if (!$closureScope->hasVariableType($variableName)->yes()) { + $variableTypes[$variableName] = VariableTypeHolder::createYes(new NullType()); + continue; + } + + $variableType = $closureScope->getVariableType($variableName); + + if ($prevScope !== null) { + $prevVariableType = $prevScope->getVariableType($variableName); + if (!$variableType->equals($prevVariableType)) { + $variableType = TypeCombinator::union($variableType, $prevVariableType); + $variableType = self::generalizeType($variableType, $prevVariableType); + } + } + + $variableTypes[$variableName] = VariableTypeHolder::createYes($variableType); + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $variableTypes, + [], + $this->inClosureBindScopeClass, + $this->getAnonymousFunctionReturnType(), + $this->getInFunctionCall(), + $this->isNegated(), + $this->inFirstLevelStatement + ); + } + + public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope): self + { + $variableTypeHolders = $this->variableTypes; + foreach ($finalScope->variableTypes as $name => $variableTypeHolder) { + if (!isset($variableTypeHolders[$name])) { + $variableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + continue; + } + + $variableTypeHolders[$name] = new VariableTypeHolder( + $variableTypeHolder->getType(), + $variableTypeHolder->getCertainty()->and($variableTypeHolders[$name]->getCertainty()) + ); + } + + $moreSpecificTypes = $this->moreSpecificTypes; + foreach ($finalScope->moreSpecificTypes as $exprString => $variableTypeHolder) { + if (!isset($moreSpecificTypes[$exprString])) { + $moreSpecificTypes[$exprString] = VariableTypeHolder::createMaybe($variableTypeHolder->getType()); + continue; + } + + $moreSpecificTypes[$exprString] = new VariableTypeHolder( + $variableTypeHolder->getType(), + $variableTypeHolder->getCertainty()->and($moreSpecificTypes[$exprString]->getCertainty()) + ); + } + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $variableTypeHolders, + $moreSpecificTypes, + $this->inClosureBindScopeClass, + $this->getAnonymousFunctionReturnType(), + $this->getInFunctionCall(), + $this->isNegated(), + $this->inFirstLevelStatement + ); + } + + public function generalizeWith(self $otherScope): self + { + $variableTypeHolders = $this->generalizeVariableTypeHolders( + $this->getVariableTypes(), + $otherScope->getVariableTypes() + ); + + $moreSpecificTypes = $this->generalizeVariableTypeHolders( + $this->moreSpecificTypes, + $otherScope->moreSpecificTypes + ); + + return $this->scopeFactory->create( + $this->context, + $this->isDeclareStrictTypes(), + $this->getFunction(), + $this->getNamespace(), + $variableTypeHolders, + $moreSpecificTypes, + $this->inClosureBindScopeClass, + $this->getAnonymousFunctionReturnType(), + $this->getInFunctionCall(), + $this->isNegated(), + $this->inFirstLevelStatement + ); + } + + /** + * @param VariableTypeHolder[] $variableTypeHolders + * @param VariableTypeHolder[] $otherVariableTypeHolders + * @return VariableTypeHolder[] + */ + private function generalizeVariableTypeHolders( + array $variableTypeHolders, + array $otherVariableTypeHolders + ): array + { + foreach ($variableTypeHolders as $name => $variableTypeHolder) { + if (!isset($otherVariableTypeHolders[$name])) { + continue; + } + + $variableTypeHolders[$name] = new VariableTypeHolder( + self::generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$name]->getType()), + $variableTypeHolder->getCertainty() + ); + } + + return $variableTypeHolders; + } + + private function generalizeType(Type $a, Type $b): Type + { + if ($a->equals($b)) { + return $a; + } + + $constantIntegers = ['a' => [], 'b' => []]; + $constantFloats = ['a' => [], 'b' => []]; + $constantBooleans = ['a' => [], 'b' => []]; + $constantStrings = ['a' => [], 'b' => []]; + $constantArrays = ['a' => [], 'b' => []]; + $generalArrays = ['a' => [], 'b' => []]; + $otherTypes = []; + + foreach ([ + 'a' => TypeUtils::flattenTypes($a), + 'b' => TypeUtils::flattenTypes($b), + ] as $key => $types) { + foreach ($types as $type) { + if ($type instanceof ConstantIntegerType) { + $constantIntegers[$key][] = $type; + continue; + } + if ($type instanceof ConstantFloatType) { + $constantFloats[$key][] = $type; + continue; + } + if ($type instanceof ConstantBooleanType) { + $constantBooleans[$key][] = $type; + continue; + } + if ($type instanceof ConstantStringType) { + $constantStrings[$key][] = $type; + continue; + } + if ($type instanceof ConstantArrayType) { + $constantArrays[$key][] = $type; + continue; + } + if ($type instanceof ArrayType) { + $generalArrays[$key][] = $type; + continue; + } + + $otherTypes[] = $type; + } + } + + $resultTypes = []; + foreach ([ + $constantIntegers, + $constantFloats, + $constantBooleans, + $constantStrings, + ] as $constantTypes) { + if (count($constantTypes['a']) === 0) { + continue; + } + if (count($constantTypes['b']) === 0) { + $resultTypes[] = TypeCombinator::union(...$constantTypes['a']); + continue; + } + + $aTypes = TypeCombinator::union(...$constantTypes['a']); + $bTypes = TypeCombinator::union(...$constantTypes['b']); + if ($aTypes->equals($bTypes)) { + $resultTypes[] = $aTypes; + continue; + } + + $resultTypes[] = TypeUtils::generalizeType($constantTypes['a'][0]); + } + + if (count($constantArrays['a']) > 0) { + if (count($constantArrays['b']) === 0) { + $resultTypes[] = TypeCombinator::union(...$constantArrays['a']); + } else { + $constantArraysA = TypeCombinator::union(...$constantArrays['a']); + $constantArraysB = TypeCombinator::union(...$constantArrays['b']); + if ($constantArraysA->getIterableKeyType()->equals($constantArraysB->getIterableKeyType())) { + $resultArrayBuilder = ConstantArrayTypeBuilder::createEmpty(); + foreach (TypeUtils::flattenTypes($constantArraysA->getIterableKeyType()) as $keyType) { + $resultArrayBuilder->setOffsetValueType( + $keyType, + self::generalizeType( + $constantArraysA->getOffsetValueType($keyType), + $constantArraysB->getOffsetValueType($keyType) + ) + ); + } + + $resultTypes[] = $resultArrayBuilder->getArray(); + } else { + $resultTypes[] = new ArrayType( + TypeCombinator::union(self::generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType())), + TypeCombinator::union(self::generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType())) + ); + } + } + } + + if (count($generalArrays['a']) > 0) { + if (count($generalArrays['b']) === 0) { + $resultTypes[] = TypeCombinator::union(...$generalArrays['a']); + } else { + $generalArraysA = TypeCombinator::union(...$generalArrays['a']); + $generalArraysB = TypeCombinator::union(...$generalArrays['b']); + + $aValueType = $generalArraysA->getIterableValueType(); + $bValueType = $generalArraysB->getIterableValueType(); + $aArrays = TypeUtils::getAnyArrays($aValueType); + $bArrays = TypeUtils::getAnyArrays($bValueType); + if ( + count($aArrays) === 1 + && !$aArrays[0] instanceof ConstantArrayType + && count($bArrays) === 1 + && !$bArrays[0] instanceof ConstantArrayType + ) { + $aDepth = $this->getArrayDepth($aArrays[0]); + $bDepth = $this->getArrayDepth($bArrays[0]); + if ( + ($aDepth > 2 || $bDepth > 2) + && abs($aDepth - $bDepth) > 0 + ) { + $aValueType = new MixedType(); + $bValueType = new MixedType(); + } + } + + $resultTypes[] = new ArrayType( + TypeCombinator::union(self::generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType())), + TypeCombinator::union(self::generalizeType($aValueType, $bValueType)) + ); + } + } + + return TypeCombinator::union(...$resultTypes, ...$otherTypes); + } + + private function getArrayDepth(ArrayType $type): int + { + $depth = 0; + while ($type instanceof ArrayType) { + $temp = $type->getIterableValueType(); + $arrays = TypeUtils::getAnyArrays($temp); + if (count($arrays) === 1) { + $type = $arrays[0]; + } else { + $type = $temp; + } + $depth++; + } + + return $depth; + } + + public function equals(self $otherScope): bool + { + if (!$this->context->equals($otherScope->context)) { + return false; + } + + if (!$this->compareVariableTypeHolders($this->variableTypes, $otherScope->variableTypes)) { + return false; + } + + return $this->compareVariableTypeHolders($this->moreSpecificTypes, $otherScope->moreSpecificTypes); + } + + /** + * @param VariableTypeHolder[] $variableTypeHolders + * @param VariableTypeHolder[] $otherVariableTypeHolders + * @return bool + */ + private function compareVariableTypeHolders(array $variableTypeHolders, array $otherVariableTypeHolders): bool + { + foreach ($variableTypeHolders as $name => $variableTypeHolder) { + if (!isset($otherVariableTypeHolders[$name])) { + return false; + } + + if (!$variableTypeHolder->getCertainty()->equals($otherVariableTypeHolders[$name]->getCertainty())) { + return false; + } + + if (!$variableTypeHolder->getType()->equals($otherVariableTypeHolders[$name]->getType())) { + return false; + } + + unset($otherVariableTypeHolders[$name]); + } + + return count($otherVariableTypeHolders) === 0; + } + public function canAccessProperty(PropertyReflection $propertyReflection): bool { return $this->canAccessClassMember($propertyReflection); diff --git a/vendor/phpstan/phpstan/src/Analyser/ScopeContext.php b/vendor/phpstan/phpstan/src/Analyser/ScopeContext.php index c2d35ebd..a157e1af 100644 --- a/vendor/phpstan/phpstan/src/Analyser/ScopeContext.php +++ b/vendor/phpstan/phpstan/src/Analyser/ScopeContext.php @@ -60,6 +60,31 @@ class ScopeContext return new self($this->file, $this->classReflection, $traitReflection); } + public function equals(self $otherContext): bool + { + if ($this->file !== $otherContext->file) { + return false; + } + + if ($this->getClassReflection() === null) { + return $otherContext->getClassReflection() === null; + } elseif ($otherContext->getClassReflection() === null) { + return false; + } + + $isSameClass = $this->getClassReflection()->getName() === $otherContext->getClassReflection()->getName(); + + if ($this->getTraitReflection() === null) { + return $otherContext->getTraitReflection() === null && $isSameClass; + } elseif ($otherContext->getTraitReflection() === null) { + return false; + } + + $isSameTrait = $this->getTraitReflection()->getName() === $otherContext->getTraitReflection()->getName(); + + return $isSameClass && $isSameTrait; + } + public function getFile(): string { return $this->file; diff --git a/vendor/phpstan/phpstan/src/Analyser/ScopeFactory.php b/vendor/phpstan/phpstan/src/Analyser/ScopeFactory.php index 52826b0d..f743d838 100644 --- a/vendor/phpstan/phpstan/src/Analyser/ScopeFactory.php +++ b/vendor/phpstan/phpstan/src/Analyser/ScopeFactory.php @@ -51,7 +51,7 @@ class ScopeFactory * @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|null $inFunctionCall * @param bool $negated * @param bool $inFirstLevelStatement - * @param string[] $currentlyAssignedExpressions + * @param array $currentlyAssignedExpressions * * @return Scope */ diff --git a/vendor/phpstan/phpstan/src/Analyser/StatementExitPoint.php b/vendor/phpstan/phpstan/src/Analyser/StatementExitPoint.php new file mode 100644 index 00000000..9c982d65 --- /dev/null +++ b/vendor/phpstan/phpstan/src/Analyser/StatementExitPoint.php @@ -0,0 +1,32 @@ +statement = $statement; + $this->scope = $scope; + } + + public function getStatement(): Stmt + { + return $this->statement; + } + + public function getScope(): Scope + { + return $this->scope; + } + +} diff --git a/vendor/phpstan/phpstan/src/Analyser/StatementList.php b/vendor/phpstan/phpstan/src/Analyser/StatementList.php deleted file mode 100644 index 817f8481..00000000 --- a/vendor/phpstan/phpstan/src/Analyser/StatementList.php +++ /dev/null @@ -1,73 +0,0 @@ -scope = $scope; - $this->statements = $statements; - $this->filterByTruthyValue = $filterByTruthyValue; - $this->processScope = $processScope; - } - - public static function fromList(Scope $scope, self $list): self - { - return new self( - $scope, - $list->statements, - $list->filterByTruthyValue, - $list->processScope - ); - } - - public function getScope(): Scope - { - $scope = $this->scope; - if ($this->processScope !== null) { - $callback = $this->processScope; - $scope = $callback($scope); - } - - return $scope; - } - - /** - * @return \PhpParser\Node[] - */ - public function getStatements(): array - { - return $this->statements; - } - - public function shouldFilterByTruthyValue(): bool - { - return $this->filterByTruthyValue; - } - -} diff --git a/vendor/phpstan/phpstan/src/Analyser/StatementResult.php b/vendor/phpstan/phpstan/src/Analyser/StatementResult.php new file mode 100644 index 00000000..d74b8f55 --- /dev/null +++ b/vendor/phpstan/phpstan/src/Analyser/StatementResult.php @@ -0,0 +1,110 @@ +scope = $scope; + $this->alwaysTerminatingStatements = $alwaysTerminatingStatements; + $this->exitPoints = $exitPoints; + } + + public function getScope(): Scope + { + return $this->scope; + } + + /** + * @return Stmt[] + */ + public function getAlwaysTerminatingStatements(): array + { + return $this->alwaysTerminatingStatements; + } + + public function areAllAlwaysTerminatingStatementsLoopTerminationStatements(): bool + { + if (count($this->alwaysTerminatingStatements) === 0) { + return false; + } + + foreach ($this->alwaysTerminatingStatements as $statement) { + if ($statement instanceof Stmt\Break_) { + continue; + } + if ($statement instanceof Stmt\Continue_) { + continue; + } + + return false; + } + + return true; + } + + public function isAlwaysTerminating(): bool + { + return count($this->alwaysTerminatingStatements) > 0; + } + + public function filterOutLoopTerminationStatements(): self + { + foreach ($this->alwaysTerminatingStatements as $statement) { + if ($statement instanceof Stmt\Break_ || $statement instanceof Stmt\Continue_) { + return new self($this->scope, [], $this->exitPoints); + } + } + + return $this; + } + + /** + * @return StatementExitPoint[] + */ + public function getExitPoints(): array + { + return $this->exitPoints; + } + + /** + * @param string $stmtClass + * @return StatementExitPoint[] + */ + public function getExitPointsByType(string $stmtClass): array + { + $exitPoints = []; + foreach ($this->exitPoints as $exitPoint) { + if (!$exitPoint->getStatement() instanceof $stmtClass) { + continue; + } + + $exitPoints[] = $exitPoint; + } + + return $exitPoints; + } + +} diff --git a/vendor/phpstan/phpstan/src/Analyser/TypeSpecifier.php b/vendor/phpstan/phpstan/src/Analyser/TypeSpecifier.php index e7ab8317..b5d97627 100644 --- a/vendor/phpstan/phpstan/src/Analyser/TypeSpecifier.php +++ b/vendor/phpstan/phpstan/src/Analyser/TypeSpecifier.php @@ -20,13 +20,16 @@ use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Name; use PHPStan\Broker\Broker; use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; +use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NonexistentParentClassType; @@ -102,6 +105,10 @@ class TypeSpecifier ): SpecifiedTypes { if ($expr instanceof Instanceof_) { + $exprNode = $expr->expr; + if ($exprNode instanceof Expr\Assign) { + $exprNode = $exprNode->var; + } if ($expr->class instanceof Name) { $className = (string) $expr->class; $lowercasedClassName = strtolower($className); @@ -121,17 +128,20 @@ class TypeSpecifier } else { $type = new ObjectType($className); } - return $this->create($expr->expr, $type, $context); + return $this->create($exprNode, $type, $context); } if ($context->true()) { - return $this->create($expr->expr, new ObjectWithoutClassType(), $context); + return $this->create($exprNode, new ObjectWithoutClassType(), $context); } } elseif ($expr instanceof Node\Expr\BinaryOp\Identical) { $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); if ($expressions !== null) { /** @var Expr $exprNode */ $exprNode = $expressions[0]; + if ($exprNode instanceof Expr\Assign) { + $exprNode = $exprNode->var; + } /** @var \PHPStan\Type\ConstantScalarType $constantType */ $constantType = $expressions[1]; if ($constantType->getValue() === false) { @@ -250,6 +260,66 @@ class TypeSpecifier ); } } + + $leftType = $scope->getType($expr->left); + $leftBooleanType = $leftType->toBoolean(); + $rightType = $scope->getType($expr->right); + if ($leftBooleanType instanceof ConstantBooleanType && $rightType instanceof BooleanType) { + return $this->specifyTypesInCondition( + $scope, + new Expr\BinaryOp\Identical( + new ConstFetch(new Name($leftBooleanType->getValue() ? 'true' : 'false')), + $expr->right + ), + $context + ); + } + + $rightBooleanType = $rightType->toBoolean(); + if ($rightBooleanType instanceof ConstantBooleanType && $leftType instanceof BooleanType) { + return $this->specifyTypesInCondition( + $scope, + new Expr\BinaryOp\Identical( + $expr->left, + new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false')) + ), + $context + ); + } + + if ( + $expr->left instanceof FuncCall + && $expr->left->name instanceof Name + && strtolower($expr->left->name->toString()) === 'get_class' + && isset($expr->left->args[0]) + && $rightType instanceof ConstantStringType + ) { + return $this->specifyTypesInCondition( + $scope, + new Instanceof_( + $expr->left->args[0]->value, + new Name($rightType->getValue()) + ), + $context + ); + } + + if ( + $expr->right instanceof FuncCall + && $expr->right->name instanceof Name + && strtolower($expr->right->name->toString()) === 'get_class' + && isset($expr->right->args[0]) + && $leftType instanceof ConstantStringType + ) { + return $this->specifyTypesInCondition( + $scope, + new Instanceof_( + $expr->right->args[0]->value, + new Name($leftType->getValue()) + ), + $context + ); + } } elseif ($expr instanceof Node\Expr\BinaryOp\NotEqual) { return $this->specifyTypesInCondition( $scope, @@ -373,6 +443,10 @@ class TypeSpecifier } } + if (count($vars) === 0) { + throw new \PHPStan\ShouldNotHappenException(); + } + $types = null; foreach ($vars as $var) { if ($expr instanceof Expr\Isset_) { @@ -401,6 +475,26 @@ class TypeSpecifier TypeSpecifierContext::createFalse() ); } + + if ( + $var instanceof PropertyFetch + && $var->name instanceof Node\Identifier + ) { + $type = $type->unionWith($this->create($var->var, new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType($var->name->toString()), + ]), TypeSpecifierContext::createTruthy())); + } elseif ( + $var instanceof StaticPropertyFetch + && $var->class instanceof Expr + && $var->name instanceof Node\VarLikeIdentifier + ) { + $type = $type->unionWith($this->create($var->class, new IntersectionType([ + new ObjectWithoutClassType(), + new HasPropertyType($var->name->toString()), + ]), TypeSpecifierContext::createTruthy())); + } + if ($types === null) { $types = $type; } else { @@ -408,9 +502,6 @@ class TypeSpecifier } } - /** @var SpecifiedTypes $types */ - $types = $types; - if ( $expr instanceof Expr\Empty_ && (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($scope->getType($expr->expr))->yes()) { @@ -480,7 +571,7 @@ class TypeSpecifier public function create(Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes { - if ($expr instanceof New_) { + if ($expr instanceof New_ || $expr instanceof Instanceof_) { return new SpecifiedTypes(); } @@ -500,7 +591,7 @@ class TypeSpecifier /** * @return \PHPStan\Type\FunctionTypeSpecifyingExtension[] */ - public function getFunctionTypeSpecifyingExtensions(): array + private function getFunctionTypeSpecifyingExtensions(): array { return $this->functionTypeSpecifyingExtensions; } @@ -509,7 +600,7 @@ class TypeSpecifier * @param string $className * @return \PHPStan\Type\MethodTypeSpecifyingExtension[] */ - public function getMethodTypeSpecifyingExtensionsForClass(string $className): array + private function getMethodTypeSpecifyingExtensionsForClass(string $className): array { if ($this->methodTypeSpecifyingExtensionsByClass === null) { $byClass = []; @@ -526,7 +617,7 @@ class TypeSpecifier * @param string $className * @return \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] */ - public function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array + private function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array { if ($this->staticMethodTypeSpecifyingExtensionsByClass === null) { $byClass = []; diff --git a/vendor/phpstan/phpstan/src/Analyser/VariableTypeHolder.php b/vendor/phpstan/phpstan/src/Analyser/VariableTypeHolder.php index 3fe0869e..2229a5a2 100644 --- a/vendor/phpstan/phpstan/src/Analyser/VariableTypeHolder.php +++ b/vendor/phpstan/phpstan/src/Analyser/VariableTypeHolder.php @@ -36,8 +36,13 @@ class VariableTypeHolder public function and(self $other): self { + if ($this->getType()->equals($other->getType())) { + $type = $this->getType(); + } else { + $type = TypeCombinator::union($this->getType(), $other->getType()); + } return new self( - TypeCombinator::union($this->getType(), $other->getType()), + $type, $this->getCertainty()->and($other->getCertainty()) ); } diff --git a/vendor/phpstan/phpstan/src/Broker/AnonymousClassNameHelper.php b/vendor/phpstan/phpstan/src/Broker/AnonymousClassNameHelper.php index f5dedc21..57fc17f6 100644 --- a/vendor/phpstan/phpstan/src/Broker/AnonymousClassNameHelper.php +++ b/vendor/phpstan/phpstan/src/Broker/AnonymousClassNameHelper.php @@ -24,11 +24,11 @@ class AnonymousClassNameHelper } public function getAnonymousClassName( - \PhpParser\Node\Expr\New_ $node, + \PhpParser\Node\Stmt\Class_ $classNode, string $filename ): string { - if (!$node->class instanceof \PhpParser\Node\Stmt\Class_) { + if (isset($classNode->namespacedName)) { throw new \PHPStan\ShouldNotHappenException(); } @@ -38,7 +38,7 @@ class AnonymousClassNameHelper return sprintf( 'AnonymousClass%s', - md5(sprintf('%s:%s', $filename, $node->class->getLine())) + md5(sprintf('%s:%s', $filename, $classNode->getLine())) ); } diff --git a/vendor/phpstan/phpstan/src/Broker/Broker.php b/vendor/phpstan/phpstan/src/Broker/Broker.php index b0e3f1ad..5fcc1c29 100644 --- a/vendor/phpstan/phpstan/src/Broker/Broker.php +++ b/vendor/phpstan/phpstan/src/Broker/Broker.php @@ -269,11 +269,11 @@ class Broker } public function getAnonymousClassReflection( - \PhpParser\Node\Expr\New_ $node, + \PhpParser\Node\Stmt\Class_ $classNode, Scope $scope ): ClassReflection { - if (!$node->class instanceof \PhpParser\Node\Stmt\Class_) { + if (isset($classNode->namespacedName)) { throw new \PHPStan\ShouldNotHappenException(); } @@ -289,22 +289,20 @@ class Broker $filename = $this->relativePathHelper->getRelativePath($scopeFile); $className = $this->anonymousClassNameHelper->getAnonymousClassName( - $node, + $classNode, $filename ); + $classNode->name = new \PhpParser\Node\Identifier($className); if (isset(self::$anonymousClasses[$className])) { return self::$anonymousClasses[$className]; } - $classNode = $node->class; - $classNode->name = new \PhpParser\Node\Identifier($className); eval($this->printer->prettyPrint([$classNode])); - unset($classNode); self::$anonymousClasses[$className] = $this->getClassFromReflection( new \ReflectionClass('\\' . $className), - sprintf('class@anonymous/%s:%s', $filename, $node->getLine()), + sprintf('class@anonymous/%s:%s', $filename, $classNode->getLine()), $scopeFile ); $this->classReflections[$className] = self::$anonymousClasses[$className]; diff --git a/vendor/phpstan/phpstan/src/Reflection/ParametersAcceptorSelector.php b/vendor/phpstan/phpstan/src/Reflection/ParametersAcceptorSelector.php index a0e0d409..4ba5f154 100644 --- a/vendor/phpstan/phpstan/src/Reflection/ParametersAcceptorSelector.php +++ b/vendor/phpstan/phpstan/src/Reflection/ParametersAcceptorSelector.php @@ -173,6 +173,11 @@ class ParametersAcceptorSelector */ public static function combineAcceptors(array $acceptors): ParametersAcceptor { + if (count($acceptors) === 0) { + throw new \PHPStan\ShouldNotHappenException( + 'getVariants() must return at least one variant.' + ); + } if (count($acceptors) === 1) { return $acceptors[0]; } @@ -236,9 +241,6 @@ class ParametersAcceptorSelector } } - /** @var \PHPStan\Type\Type $returnType */ - $returnType = $returnType; - return new FunctionVariant($parameters, $isVariadic, $returnType); } diff --git a/vendor/phpstan/phpstan/src/Reflection/SignatureMap/functionMap.php b/vendor/phpstan/phpstan/src/Reflection/SignatureMap/functionMap.php index fd76f879..de95bbea 100644 --- a/vendor/phpstan/phpstan/src/Reflection/SignatureMap/functionMap.php +++ b/vendor/phpstan/phpstan/src/Reflection/SignatureMap/functionMap.php @@ -2174,7 +2174,7 @@ return [ 'Ds\Set::contains' => ['bool', '...values='=>'mixed'], 'Ds\Set::diff' => ['Ds\Set', 'set'=>'Ds\Set'], 'Ds\Set::filter' => ['Ds\Set', 'callback='=>'callable'], -'Ds\Set::first' => ['void'], +'Ds\Set::first' => ['mixed'], 'Ds\Set::get' => ['mixed', 'index'=>'int'], 'Ds\Set::intersect' => ['Ds\Set', 'set'=>'Ds\Set'], 'Ds\Set::join' => ['void', 'glue='=>'string'], diff --git a/vendor/phpstan/phpstan/src/Rules/Operators/InvalidBinaryOperationRule.php b/vendor/phpstan/phpstan/src/Rules/Operators/InvalidBinaryOperationRule.php index d12b8408..aa3d0a1d 100644 --- a/vendor/phpstan/phpstan/src/Rules/Operators/InvalidBinaryOperationRule.php +++ b/vendor/phpstan/phpstan/src/Rules/Operators/InvalidBinaryOperationRule.php @@ -96,8 +96,8 @@ class InvalidBinaryOperationRule implements \PHPStan\Rules\Rule } $scope = $scope - ->assignVariable($leftName, $leftType, \PHPStan\TrinaryLogic::createYes()) - ->assignVariable($rightName, $rightType, \PHPStan\TrinaryLogic::createYes()); + ->assignVariable($leftName, $leftType) + ->assignVariable($rightName, $rightType); if (!$scope->getType($newNode) instanceof ErrorType) { return []; diff --git a/vendor/phpstan/phpstan/src/Rules/Properties/AccessPropertiesInAssignRule.php b/vendor/phpstan/phpstan/src/Rules/Properties/AccessPropertiesInAssignRule.php new file mode 100644 index 00000000..4641ed10 --- /dev/null +++ b/vendor/phpstan/phpstan/src/Rules/Properties/AccessPropertiesInAssignRule.php @@ -0,0 +1,39 @@ +accessPropertiesRule = $accessPropertiesRule; + } + + public function getNodeType(): string + { + return Node\Expr\Assign::class; + } + + /** + * @param \PhpParser\Node\Expr\Assign $node + * @param \PHPStan\Analyser\Scope $scope + * @return (string|\PHPStan\Rules\RuleError)[] + */ + public function processNode(Node $node, Scope $scope): array + { + if (!$node->var instanceof Node\Expr\PropertyFetch) { + return []; + } + + return $this->accessPropertiesRule->processNode($node->var, $scope); + } + +} diff --git a/vendor/phpstan/phpstan/src/Rules/Properties/AccessPropertiesRule.php b/vendor/phpstan/phpstan/src/Rules/Properties/AccessPropertiesRule.php index eb304862..a8ff004c 100644 --- a/vendor/phpstan/phpstan/src/Rules/Properties/AccessPropertiesRule.php +++ b/vendor/phpstan/phpstan/src/Rules/Properties/AccessPropertiesRule.php @@ -63,6 +63,10 @@ class AccessPropertiesRule implements \PHPStan\Rules\Rule return $typeResult->getUnknownClassErrors(); } + if ($scope->isInExpressionAssign($node)) { + return []; + } + if (!$type->canAccessProperties()->yes()) { return [ sprintf('Cannot access property $%s on %s.', $name, $type->describe(VerbosityLevel::typeOnly())), diff --git a/vendor/phpstan/phpstan/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php b/vendor/phpstan/phpstan/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php new file mode 100644 index 00000000..b54e6e56 --- /dev/null +++ b/vendor/phpstan/phpstan/src/Rules/Properties/AccessStaticPropertiesInAssignRule.php @@ -0,0 +1,39 @@ +accessStaticPropertiesRule = $accessStaticPropertiesRule; + } + + public function getNodeType(): string + { + return Node\Expr\Assign::class; + } + + /** + * @param \PhpParser\Node\Expr\Assign $node + * @param \PHPStan\Analyser\Scope $scope + * @return (string|\PHPStan\Rules\RuleError)[] + */ + public function processNode(Node $node, Scope $scope): array + { + if (!$node->var instanceof Node\Expr\StaticPropertyFetch) { + return []; + } + + return $this->accessStaticPropertiesRule->processNode($node->var, $scope); + } + +} diff --git a/vendor/phpstan/phpstan/src/Rules/Properties/AccessStaticPropertiesRule.php b/vendor/phpstan/phpstan/src/Rules/Properties/AccessStaticPropertiesRule.php index 63ee7e8f..1b47e629 100644 --- a/vendor/phpstan/phpstan/src/Rules/Properties/AccessStaticPropertiesRule.php +++ b/vendor/phpstan/phpstan/src/Rules/Properties/AccessStaticPropertiesRule.php @@ -144,6 +144,10 @@ class AccessStaticPropertiesRule implements \PHPStan\Rules\Rule $typeForDescribe = $classType; $classType = TypeCombinator::remove($classType, new StringType()); + if ($scope->isInExpressionAssign($node)) { + return []; + } + if (!$classType->canAccessProperties()->yes()) { return array_merge($messages, [ sprintf('Cannot access static property $%s on %s.', $name, $typeForDescribe->describe(VerbosityLevel::typeOnly())), diff --git a/vendor/phpstan/phpstan/src/Type/Constant/ConstantArrayType.php b/vendor/phpstan/phpstan/src/Type/Constant/ConstantArrayType.php index bc1b8fc3..0021a855 100644 --- a/vendor/phpstan/phpstan/src/Type/Constant/ConstantArrayType.php +++ b/vendor/phpstan/phpstan/src/Type/Constant/ConstantArrayType.php @@ -10,7 +10,6 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\CompoundType; -use PHPStan\Type\ConstantScalarType; use PHPStan\Type\ConstantType; use PHPStan\Type\ErrorType; use PHPStan\Type\MixedType; @@ -360,7 +359,8 @@ class ConstantArrayType extends ArrayType implements ConstantType if (!$preserveKeys) { $i = 0; - $keyTypes = array_map(static function (ConstantScalarType $keyType) use (&$i): ConstantScalarType { + /** @var array $keyTypes */ + $keyTypes = array_map(static function ($keyType) use (&$i) { if ($keyType instanceof ConstantIntegerType) { $i++; return new ConstantIntegerType($i - 1); @@ -370,12 +370,14 @@ class ConstantArrayType extends ArrayType implements ConstantType }, $keyTypes); } + /** @var int|float $nextAutoIndex */ $nextAutoIndex = 0; foreach ($keyTypes as $keyType) { if (!$keyType instanceof ConstantIntegerType) { continue; } + /** @var int|float $nextAutoIndex */ $nextAutoIndex = max($nextAutoIndex, $keyType->getValue() + 1); } diff --git a/vendor/phpstan/phpstan/src/Type/FileTypeMapper.php b/vendor/phpstan/phpstan/src/Type/FileTypeMapper.php index 0aaf2b5d..2c0ee037 100644 --- a/vendor/phpstan/phpstan/src/Type/FileTypeMapper.php +++ b/vendor/phpstan/phpstan/src/Type/FileTypeMapper.php @@ -178,10 +178,7 @@ class FileTypeMapper throw new \PHPStan\ShouldNotHappenException(); } - $className = $this->anonymousClassNameHelper->getAnonymousClassName( - new Node\Expr\New_($node), - $fileName - ); + $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName); } else { $className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); } @@ -198,6 +195,9 @@ class FileTypeMapper if ($traitReflection->getFileName() === false) { continue; } + if (!file_exists($traitReflection->getFileName())) { + continue; + } $className = $classStack[count($classStack) - 1] ?? null; if ($className === null) { diff --git a/vendor/phpstan/phpstan/src/Type/ObjectType.php b/vendor/phpstan/phpstan/src/Type/ObjectType.php index 6105caf6..175be2bf 100644 --- a/vendor/phpstan/phpstan/src/Type/ObjectType.php +++ b/vendor/phpstan/phpstan/src/Type/ObjectType.php @@ -20,10 +20,7 @@ class ObjectType implements TypeWithClassName use TruthyBooleanTypeTrait; - private const EXTRA_OFFSET_CLASSES = [ - 'SimpleXMLElement' => true, - 'DOMNodeList' => true, - ]; + private const EXTRA_OFFSET_CLASSES = ['SimpleXMLElement', 'DOMNodeList']; /** @var string */ private $className; @@ -454,8 +451,13 @@ class ObjectType implements TypeWithClassName $classReflection = $broker->getClass($this->className); - if (array_key_exists($classReflection->getName(), self::EXTRA_OFFSET_CLASSES)) { - return TrinaryLogic::createYes(); + foreach (self::EXTRA_OFFSET_CLASSES as $extraOffsetClass) { + if ($classReflection->getName() === $extraOffsetClass) { + return TrinaryLogic::createYes(); + } + if ($classReflection->isSubclassOf($extraOffsetClass)) { + return TrinaryLogic::createYes(); + } } return TrinaryLogic::createNo(); diff --git a/vendor/phpstan/phpstan/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php b/vendor/phpstan/phpstan/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php index 50078340..24a851bc 100644 --- a/vendor/phpstan/phpstan/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php +++ b/vendor/phpstan/phpstan/src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php @@ -8,7 +8,6 @@ use PhpParser\Node\Expr\Variable; use PhpParser\Node\Stmt\Return_; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\TrinaryLogic; use PHPStan\Type\ArrayType; use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\Constant\ConstantArrayType; @@ -63,7 +62,7 @@ class ArrayFilterFunctionReturnTypeReturnTypeExtension implements \PHPStan\Type\ throw new \PHPStan\ShouldNotHappenException(); } $itemVariableName = $callbackArg->params[0]->var->name; - $scope = $scope->assignVariable($itemVariableName, $itemType, TrinaryLogic::createYes()); + $scope = $scope->assignVariable($itemVariableName, $itemType); $scope = $scope->filterByTruthyValue($statement->expr); $itemType = $scope->getVariableType($itemVariableName); } diff --git a/vendor/phpstan/phpstan/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php b/vendor/phpstan/phpstan/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php index 6ef6b6ae..e46fa68e 100644 --- a/vendor/phpstan/phpstan/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php +++ b/vendor/phpstan/phpstan/src/Type/Php/ArraySliceFunctionReturnTypeExtension.php @@ -61,13 +61,6 @@ class ArraySliceFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunc $constantArrays = TypeUtils::getConstantArrays($valueType); if (count($constantArrays) === 0) { - if (!$valueType instanceof ArrayType) { - return new ArrayType( - new MixedType(), - new MixedType() - ); - } - return $valueType; } diff --git a/vendor/phpstan/phpstan/src/Type/Php/FilterVarDynamicReturnTypeExtension.php b/vendor/phpstan/phpstan/src/Type/Php/FilterVarDynamicReturnTypeExtension.php new file mode 100644 index 00000000..b6b14cce --- /dev/null +++ b/vendor/phpstan/phpstan/src/Type/Php/FilterVarDynamicReturnTypeExtension.php @@ -0,0 +1,80 @@ + */ + private $filterTypesHashMaps; + + public function __construct() + { + $booleanType = new BooleanType(); + $floatOrFalseType = new UnionType([new FloatType(), new ConstantBooleanType(false)]); + $intOrFalseType = new UnionType([new IntegerType(), new ConstantBooleanType(false)]); + $stringOrFalseType = new UnionType([new StringType(), new ConstantBooleanType(false)]); + + $this->filterTypesHashMaps = [ + 'FILTER_SANITIZE_EMAIL' => $stringOrFalseType, + 'FILTER_SANITIZE_ENCODED' => $stringOrFalseType, + 'FILTER_SANITIZE_MAGIC_QUOTES' => $stringOrFalseType, + 'FILTER_SANITIZE_NUMBER_FLOAT' => $stringOrFalseType, + 'FILTER_SANITIZE_NUMBER_INT' => $stringOrFalseType, + 'FILTER_SANITIZE_SPECIAL_CHARS' => $stringOrFalseType, + 'FILTER_SANITIZE_STRING' => $stringOrFalseType, + 'FILTER_SANITIZE_URL' => $stringOrFalseType, + 'FILTER_VALIDATE_BOOLEAN' => $booleanType, + 'FILTER_VALIDATE_EMAIL' => $stringOrFalseType, + 'FILTER_VALIDATE_FLOAT' => $floatOrFalseType, + 'FILTER_VALIDATE_INT' => $intOrFalseType, + 'FILTER_VALIDATE_IP' => $stringOrFalseType, + 'FILTER_VALIDATE_MAC' => $stringOrFalseType, + 'FILTER_VALIDATE_REGEXP' => $stringOrFalseType, + 'FILTER_VALIDATE_URL' => $stringOrFalseType, + ]; + } + + public function isFunctionSupported(FunctionReflection $functionReflection): bool + { + return strtolower($functionReflection->getName()) === 'filter_var'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope + ): Type + { + $mixedType = new MixedType(); + + $filterArg = $functionCall->args[1] ?? null; + if ($filterArg === null) { + return $mixedType; + } + + $filterExpr = $filterArg->value; + if (!$filterExpr instanceof ConstFetch) { + return $mixedType; + } + + $filterName = (string) $filterExpr->name; + + return $this->filterTypesHashMaps[$filterName] ?? $mixedType; + } + +} diff --git a/vendor/phpstan/phpstan/src/Type/StaticType.php b/vendor/phpstan/phpstan/src/Type/StaticType.php index fa3d227c..df6ce925 100644 --- a/vendor/phpstan/phpstan/src/Type/StaticType.php +++ b/vendor/phpstan/phpstan/src/Type/StaticType.php @@ -77,7 +77,13 @@ class StaticType implements StaticResolvableType, TypeWithClassName public function equals(Type $type): bool { - return $this->staticObjectType->equals($type); + if (get_class($type) !== static::class) { + return false; + } + + /** @var StaticType $type */ + $type = $type; + return $this->staticObjectType->equals($type->staticObjectType); } public function describe(VerbosityLevel $level): string diff --git a/vendor/phpstan/phpstan/src/Type/TypeCombinator.php b/vendor/phpstan/phpstan/src/Type/TypeCombinator.php index 988bc9ce..1429d8ff 100644 --- a/vendor/phpstan/phpstan/src/Type/TypeCombinator.php +++ b/vendor/phpstan/phpstan/src/Type/TypeCombinator.php @@ -77,7 +77,11 @@ class TypeCombinator public static function removeNull(Type $type): Type { - return self::remove($type, new NullType()); + if (self::containsNull($type)) { + return self::remove($type, new NullType()); + } + + return $type; } public static function containsNull(Type $type): bool @@ -146,14 +150,14 @@ class TypeCombinator continue; } if ($innerType instanceof AccessoryType || $innerType instanceof CallableType) { - $intermediateAccessoryTypes[] = $innerType; + $intermediateAccessoryTypes[$innerType->describe(VerbosityLevel::precise())] = $innerType; continue; } } if ($intermediateArrayType !== null) { $arrayTypes[] = $intermediateArrayType; - $arrayAccessoryTypes = array_merge($arrayAccessoryTypes, $intermediateAccessoryTypes); + $arrayAccessoryTypes[] = $intermediateAccessoryTypes; unset($types[$i]); continue; } @@ -163,14 +167,25 @@ class TypeCombinator } $arrayTypes[] = $types[$i]; + $arrayAccessoryTypes[] = []; unset($types[$i]); } /** @var ArrayType[] $arrayTypes */ $arrayTypes = $arrayTypes; + $arrayAccessoryTypesToProcess = []; + if (count($arrayAccessoryTypes) > 1) { + $arrayAccessoryTypesToProcess = array_values(array_intersect_key(...$arrayAccessoryTypes)); + } elseif (count($arrayAccessoryTypes) > 0) { + $arrayAccessoryTypesToProcess = array_values($arrayAccessoryTypes[0]); + } + $types = array_values( - array_merge($types, self::processArrayTypes($arrayTypes, $arrayAccessoryTypes)) + array_merge( + $types, + self::processArrayTypes($arrayTypes, $arrayAccessoryTypesToProcess) + ) ); // simplify string[] | int[] to (string|int)[] @@ -322,18 +337,18 @@ class TypeCombinator $constantKeyTypesNumbered = $constantKeyTypesNumbered; $constantArraysBuckets = []; - foreach ($arrayTypes as $arrayType) { + foreach ($arrayTypes as $arrayTypeAgain) { $arrayIndex = 0; - foreach ($arrayType->getKeyTypes() as $keyType) { + foreach ($arrayTypeAgain->getKeyTypes() as $keyType) { $arrayIndex += $constantKeyTypesNumbered[$keyType->getValue()]; } if (!array_key_exists($arrayIndex, $constantArraysBuckets)) { $bucket = []; - foreach ($arrayType->getKeyTypes() as $i => $keyType) { + foreach ($arrayTypeAgain->getKeyTypes() as $i => $keyType) { $bucket[$keyType->getValue()] = [ 'keyType' => $keyType, - 'valueType' => $arrayType->getValueTypes()[$i], + 'valueType' => $arrayTypeAgain->getValueTypes()[$i], ]; } $constantArraysBuckets[$arrayIndex] = $bucket; @@ -341,10 +356,10 @@ class TypeCombinator } $bucket = $constantArraysBuckets[$arrayIndex]; - foreach ($arrayType->getKeyTypes() as $i => $keyType) { + foreach ($arrayTypeAgain->getKeyTypes() as $i => $keyType) { $bucket[$keyType->getValue()]['valueType'] = self::union( $bucket[$keyType->getValue()]['valueType'], - $arrayType->getValueTypes()[$i] + $arrayTypeAgain->getValueTypes()[$i] ); } diff --git a/vendor/phpstan/phpstan/src/Type/TypeUtils.php b/vendor/phpstan/phpstan/src/Type/TypeUtils.php index b24d06bf..e0c195cd 100644 --- a/vendor/phpstan/phpstan/src/Type/TypeUtils.php +++ b/vendor/phpstan/phpstan/src/Type/TypeUtils.php @@ -45,6 +45,24 @@ class TypeUtils return self::map(ConstantType::class, $type, false); } + /** + * @param \PHPStan\Type\Type $type + * @return \PHPStan\Type\ConstantType[] + */ + public static function getAnyConstantTypes(Type $type): array + { + return self::map(ConstantType::class, $type, false, false); + } + + /** + * @param \PHPStan\Type\Type $type + * @return \PHPStan\Type\ArrayType[] + */ + public static function getAnyArrays(Type $type): array + { + return self::map(ArrayType::class, $type, true, false); + } + public static function generalizeType(Type $type): Type { if ($type instanceof ConstantType) { @@ -97,12 +115,14 @@ class TypeUtils * @param string $typeClass * @param Type $type * @param bool $inspectIntersections + * @param bool $stopOnUnmatched * @return mixed[] */ private static function map( string $typeClass, Type $type, - bool $inspectIntersections + bool $inspectIntersections, + bool $stopOnUnmatched = true ): array { if ($type instanceof $typeClass) { @@ -113,7 +133,11 @@ class TypeUtils $matchingTypes = []; foreach ($type->getTypes() as $innerType) { if (!$innerType instanceof $typeClass) { - return []; + if ($stopOnUnmatched) { + return []; + } + + continue; } $matchingTypes[] = $innerType; @@ -126,6 +150,10 @@ class TypeUtils $matchingTypes = []; foreach ($type->getTypes() as $innerType) { if (!$innerType instanceof $typeClass) { + if ($stopOnUnmatched) { + return []; + } + continue; } diff --git a/vendor/symfony/console/Application.php b/vendor/symfony/console/Application.php index f96d6905..17df91f9 100644 --- a/vendor/symfony/console/Application.php +++ b/vendor/symfony/console/Application.php @@ -199,6 +199,13 @@ class Application return 0; } + try { + // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument. + $input->bind($this->getDefinition()); + } catch (ExceptionInterface $e) { + // Errors must be ignored, full binding/validation happens later when the command is known. + } + $name = $this->getCommandName($input); if (true === $input->hasParameterOption(['--help', '-h'], true)) { if (!$name) { diff --git a/vendor/symfony/console/Helper/ProgressBar.php b/vendor/symfony/console/Helper/ProgressBar.php index b68d9fe3..12a6cf22 100644 --- a/vendor/symfony/console/Helper/ProgressBar.php +++ b/vendor/symfony/console/Helper/ProgressBar.php @@ -381,20 +381,17 @@ final class ProgressBar $lines = floor(Helper::strlen($message) / $this->terminal->getWidth()) + $this->formatLineCount + 1; $this->output->clear($lines); } else { - // Move the cursor to the beginning of the line - $this->output->write("\x0D"); - - // Erase the line - $this->output->write("\x1B[2K"); - // Erase previous lines if ($this->formatLineCount > 0) { - $this->output->write(str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount)); + $message = str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount).$message; } + + // Move the cursor to the beginning of the line and erase the line + $message = "\x0D\x1B[2K$message"; } } } elseif ($this->step > 0) { - $this->output->writeln(''); + $message = PHP_EOL.$message; } $this->firstRun = false; diff --git a/vendor/symfony/console/Helper/QuestionHelper.php b/vendor/symfony/console/Helper/QuestionHelper.php index 575c5345..cf6447a4 100644 --- a/vendor/symfony/console/Helper/QuestionHelper.php +++ b/vendor/symfony/console/Helper/QuestionHelper.php @@ -126,7 +126,7 @@ class QuestionHelper extends Helper if (false === $ret) { $ret = fgets($inputStream, 4096); if (false === $ret) { - throw new RuntimeException('Aborted'); + throw new RuntimeException('Aborted.'); } $ret = trim($ret); } @@ -213,8 +213,10 @@ class QuestionHelper extends Helper while (!feof($inputStream)) { $c = fread($inputStream, 1); - // Backspace Character - if ("\177" === $c) { + // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false. + if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) { + throw new RuntimeException('Aborted.'); + } elseif ("\177" === $c) { // Backspace Character if (0 === $numMatches && 0 !== $i) { --$i; // Move cursor backwards @@ -267,6 +269,10 @@ class QuestionHelper extends Helper continue; } else { + if ("\x80" <= $c) { + $c .= fread($inputStream, ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3][$c & "\xF0"]); + } + $output->write($c); $ret .= $c; ++$i; @@ -339,7 +345,7 @@ class QuestionHelper extends Helper shell_exec(sprintf('stty %s', $sttyMode)); if (false === $value) { - throw new RuntimeException('Aborted'); + throw new RuntimeException('Aborted.'); } $value = trim($value); diff --git a/vendor/symfony/console/Input/ArgvInput.php b/vendor/symfony/console/Input/ArgvInput.php index 85ff6f91..c56c20c6 100644 --- a/vendor/symfony/console/Input/ArgvInput.php +++ b/vendor/symfony/console/Input/ArgvInput.php @@ -257,8 +257,27 @@ class ArgvInput extends Input */ public function getFirstArgument() { - foreach ($this->tokens as $token) { + $isOption = false; + foreach ($this->tokens as $i => $token) { if ($token && '-' === $token[0]) { + if (false !== strpos($token, '=') || !isset($this->tokens[$i + 1])) { + continue; + } + + // If it's a long option, consider that everything after "--" is the option name. + // Otherwise, use the last char (if it's a short option set, only the last one can take a value with space separator) + $name = '-' === $token[1] ? substr($token, 2) : substr($token, -1); + if (!isset($this->options[$name]) && !$this->definition->hasShortcut($name)) { + // noop + } elseif ((isset($this->options[$name]) || isset($this->options[$name = $this->definition->shortcutToName($name)])) && $this->tokens[$i + 1] === $this->options[$name]) { + $isOption = true; + } + + continue; + } + + if ($isOption) { + $isOption = false; continue; } diff --git a/vendor/symfony/console/Input/ArrayInput.php b/vendor/symfony/console/Input/ArrayInput.php index cf09ff45..44c2f0d5 100644 --- a/vendor/symfony/console/Input/ArrayInput.php +++ b/vendor/symfony/console/Input/ArrayInput.php @@ -19,7 +19,7 @@ use Symfony\Component\Console\Exception\InvalidOptionException; * * Usage: * - * $input = new ArrayInput(['name' => 'foo', '--bar' => 'foobar']); + * $input = new ArrayInput(['command' => 'foo:bar', 'foo' => 'bar', '--bar' => 'foobar']); * * @author Fabien Potencier */ diff --git a/vendor/symfony/console/Input/Input.php b/vendor/symfony/console/Input/Input.php index 7a16e0ee..c1220316 100644 --- a/vendor/symfony/console/Input/Input.php +++ b/vendor/symfony/console/Input/Input.php @@ -69,7 +69,7 @@ abstract class Input implements InputInterface, StreamableInputInterface $givenArguments = $this->arguments; $missingArguments = array_filter(array_keys($definition->getArguments()), function ($argument) use ($definition, $givenArguments) { - return !array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired(); + return !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired(); }); if (\count($missingArguments) > 0) { @@ -150,7 +150,7 @@ abstract class Input implements InputInterface, StreamableInputInterface throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); } - return array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); + return \array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); } /** diff --git a/vendor/symfony/console/Input/InputDefinition.php b/vendor/symfony/console/Input/InputDefinition.php index c7e5c987..2189c462 100644 --- a/vendor/symfony/console/Input/InputDefinition.php +++ b/vendor/symfony/console/Input/InputDefinition.php @@ -338,8 +338,10 @@ class InputDefinition * @return string The InputOption name * * @throws InvalidArgumentException When option given does not exist + * + * @internal */ - private function shortcutToName($shortcut) + public function shortcutToName($shortcut) { if (!isset($this->shortcuts[$shortcut])) { throw new InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); diff --git a/vendor/symfony/console/Tester/CommandTester.php b/vendor/symfony/console/Tester/CommandTester.php index da51559f..57efc9a6 100644 --- a/vendor/symfony/console/Tester/CommandTester.php +++ b/vendor/symfony/console/Tester/CommandTester.php @@ -60,9 +60,8 @@ class CommandTester } $this->input = new ArrayInput($input); - if ($this->inputs) { - $this->input->setStream(self::createStream($this->inputs)); - } + // Use an in-memory input stream even if no inputs are set so that QuestionHelper::ask() does not rely on the blocking STDIN. + $this->input->setStream(self::createStream($this->inputs)); if (isset($options['interactive'])) { $this->input->setInteractive($options['interactive']); diff --git a/vendor/symfony/console/Tester/TesterTrait.php b/vendor/symfony/console/Tester/TesterTrait.php index e5df56d8..7b5e128c 100644 --- a/vendor/symfony/console/Tester/TesterTrait.php +++ b/vendor/symfony/console/Tester/TesterTrait.php @@ -126,7 +126,7 @@ trait TesterTrait */ private function initOutput(array $options) { - $this->captureStreamsIndependently = array_key_exists('capture_stderr_separately', $options) && $options['capture_stderr_separately']; + $this->captureStreamsIndependently = \array_key_exists('capture_stderr_separately', $options) && $options['capture_stderr_separately']; if (!$this->captureStreamsIndependently) { $this->output = new StreamOutput(fopen('php://memory', 'w', false)); if (isset($options['decorated'])) { diff --git a/vendor/symfony/console/Tests/ApplicationTest.php b/vendor/symfony/console/Tests/ApplicationTest.php index 2aaf9d7d..e9b1be32 100644 --- a/vendor/symfony/console/Tests/ApplicationTest.php +++ b/vendor/symfony/console/Tests/ApplicationTest.php @@ -968,6 +968,19 @@ class ApplicationTest extends TestCase $this->assertSame('called'.PHP_EOL, $tester->getDisplay(), '->run() does not call interact() if -n is passed'); } + public function testRunWithGlobalOptionAndNoCommand() + { + $application = new Application(); + $application->setAutoExit(false); + $application->setCatchExceptions(false); + $application->getDefinition()->addOption(new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL)); + + $output = new StreamOutput(fopen('php://memory', 'w', false)); + $input = new ArgvInput(['cli.php', '--foo', 'bar']); + + $this->assertSame(0, $application->run($input, $output)); + } + /** * Issue #9285. * diff --git a/vendor/symfony/console/Tests/Helper/QuestionHelperTest.php b/vendor/symfony/console/Tests/Helper/QuestionHelperTest.php index de3ec455..e3edbed0 100644 --- a/vendor/symfony/console/Tests/Helper/QuestionHelperTest.php +++ b/vendor/symfony/console/Tests/Helper/QuestionHelperTest.php @@ -237,6 +237,43 @@ class QuestionHelperTest extends AbstractQuestionHelperTest $this->assertSame('b', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question)); } + public function getInputs() + { + return [ + ['$'], // 1 byte character + ['¢'], // 2 bytes character + ['€'], // 3 bytes character + ['𐍈'], // 4 bytes character + ]; + } + + /** + * @dataProvider getInputs + */ + public function testAskWithAutocompleteWithMultiByteCharacter($character) + { + if (!$this->hasSttyAvailable()) { + $this->markTestSkipped('`stty` is required to test autocomplete functionality'); + } + + $inputStream = $this->getInputStream("$character\n"); + + $possibleChoices = [ + '$' => '1 byte character', + '¢' => '2 bytes character', + '€' => '3 bytes character', + '𐍈' => '4 bytes character', + ]; + + $dialog = new QuestionHelper(); + $dialog->setHelperSet(new HelperSet([new FormatterHelper()])); + + $question = new ChoiceQuestion('Please select a character', $possibleChoices); + $question->setMaxAttempts(1); + + $this->assertSame($character, $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question)); + } + public function testAutocompleteWithTrailingBackslash() { if (!$this->hasSttyAvailable()) { @@ -549,7 +586,7 @@ class QuestionHelperTest extends AbstractQuestionHelperTest /** * @expectedException \Symfony\Component\Console\Exception\RuntimeException - * @expectedExceptionMessage Aborted + * @expectedExceptionMessage Aborted. */ public function testAskThrowsExceptionOnMissingInput() { @@ -559,7 +596,17 @@ class QuestionHelperTest extends AbstractQuestionHelperTest /** * @expectedException \Symfony\Component\Console\Exception\RuntimeException - * @expectedExceptionMessage Aborted + * @expectedExceptionMessage Aborted. + */ + public function testAskThrowsExceptionOnMissingInputForChoiceQuestion() + { + $dialog = new QuestionHelper(); + $dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new ChoiceQuestion('Choice', ['a', 'b'])); + } + + /** + * @expectedException \Symfony\Component\Console\Exception\RuntimeException + * @expectedExceptionMessage Aborted. */ public function testAskThrowsExceptionOnMissingInputWithValidator() { diff --git a/vendor/symfony/console/Tests/Helper/SymfonyQuestionHelperTest.php b/vendor/symfony/console/Tests/Helper/SymfonyQuestionHelperTest.php index e7fd8060..cf7a78c3 100644 --- a/vendor/symfony/console/Tests/Helper/SymfonyQuestionHelperTest.php +++ b/vendor/symfony/console/Tests/Helper/SymfonyQuestionHelperTest.php @@ -124,7 +124,7 @@ class SymfonyQuestionHelperTest extends AbstractQuestionHelperTest /** * @expectedException \Symfony\Component\Console\Exception\RuntimeException - * @expectedExceptionMessage Aborted + * @expectedExceptionMessage Aborted. */ public function testAskThrowsExceptionOnMissingInput() { diff --git a/vendor/symfony/console/Tests/Input/ArgvInputTest.php b/vendor/symfony/console/Tests/Input/ArgvInputTest.php index 37caaf2d..e20bcdd2 100644 --- a/vendor/symfony/console/Tests/Input/ArgvInputTest.php +++ b/vendor/symfony/console/Tests/Input/ArgvInputTest.php @@ -312,6 +312,14 @@ class ArgvInputTest extends TestCase $input = new ArgvInput(['cli.php', '-fbbar', 'foo']); $this->assertEquals('foo', $input->getFirstArgument(), '->getFirstArgument() returns the first argument from the raw input'); + + $input = new ArgvInput(['cli.php', '--foo', 'fooval', 'bar']); + $input->bind(new InputDefinition([new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL), new InputArgument('arg')])); + $this->assertSame('bar', $input->getFirstArgument()); + + $input = new ArgvInput(['cli.php', '-bf', 'fooval', 'argval']); + $input->bind(new InputDefinition([new InputOption('bar', 'b', InputOption::VALUE_NONE), new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL), new InputArgument('arg')])); + $this->assertSame('argval', $input->getFirstArgument()); } public function testHasParameterOption() diff --git a/vendor/symfony/console/Tests/Tester/CommandTesterTest.php b/vendor/symfony/console/Tests/Tester/CommandTesterTest.php index 6d9b7417..70662967 100644 --- a/vendor/symfony/console/Tests/Tester/CommandTesterTest.php +++ b/vendor/symfony/console/Tests/Tester/CommandTesterTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Output\Output; +use Symfony\Component\Console\Question\ChoiceQuestion; use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Tester\CommandTester; @@ -139,7 +140,7 @@ class CommandTesterTest extends TestCase /** * @expectedException \RuntimeException - * @expectedMessage Aborted + * @expectedExceptionMessage Aborted. */ public function testCommandWithWrongInputsNumber() { @@ -153,13 +154,40 @@ class CommandTesterTest extends TestCase $command->setHelperSet(new HelperSet([new QuestionHelper()])); $command->setCode(function ($input, $output) use ($questions, $command) { $helper = $command->getHelper('question'); + $helper->ask($input, $output, new ChoiceQuestion('choice', ['a', 'b'])); + $helper->ask($input, $output, new Question($questions[0])); + $helper->ask($input, $output, new Question($questions[1])); + $helper->ask($input, $output, new Question($questions[2])); + }); + + $tester = new CommandTester($command); + $tester->setInputs(['a', 'Bobby', 'Fine']); + $tester->execute([]); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage Aborted. + */ + public function testCommandWithQuestionsButNoInputs() + { + $questions = [ + 'What\'s your name?', + 'How are you?', + 'Where do you come from?', + ]; + + $command = new Command('foo'); + $command->setHelperSet(new HelperSet([new QuestionHelper()])); + $command->setCode(function ($input, $output) use ($questions, $command) { + $helper = $command->getHelper('question'); + $helper->ask($input, $output, new ChoiceQuestion('choice', ['a', 'b'])); $helper->ask($input, $output, new Question($questions[0])); $helper->ask($input, $output, new Question($questions[1])); $helper->ask($input, $output, new Question($questions[2])); }); $tester = new CommandTester($command); - $tester->setInputs(['Bobby', 'Fine']); $tester->execute([]); } diff --git a/vendor/symfony/finder/Finder.php b/vendor/symfony/finder/Finder.php index ab8f92b3..83163f51 100644 --- a/vendor/symfony/finder/Finder.php +++ b/vendor/symfony/finder/Finder.php @@ -29,7 +29,7 @@ use Symfony\Component\Finder\Iterator\SortableIterator; * * All rules may be invoked several times. * - * All methods return the current Finder object to allow easy chaining: + * All methods return the current Finder object to allow chaining: * * $finder = Finder::create()->files()->name('*.php')->in(__DIR__); * @@ -674,12 +674,15 @@ class Finder implements \IteratorAggregate, \Countable private function searchInDirectory(string $dir): \Iterator { + $exclude = $this->exclude; + $notPaths = $this->notPaths; + if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) { - $this->exclude = array_merge($this->exclude, self::$vcsPatterns); + $exclude = array_merge($exclude, self::$vcsPatterns); } if (static::IGNORE_DOT_FILES === (static::IGNORE_DOT_FILES & $this->ignore)) { - $this->notPaths[] = '#(^|/)\..+(/|$)#'; + $notPaths[] = '#(^|/)\..+(/|$)#'; } $minDepth = 0; @@ -712,8 +715,8 @@ class Finder implements \IteratorAggregate, \Countable $iterator = new Iterator\RecursiveDirectoryIterator($dir, $flags, $this->ignoreUnreadableDirs); - if ($this->exclude) { - $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude); + if ($exclude) { + $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $exclude); } $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); @@ -746,8 +749,8 @@ class Finder implements \IteratorAggregate, \Countable $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters); } - if ($this->paths || $this->notPaths) { - $iterator = new Iterator\PathFilterIterator($iterator, $this->paths, $this->notPaths); + if ($this->paths || $notPaths) { + $iterator = new Iterator\PathFilterIterator($iterator, $this->paths, $notPaths); } if ($this->sort || $this->reverseSorting) { diff --git a/vendor/symfony/finder/Tests/FinderTest.php b/vendor/symfony/finder/Tests/FinderTest.php index 1b50663e..9217cb71 100644 --- a/vendor/symfony/finder/Tests/FinderTest.php +++ b/vendor/symfony/finder/Tests/FinderTest.php @@ -421,6 +421,59 @@ class FinderTest extends Iterator\RealIteratorTestCase ]), $finder->in(self::$tmpDir)->getIterator()); } + public function testIgnoreVCSCanBeDisabledAfterFirstIteration() + { + $finder = $this->buildFinder(); + $finder->in(self::$tmpDir); + $finder->ignoreDotFiles(false); + + $this->assertIterator($this->toAbsolute([ + 'foo', + 'foo/bar.tmp', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + 'test.php', + 'test.py', + 'toto', + '.bar', + '.foo', + '.foo/.bar', + '.foo/bar', + 'foo bar', + ]), $finder->getIterator()); + + $finder->ignoreVCS(false); + $this->assertIterator($this->toAbsolute(['.git', + 'foo', + 'foo/bar.tmp', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + 'test.php', + 'test.py', + 'toto', + 'toto/.git', + '.bar', + '.foo', + '.foo/.bar', + '.foo/bar', + 'foo bar', + ]), $finder->getIterator()); + } + public function testIgnoreDotFiles() { $finder = $this->buildFinder(); @@ -496,6 +549,53 @@ class FinderTest extends Iterator\RealIteratorTestCase ]), $finder->in(self::$tmpDir)->getIterator()); } + public function testIgnoreDotFilesCanBeDisabledAfterFirstIteration() + { + $finder = $this->buildFinder(); + $finder->in(self::$tmpDir); + + $this->assertIterator($this->toAbsolute([ + 'foo', + 'foo/bar.tmp', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + 'test.php', + 'test.py', + 'toto', + 'foo bar', + ]), $finder->getIterator()); + + $finder->ignoreDotFiles(false); + $this->assertIterator($this->toAbsolute([ + 'foo', + 'foo/bar.tmp', + 'qux', + 'qux/baz_100_1.py', + 'qux/baz_1_2.py', + 'qux_0_1.php', + 'qux_1000_1.php', + 'qux_1002_0.php', + 'qux_10_2.php', + 'qux_12_0.php', + 'qux_2_0.php', + 'test.php', + 'test.py', + 'toto', + '.bar', + '.foo', + '.foo/.bar', + '.foo/bar', + 'foo bar', + ]), $finder->getIterator()); + } + public function testSortByName() { $finder = $this->buildFinder();