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
[](https://github.com/nette/finder/releases)
[](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.
-
-
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();