Update composer packages

This commit is contained in:
Alex Cabal 2019-03-10 17:08:02 -05:00
parent 58cc098058
commit 415aed8b31
59 changed files with 3352 additions and 2347 deletions

58
composer.lock generated
View file

@ -382,33 +382,33 @@
}, },
{ {
"name": "nette/finder", "name": "nette/finder",
"version": "v2.4.2", "version": "v2.5.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nette/finder.git", "url": "https://github.com/nette/finder.git",
"reference": "ee951a656cb8ac622e5dd33474a01fd2470505a0" "reference": "6be1b83ea68ac558aff189d640abe242e0306fe2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nette/finder/zipball/ee951a656cb8ac622e5dd33474a01fd2470505a0", "url": "https://api.github.com/repos/nette/finder/zipball/6be1b83ea68ac558aff189d640abe242e0306fe2",
"reference": "ee951a656cb8ac622e5dd33474a01fd2470505a0", "reference": "6be1b83ea68ac558aff189d640abe242e0306fe2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"nette/utils": "~2.4", "nette/utils": "^2.4 || ~3.0.0",
"php": ">=5.6.0" "php": ">=7.1"
}, },
"conflict": { "conflict": {
"nette/nette": "<2.2" "nette/nette": "<2.2"
}, },
"require-dev": { "require-dev": {
"nette/tester": "~2.0", "nette/tester": "^2.0",
"tracy/tracy": "^2.3" "tracy/tracy": "^2.3"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "2.4-dev" "dev-master": "2.5-dev"
} }
}, },
"autoload": { "autoload": {
@ -440,7 +440,7 @@
"iterator", "iterator",
"nette" "nette"
], ],
"time": "2018-06-28T11:49:23+00:00" "time": "2019-02-28T18:13:25+00:00"
}, },
{ {
"name": "nette/neon", "name": "nette/neon",
@ -567,16 +567,16 @@
}, },
{ {
"name": "nette/robot-loader", "name": "nette/robot-loader",
"version": "v3.1.0", "version": "v3.1.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nette/robot-loader.git", "url": "https://github.com/nette/robot-loader.git",
"reference": "fc76c70e740b10f091e502b2e393d0be912f38d4" "reference": "3e8d75d6d976e191bdf46752ca40a286671219d2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nette/robot-loader/zipball/fc76c70e740b10f091e502b2e393d0be912f38d4", "url": "https://api.github.com/repos/nette/robot-loader/zipball/3e8d75d6d976e191bdf46752ca40a286671219d2",
"reference": "fc76c70e740b10f091e502b2e393d0be912f38d4", "reference": "3e8d75d6d976e191bdf46752ca40a286671219d2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -628,7 +628,7 @@
"nette", "nette",
"trait" "trait"
], ],
"time": "2018-08-13T14:19:06+00:00" "time": "2019-03-01T20:23:02+00:00"
}, },
{ {
"name": "nette/utils", "name": "nette/utils",
@ -861,16 +861,16 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "0.11.2", "version": "0.11.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "8e185a74004920419ee97bf9dc62e6a175e8dca5" "reference": "e4644b4a8fd393c346f1137305fb2f76a7dc20a7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8e185a74004920419ee97bf9dc62e6a175e8dca5", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e4644b4a8fd393c346f1137305fb2f76a7dc20a7",
"reference": "8e185a74004920419ee97bf9dc62e6a175e8dca5", "reference": "e4644b4a8fd393c346f1137305fb2f76a7dc20a7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -930,7 +930,7 @@
"MIT" "MIT"
], ],
"description": "PHPStan - PHP Static Analysis Tool", "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", "name": "psr/log",
@ -981,16 +981,16 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v4.2.3", "version": "v4.2.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4" "reference": "9dc2299a016497f9ee620be94524e6c0af0280a9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4", "url": "https://api.github.com/repos/symfony/console/zipball/9dc2299a016497f9ee620be94524e6c0af0280a9",
"reference": "1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4", "reference": "9dc2299a016497f9ee620be94524e6c0af0280a9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1049,7 +1049,7 @@
], ],
"description": "Symfony Console Component", "description": "Symfony Console Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2019-01-25T14:35:16+00:00" "time": "2019-02-23T15:17:42+00:00"
}, },
{ {
"name": "symfony/contracts", "name": "symfony/contracts",
@ -1121,16 +1121,16 @@
}, },
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v4.2.3", "version": "v4.2.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/finder.git", "url": "https://github.com/symfony/finder.git",
"reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c" "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/ef71816cbb264988bb57fe6a73f610888b9aa70c", "url": "https://api.github.com/repos/symfony/finder/zipball/267b7002c1b70ea80db0833c3afe05f0fbde580a",
"reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c", "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1166,7 +1166,7 @@
], ],
"description": "Symfony Finder Component", "description": "Symfony Finder Component",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2019-01-16T20:35:37+00:00" "time": "2019-02-23T15:42:05+00:00"
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",

View file

@ -249,35 +249,35 @@
}, },
{ {
"name": "nette/finder", "name": "nette/finder",
"version": "v2.4.2", "version": "v2.5.0",
"version_normalized": "2.4.2.0", "version_normalized": "2.5.0.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nette/finder.git", "url": "https://github.com/nette/finder.git",
"reference": "ee951a656cb8ac622e5dd33474a01fd2470505a0" "reference": "6be1b83ea68ac558aff189d640abe242e0306fe2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nette/finder/zipball/ee951a656cb8ac622e5dd33474a01fd2470505a0", "url": "https://api.github.com/repos/nette/finder/zipball/6be1b83ea68ac558aff189d640abe242e0306fe2",
"reference": "ee951a656cb8ac622e5dd33474a01fd2470505a0", "reference": "6be1b83ea68ac558aff189d640abe242e0306fe2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"nette/utils": "~2.4", "nette/utils": "^2.4 || ~3.0.0",
"php": ">=5.6.0" "php": ">=7.1"
}, },
"conflict": { "conflict": {
"nette/nette": "<2.2" "nette/nette": "<2.2"
}, },
"require-dev": { "require-dev": {
"nette/tester": "~2.0", "nette/tester": "^2.0",
"tracy/tracy": "^2.3" "tracy/tracy": "^2.3"
}, },
"time": "2018-06-28T11:49:23+00:00", "time": "2019-02-28T18:13:25+00:00",
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "2.4-dev" "dev-master": "2.5-dev"
} }
}, },
"installation-source": "dist", "installation-source": "dist",
@ -440,17 +440,17 @@
}, },
{ {
"name": "nette/robot-loader", "name": "nette/robot-loader",
"version": "v3.1.0", "version": "v3.1.1",
"version_normalized": "3.1.0.0", "version_normalized": "3.1.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/nette/robot-loader.git", "url": "https://github.com/nette/robot-loader.git",
"reference": "fc76c70e740b10f091e502b2e393d0be912f38d4" "reference": "3e8d75d6d976e191bdf46752ca40a286671219d2"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/nette/robot-loader/zipball/fc76c70e740b10f091e502b2e393d0be912f38d4", "url": "https://api.github.com/repos/nette/robot-loader/zipball/3e8d75d6d976e191bdf46752ca40a286671219d2",
"reference": "fc76c70e740b10f091e502b2e393d0be912f38d4", "reference": "3e8d75d6d976e191bdf46752ca40a286671219d2",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -466,7 +466,7 @@
"nette/tester": "^2.0", "nette/tester": "^2.0",
"tracy/tracy": "^2.3" "tracy/tracy": "^2.3"
}, },
"time": "2018-08-13T14:19:06+00:00", "time": "2019-03-01T20:23:02+00:00",
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
@ -744,17 +744,17 @@
}, },
{ {
"name": "phpstan/phpstan", "name": "phpstan/phpstan",
"version": "0.11.2", "version": "0.11.3",
"version_normalized": "0.11.2.0", "version_normalized": "0.11.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/phpstan/phpstan.git", "url": "https://github.com/phpstan/phpstan.git",
"reference": "8e185a74004920419ee97bf9dc62e6a175e8dca5" "reference": "e4644b4a8fd393c346f1137305fb2f76a7dc20a7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/8e185a74004920419ee97bf9dc62e6a175e8dca5", "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e4644b4a8fd393c346f1137305fb2f76a7dc20a7",
"reference": "8e185a74004920419ee97bf9dc62e6a175e8dca5", "reference": "e4644b4a8fd393c346f1137305fb2f76a7dc20a7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -792,7 +792,7 @@
"slevomat/coding-standard": "^4.7.2", "slevomat/coding-standard": "^4.7.2",
"squizlabs/php_codesniffer": "^3.3.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": [
"bin/phpstan" "bin/phpstan"
], ],
@ -868,17 +868,17 @@
}, },
{ {
"name": "symfony/console", "name": "symfony/console",
"version": "v4.2.3", "version": "v4.2.4",
"version_normalized": "4.2.3.0", "version_normalized": "4.2.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/console.git", "url": "https://github.com/symfony/console.git",
"reference": "1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4" "reference": "9dc2299a016497f9ee620be94524e6c0af0280a9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4", "url": "https://api.github.com/repos/symfony/console/zipball/9dc2299a016497f9ee620be94524e6c0af0280a9",
"reference": "1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4", "reference": "9dc2299a016497f9ee620be94524e6c0af0280a9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -907,7 +907,7 @@
"symfony/lock": "", "symfony/lock": "",
"symfony/process": "" "symfony/process": ""
}, },
"time": "2019-01-25T14:35:16+00:00", "time": "2019-02-23T15:17:42+00:00",
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
@ -1012,23 +1012,23 @@
}, },
{ {
"name": "symfony/finder", "name": "symfony/finder",
"version": "v4.2.3", "version": "v4.2.4",
"version_normalized": "4.2.3.0", "version_normalized": "4.2.4.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/finder.git", "url": "https://github.com/symfony/finder.git",
"reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c" "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/ef71816cbb264988bb57fe6a73f610888b9aa70c", "url": "https://api.github.com/repos/symfony/finder/zipball/267b7002c1b70ea80db0833c3afe05f0fbde580a",
"reference": "ef71816cbb264988bb57fe6a73f610888b9aa70c", "reference": "267b7002c1b70ea80db0833c3afe05f0fbde580a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^7.1.3" "php": "^7.1.3"
}, },
"time": "2019-01-16T20:35:37+00:00", "time": "2019-02-23T15:42:05+00:00",
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {

View file

@ -15,11 +15,11 @@
} }
], ],
"require": { "require": {
"php": ">=5.6.0", "php": ">=7.1",
"nette/utils": "~2.4" "nette/utils": "^2.4 || ~3.0.0"
}, },
"require-dev": { "require-dev": {
"nette/tester": "~2.0", "nette/tester": "^2.0",
"tracy/tracy": "^2.3" "tracy/tracy": "^2.3"
}, },
"conflict": { "conflict": {
@ -31,7 +31,7 @@
"minimum-stability": "dev", "minimum-stability": "dev",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "2.4-dev" "dev-master": "2.5-dev"
} }
} }
} }

View file

@ -7,18 +7,31 @@ Nette Finder: Files Searching
[![Latest Stable Version](https://poser.pugx.org/nette/finder/v/stable)](https://github.com/nette/finder/releases) [![Latest Stable Version](https://poser.pugx.org/nette/finder/v/stable)](https://github.com/nette/finder/releases)
[![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/finder/blob/master/license.md) [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/finder/blob/master/license.md)
Class `Nette\Utils\Finder` makes browsing the directory structure really easy.
Introduction
------------
Nette Finder makes browsing the directory structure really easy.
Documentation can be found on the [website](https://doc.nette.org/finder).
If you like Nette, **[please make a donation now](https://nette.org/donate)**. Thank you!
All examples assume the following class alias is defined: Installation
------------
```php The recommended way to install is via Composer:
use Nette\Utils\Finder;
```
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? 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 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')`. 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 Filtering
---------- ---------
You can also filter results. For example by size. This way we will traverse the files of size between 100B and 200B: 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 Connection to Amazon S3
---------------------- -----------------------
It's possible to use custom streams, for example Zend_Service_Amazon_S3: It's possible to use custom streams, for example Zend_Service_Amazon_S3:

View file

@ -5,6 +5,8 @@
* Copyright (c) 2004 David Grudl (https://davidgrudl.com) * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/ */
declare(strict_types=1);
namespace Nette\Utils; namespace Nette\Utils;
use Nette; use Nette;
@ -26,6 +28,9 @@ class Finder implements \IteratorAggregate, \Countable
{ {
use Nette\SmartObject; use Nette\SmartObject;
/** @var callable extension methods */
private static $extMethods = [];
/** @var array */ /** @var array */
private $paths = []; private $paths = [];
@ -47,10 +52,10 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Begins search for files matching mask and all directories. * Begins search for files matching mask and all directories.
* @param mixed * @param string|string[] $masks
* @return static * @return static
*/ */
public static function find(...$masks) public static function find(...$masks): self
{ {
$masks = $masks && is_array($masks[0]) ? $masks[0] : $masks; $masks = $masks && is_array($masks[0]) ? $masks[0] : $masks;
return (new static)->select($masks, 'isDir')->select($masks, 'isFile'); return (new static)->select($masks, 'isDir')->select($masks, 'isFile');
@ -59,10 +64,10 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Begins search for files matching mask. * Begins search for files matching mask.
* @param mixed * @param string|string[] $masks
* @return static * @return static
*/ */
public static function findFiles(...$masks) public static function findFiles(...$masks): self
{ {
$masks = $masks && is_array($masks[0]) ? $masks[0] : $masks; $masks = $masks && is_array($masks[0]) ? $masks[0] : $masks;
return (new static)->select($masks, 'isFile'); return (new static)->select($masks, 'isFile');
@ -71,10 +76,10 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Begins search for directories matching mask. * Begins search for directories matching mask.
* @param mixed * @param string|string[] $masks
* @return static * @return static
*/ */
public static function findDirectories(...$masks) public static function findDirectories(...$masks): self
{ {
$masks = $masks && is_array($masks[0]) ? $masks[0] : $masks; $masks = $masks && is_array($masks[0]) ? $masks[0] : $masks;
return (new static)->select($masks, 'isDir'); return (new static)->select($masks, 'isDir');
@ -83,31 +88,27 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Creates filtering group by mask & type selector. * Creates filtering group by mask & type selector.
* @param array
* @param string
* @return static * @return static
*/ */
private function select($masks, $type) private function select(array $masks, string $type): self
{ {
$this->cursor = &$this->groups[]; $this->cursor = &$this->groups[];
$pattern = self::buildPattern($masks); $pattern = self::buildPattern($masks);
if ($type || $pattern) { $this->filter(function (RecursiveDirectoryIterator $file) use ($type, $pattern): bool {
$this->filter(function (RecursiveDirectoryIterator $file) use ($type, $pattern) {
return !$file->isDot() return !$file->isDot()
&& (!$type || $file->$type()) && $file->$type()
&& (!$pattern || preg_match($pattern, '/' . strtr($file->getSubPathName(), '\\', '/'))); && (!$pattern || preg_match($pattern, '/' . strtr($file->getSubPathName(), '\\', '/')));
}); });
}
return $this; return $this;
} }
/** /**
* Searchs in the given folder(s). * Searches in the given folder(s).
* @param string|array * @param string|string[] $paths
* @return static * @return static
*/ */
public function in(...$paths) public function in(...$paths): self
{ {
$this->maxDepth = 0; $this->maxDepth = 0;
return $this->from(...$paths); return $this->from(...$paths);
@ -115,11 +116,11 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Searchs recursively from the given folder(s). * Searches recursively from the given folder(s).
* @param string|array * @param string|string[] $paths
* @return static * @return static
*/ */
public function from(...$paths) public function from(...$paths): self
{ {
if ($this->paths) { if ($this->paths) {
throw new Nette\InvalidStateException('Directory to search has already been specified.'); 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. * Shows folder content prior to the folder.
* @return static * @return static
*/ */
public function childFirst() public function childFirst(): self
{ {
$this->order = RecursiveIteratorIterator::CHILD_FIRST; $this->order = RecursiveIteratorIterator::CHILD_FIRST;
return $this; return $this;
@ -143,10 +144,8 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Converts Finder pattern to regular expression. * Converts Finder pattern to regular expression.
* @param array
* @return string|null
*/ */
private static function buildPattern($masks) private static function buildPattern(array $masks): ?string
{ {
$pattern = []; $pattern = [];
foreach ($masks as $mask) { foreach ($masks as $mask) {
@ -174,9 +173,8 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Get the number of found files and/or directories. * Get the number of found files and/or directories.
* @return int
*/ */
public function count() public function count(): int
{ {
return iterator_count($this->getIterator()); return iterator_count($this->getIterator());
} }
@ -184,23 +182,20 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Returns iterator. * Returns iterator.
* @return \Iterator
*/ */
public function getIterator() public function getIterator(): \Iterator
{ {
if (!$this->paths) { if (!$this->paths) {
throw new Nette\InvalidStateException('Call in() or from() to specify directory to search.'); throw new Nette\InvalidStateException('Call in() or from() to specify directory to search.');
} elseif (count($this->paths) === 1) { } elseif (count($this->paths) === 1) {
return $this->buildIterator($this->paths[0]); return $this->buildIterator((string) $this->paths[0]);
} else { } else {
$iterator = new \AppendIterator(); $iterator = new \AppendIterator();
$iterator->append($workaround = new \ArrayIterator(['workaround PHP bugs #49104, #63077']));
foreach ($this->paths as $path) { foreach ($this->paths as $path) {
$iterator->append($this->buildIterator($path)); $iterator->append($this->buildIterator((string) $path));
} }
unset($workaround[0]);
return $iterator; return $iterator;
} }
} }
@ -208,18 +203,16 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Returns per-path iterator. * 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); $iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::FOLLOW_SYMLINKS);
if ($this->exclude) { 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()) { if (!$file->isDot() && !$file->isFile()) {
foreach ($this->exclude as $filter) { foreach ($this->exclude as $filter) {
if (!call_user_func($filter, $file)) { if (!$filter($file)) {
return false; return false;
} }
} }
@ -233,14 +226,14 @@ class Finder implements \IteratorAggregate, \Countable
$iterator->setMaxDepth($this->maxDepth); $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) { while ($file instanceof \OuterIterator) {
$file = $file->getInnerIterator(); $file = $file->getInnerIterator();
} }
foreach ($this->groups as $filters) { foreach ($this->groups as $filters) {
foreach ($filters as $filter) { foreach ($filters as $filter) {
if (!call_user_func($filter, $file)) { if (!$filter($file)) {
continue 2; continue 2;
} }
} }
@ -259,15 +252,15 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Restricts the search using mask. * Restricts the search using mask.
* Excludes directories from recursive traversing. * Excludes directories from recursive traversing.
* @param mixed * @param string|string[] $masks
* @return static * @return static
*/ */
public function exclude(...$masks) public function exclude(...$masks): self
{ {
$masks = $masks && is_array($masks[0]) ? $masks[0] : $masks; $masks = $masks && is_array($masks[0]) ? $masks[0] : $masks;
$pattern = self::buildPattern($masks); $pattern = self::buildPattern($masks);
if ($pattern) { if ($pattern) {
$this->filter(function (RecursiveDirectoryIterator $file) use ($pattern) { $this->filter(function (RecursiveDirectoryIterator $file) use ($pattern): bool {
return !preg_match($pattern, '/' . strtr($file->getSubPathName(), '\\', '/')); return !preg_match($pattern, '/' . strtr($file->getSubPathName(), '\\', '/'));
}); });
} }
@ -277,10 +270,10 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Restricts the search using callback. * Restricts the search using callback.
* @param callable function (RecursiveDirectoryIterator $file) * @param callable $callback function (RecursiveDirectoryIterator $file): bool
* @return static * @return static
*/ */
public function filter($callback) public function filter(callable $callback): self
{ {
$this->cursor[] = $callback; $this->cursor[] = $callback;
return $this; return $this;
@ -289,10 +282,9 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Limits recursion level. * Limits recursion level.
* @param int
* @return static * @return static
*/ */
public function limitDepth($depth) public function limitDepth(int $depth): self
{ {
$this->maxDepth = $depth; $this->maxDepth = $depth;
return $this; return $this;
@ -301,22 +293,21 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Restricts the search by size. * Restricts the search by size.
* @param string "[operator] [size] [unit]" example: >=10kB * @param string $operator "[operator] [size] [unit]" example: >=10kB
* @param int
* @return static * @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 (func_num_args() === 1) { // in $operator is predicate
if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?((?:\d*\.)?\d+)\s*(K|M|G|)B?\z#i', $operator, $matches)) { if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?((?:\d*\.)?\d+)\s*(K|M|G|)B?\z#i', $operator, $matches)) {
throw new Nette\InvalidArgumentException('Invalid size predicate format.'); 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]; static $units = ['' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9];
$size *= $units[strtolower($unit)]; $size *= $units[strtolower($unit)];
$operator = $operator ?: '='; $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); return self::compare($file->getSize(), $operator, $size);
}); });
} }
@ -324,21 +315,21 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Restricts the search by modified time. * Restricts the search by modified time.
* @param string "[operator] [date]" example: >1978-01-23 * @param string $operator "[operator] [date]" example: >1978-01-23
* @param mixed * @param string|int|\DateTimeInterface $date
* @return static * @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 (func_num_args() === 1) { // in $operator is predicate
if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?(.+)\z#i', $operator, $matches)) { if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?(.+)\z#i', $operator, $matches)) {
throw new Nette\InvalidArgumentException('Invalid date predicate format.'); throw new Nette\InvalidArgumentException('Invalid date predicate format.');
} }
list(, $operator, $date) = $matches; [, $operator, $date] = $matches;
$operator = $operator ?: '='; $operator = $operator ?: '=';
} }
$date = DateTime::from($date)->format('U'); $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); return self::compare($file->getMTime(), $operator, $date);
}); });
} }
@ -346,11 +337,8 @@ class Finder implements \IteratorAggregate, \Countable
/** /**
* Compares two values. * 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) { switch ($operator) {
case '>': case '>':
@ -377,17 +365,16 @@ class Finder implements \IteratorAggregate, \Countable
/********************* extension methods ****************d*g**/ /********************* 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 isset(self::$extMethods[$name])
return $callback($this, ...$args); ? (self::$extMethods[$name])($this, ...$args)
} : parent::__call($name, $args);
Nette\Utils\ObjectMixin::strictCall(__CLASS__, $name);
} }
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;
} }
} }

View file

@ -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. 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` - get rid of all `require`
- only necessary scripts are loaded
- requires no strict file naming conventions - requires no strict file naming conventions
- allows more classes in single file - 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! 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 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 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')`. 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)`. 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!

View file

@ -28,10 +28,10 @@ class RobotLoader
const RETRY_LIMIT = 3; const RETRY_LIMIT = 3;
/** @var array comma separated wildcards */ /** @var array */
public $ignoreDirs = ['.*', '*.old', '*.bak', '*.tmp', 'temp']; public $ignoreDirs = ['.*', '*.old', '*.bak', '*.tmp', 'temp'];
/** @var array comma separated wildcards */ /** @var array */
public $acceptFiles = ['*.php']; public $acceptFiles = ['*.php'];
/** @var bool */ /** @var bool */
@ -95,7 +95,7 @@ class RobotLoader
$missing = &$this->missing[$type]; $missing = &$this->missing[$type];
$missing++; $missing++;
if (!$this->refreshed && $missing <= self::RETRY_LIMIT) { if (!$this->refreshed && $missing <= self::RETRY_LIMIT) {
$this->refresh(); $this->refreshClasses();
$this->saveCache(); $this->saveCache();
} elseif ($info) { } elseif ($info) {
unset($this->classes[$type]); unset($this->classes[$type]);
@ -171,7 +171,8 @@ class RobotLoader
*/ */
public function rebuild() public function rebuild()
{ {
$this->refresh(); $this->classes = $this->missing = [];
$this->refreshClasses();
if ($this->tempDirectory) { if ($this->tempDirectory) {
$this->saveCache(); $this->saveCache();
} }
@ -179,12 +180,26 @@ class RobotLoader
/** /**
* Refreshes class list. * Refreshes class list cache.
* @return void * @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 = []; $files = [];
foreach ($this->classes as $class => $info) { foreach ($this->classes as $class => $info) {
$files[$info['file']]['time'] = $info['time']; $files[$info['file']]['time'] = $info['time'];
@ -193,7 +208,8 @@ class RobotLoader
$this->classes = []; $this->classes = [];
foreach ($this->scanPaths as $path) { 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(); $file = $file->getPathname();
if (isset($files[$file]) && $files[$file]['time'] == filemtime($file)) { if (isset($files[$file]) && $files[$file]['time'] == filemtime($file)) {
$classes = $files[$file]['classes']; $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) { ->filter(function (SplFileInfo $file) use (&$disallow) {
return !isset($disallow[str_replace('\\', '/', $file->getRealPath())]); return !isset($disallow[str_replace('\\', '/', $file->getRealPath())]);
}) })
@ -419,9 +436,7 @@ class RobotLoader
list($this->classes, $this->missing) = @include $file; // @ file may not exist list($this->classes, $this->missing) = @include $file; // @ file may not exist
if (!is_array($this->classes)) { if (!is_array($this->classes)) {
$this->classes = []; $this->rebuild();
$this->refresh();
$this->saveCache();
} }
flock($handle, LOCK_UN); flock($handle, LOCK_UN);

View file

@ -19,22 +19,22 @@ final class Versions
'jean85/pretty-package-versions' => '1.2@75c7effcf3f77501d0e0caa75111aff4daa0dd48', 'jean85/pretty-package-versions' => '1.2@75c7effcf3f77501d0e0caa75111aff4daa0dd48',
'nette/bootstrap' => 'v2.4.6@268816e3f1bb7426c3a4ceec2bd38a036b532543', 'nette/bootstrap' => 'v2.4.6@268816e3f1bb7426c3a4ceec2bd38a036b532543',
'nette/di' => 'v2.4.15@d0561b8f77e8ef2ed6d83328860e16c81a5a8649', '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/neon' => 'v3.0.0@cbff32059cbdd8720deccf9e9eace6ee516f02eb',
'nette/php-generator' => 'v3.2.1@9de4e093a130f7a1bd175198799ebc0efbac6924', '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', 'nette/utils' => 'v2.5.3@17b9f76f2abd0c943adfb556e56f2165460b15ce',
'nikic/php-parser' => 'v4.2.1@5221f49a608808c1e4d436df32884cbc1b821ac0', 'nikic/php-parser' => 'v4.2.1@5221f49a608808c1e4d436df32884cbc1b821ac0',
'ocramius/package-versions' => '1.4.0@a4d4b60d0e60da2487bd21a2c6ac089f85570dbb', 'ocramius/package-versions' => '1.4.0@a4d4b60d0e60da2487bd21a2c6ac089f85570dbb',
'phpstan/phpdoc-parser' => '0.3.1@2cc49f47c69b023eaf05b48e6529389893b13d74', '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', '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/contracts' => 'v1.0.2@1aa7ab2429c3d594dd70689604b5cf7421254cdf',
'symfony/finder' => 'v4.2.3@ef71816cbb264988bb57fe6a73f610888b9aa70c', 'symfony/finder' => 'v4.2.4@267b7002c1b70ea80db0833c3afe05f0fbde580a',
'symfony/polyfill-mbstring' => 'v1.10.0@c79c051f5b3a46be09205c73b80b346e4153e494', 'symfony/polyfill-mbstring' => 'v1.10.0@c79c051f5b3a46be09205c73b80b346e4153e494',
'thecodingmachine/phpstan-safe-rule' => 'v0.1.3@00f4845905feb5240ca62fb799e3c51ba85c9230', 'thecodingmachine/phpstan-safe-rule' => 'v0.1.3@00f4845905feb5240ca62fb799e3c51ba85c9230',
'__root__' => 'dev-master@04a956886ab327ddbe5eec546b911b9e55a0e5ef', '__root__' => 'dev-master@58cc098058143344a846f01a5d2252a45e2be9ba',
); );
private function __construct() private function __construct()

View file

@ -27,11 +27,12 @@ can be checked before you run the actual line.
<a href="https://mike-pretzlaw.de/"><img src="https://i.imgur.com/TW2US6H.png" alt="Mike Pretzlaw" width="247" height="64"></a> <a href="https://mike-pretzlaw.de/"><img src="https://i.imgur.com/TW2US6H.png" alt="Mike Pretzlaw" width="247" height="64"></a>
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;
<a href="https://coders.thecodingmachine.com/phpstan"><img src="https://i.imgur.com/kQhNOTP.png" alt="TheCodingMachine" width="247" height="64"></a> <a href="https://coders.thecodingmachine.com/phpstan"><img src="https://i.imgur.com/kQhNOTP.png" alt="TheCodingMachine" width="247" height="64"></a>
&nbsp;&nbsp;&nbsp;
<a href="https://www.wispay.io/t/JdL" target="_blank"><img src="https://assets.wispay.io/wgt2_d_o.png" width="247" height="78"></a>
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. 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
<br>LTC: LSU5xLsWEfrVx1P9yJwmhziHAXikiE8xtC
## Prerequisites ## 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 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) * [Yii2](https://github.com/proget-hq/phpstan-yii2)
* [PhpSpec](https://github.com/proget-hq/phpstan-phpspec) * [PhpSpec](https://github.com/proget-hq/phpstan-phpspec)
* [TYPO3](https://github.com/sascha-egerer/phpstan-typo3) * [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! 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. - `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 - `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. - `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 ## Class reflection extensions

View file

@ -21,7 +21,8 @@ rules:
- PHPStan\Rules\Functions\PrintfParametersRule - PHPStan\Rules\Functions\PrintfParametersRule
- PHPStan\Rules\Functions\UnusedClosureUsesRule - PHPStan\Rules\Functions\UnusedClosureUsesRule
- PHPStan\Rules\Methods\ExistingClassesInTypehintsRule - PHPStan\Rules\Methods\ExistingClassesInTypehintsRule
- PHPStan\Rules\Properties\AccessStaticPropertiesRule - PHPStan\Rules\Properties\AccessPropertiesInAssignRule
- PHPStan\Rules\Properties\AccessStaticPropertiesInAssignRule
- PHPStan\Rules\Variables\ThisVariableRule - PHPStan\Rules\Variables\ThisVariableRule
services: services:
@ -90,6 +91,11 @@ services:
arguments: arguments:
reportMagicProperties: %reportMagicProperties% reportMagicProperties: %reportMagicProperties%
-
class: PHPStan\Rules\Properties\AccessStaticPropertiesRule
tags:
- phpstan.rules.rule
- -
class: PHPStan\Rules\Properties\ExistingClassesInPropertiesRule class: PHPStan\Rules\Properties\ExistingClassesInPropertiesRule
tags: tags:

View file

@ -379,6 +379,11 @@ services:
tags: tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension - phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: PHPStan\Type\Php\FilterVarDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
- -
class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension
tags: tags:

View file

@ -0,0 +1,37 @@
<?php declare(strict_types = 1);
namespace PHPStan\Analyser;
class EnsuredNonNullabilityResult
{
/** @var Scope */
private $scope;
/** @var EnsuredNonNullabilityResultExpression[] */
private $specifiedExpressions;
/**
* @param Scope $scope
* @param EnsuredNonNullabilityResultExpression[] $specifiedExpressions
*/
public function __construct(Scope $scope, array $specifiedExpressions)
{
$this->scope = $scope;
$this->specifiedExpressions = $specifiedExpressions;
}
public function getScope(): Scope
{
return $this->scope;
}
/**
* @return EnsuredNonNullabilityResultExpression[]
*/
public function getSpecifiedExpressions(): array
{
return $this->specifiedExpressions;
}
}

View file

@ -0,0 +1,33 @@
<?php declare(strict_types = 1);
namespace PHPStan\Analyser;
use PhpParser\Node\Expr;
use PHPStan\Type\Type;
class EnsuredNonNullabilityResultExpression
{
/** @var Expr */
private $expression;
/** @var Type */
private $originalType;
public function __construct(Expr $expression, Type $originalType)
{
$this->expression = $expression;
$this->originalType = $originalType;
}
public function getExpression(): Expr
{
return $this->expression;
}
public function getOriginalType(): Type
{
return $this->originalType;
}
}

View file

@ -0,0 +1,69 @@
<?php declare(strict_types = 1);
namespace PHPStan\Analyser;
use PHPStan\Type\Type;
class ExpressionContext
{
/** @var bool */
private $isDeep;
/** @var string|null */
private $inAssignRightSideVariableName;
/** @var Type|null */
private $inAssignRightSideType;
private function __construct(
bool $isDeep,
?string $inAssignRightSideVariableName,
?Type $inAssignRightSideType
)
{
$this->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;
}
}

View file

@ -0,0 +1,74 @@
<?php declare(strict_types = 1);
namespace PHPStan\Analyser;
class ExpressionResult
{
/** @var Scope */
private $scope;
/** @var (callable(): Scope)|null */
private $truthyScopeCallback;
/** @var Scope|null */
private $truthyScope;
/** @var (callable(): Scope)|null */
private $falseyScopeCallback;
/** @var Scope|null */
private $falseyScope;
/**
* @param Scope $scope
* @param (callable(): Scope)|null $truthyScopeCallback
* @param (callable(): Scope)|null $falseyScopeCallback
*/
public function __construct(
Scope $scope,
?callable $truthyScopeCallback = null,
?callable $falseyScopeCallback = null
)
{
$this->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;
}
}

View file

@ -1,119 +0,0 @@
<?php declare(strict_types = 1);
namespace PHPStan\Analyser;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\Continue_;
class LookForAssignsSettings
{
private const EARLY_TERMINATION_CONTINUE = 1;
private const EARLY_TERMINATION_BREAK = 2;
private const EARLY_TERMINATION_STOP = 4;
private const EARLY_TERMINATION_ALL = self::EARLY_TERMINATION_CONTINUE
+ self::EARLY_TERMINATION_BREAK
+ self::EARLY_TERMINATION_STOP;
private const EARLY_TERMINATION_CLOSURE = 8;
private const REPEAT_ANALYSIS = 16;
/** @var int */
private $respectEarlyTermination;
/** @var self[] */
private static $registry = [];
private function __construct(
int $respectEarlyTermination
)
{
$this->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;
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -60,6 +60,31 @@ class ScopeContext
return new self($this->file, $this->classReflection, $traitReflection); 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 public function getFile(): string
{ {
return $this->file; return $this->file;

View file

@ -51,7 +51,7 @@ class ScopeFactory
* @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|null $inFunctionCall * @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|null $inFunctionCall
* @param bool $negated * @param bool $negated
* @param bool $inFirstLevelStatement * @param bool $inFirstLevelStatement
* @param string[] $currentlyAssignedExpressions * @param array<string, true> $currentlyAssignedExpressions
* *
* @return Scope * @return Scope
*/ */

View file

@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace PHPStan\Analyser;
use PhpParser\Node\Stmt;
class StatementExitPoint
{
/** @var Stmt */
private $statement;
/** @var Scope */
private $scope;
public function __construct(Stmt $statement, Scope $scope)
{
$this->statement = $statement;
$this->scope = $scope;
}
public function getStatement(): Stmt
{
return $this->statement;
}
public function getScope(): Scope
{
return $this->scope;
}
}

View file

@ -1,73 +0,0 @@
<?php declare(strict_types = 1);
namespace PHPStan\Analyser;
class StatementList
{
/** @var \PHPStan\Analyser\Scope */
private $scope;
/** @var \PhpParser\Node[] */
private $statements;
/** @var bool */
private $filterByTruthyValue;
/** @var callable(Scope $scope): Scope|null */
private $processScope;
/**
* @param Scope $scope
* @param \PhpParser\Node[] $statements
* @param bool $filterByTruthyValue
* @param callable(Scope $scope): Scope|null $processScope
*/
public function __construct(
Scope $scope,
array $statements,
bool $filterByTruthyValue = false,
?callable $processScope = null
)
{
$this->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;
}
}

View file

@ -0,0 +1,110 @@
<?php declare(strict_types = 1);
namespace PHPStan\Analyser;
use PhpParser\Node\Stmt;
class StatementResult
{
/** @var Scope */
private $scope;
/** @var Stmt[] */
private $alwaysTerminatingStatements;
/** @var StatementExitPoint[] */
private $exitPoints;
/**
* @param Scope $scope
* @param Stmt[] $alwaysTerminatingStatements
* @param StatementExitPoint[] $exitPoints
*/
public function __construct(
Scope $scope,
array $alwaysTerminatingStatements,
array $exitPoints
)
{
$this->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;
}
}

View file

@ -20,13 +20,16 @@ use PhpParser\Node\Expr\StaticPropertyFetch;
use PhpParser\Node\Name; use PhpParser\Node\Name;
use PHPStan\Broker\Broker; use PHPStan\Broker\Broker;
use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasOffsetType;
use PHPStan\Type\Accessory\HasPropertyType;
use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\ArrayType; use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantFloatType;
use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType; use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType; use PHPStan\Type\NeverType;
use PHPStan\Type\NonexistentParentClassType; use PHPStan\Type\NonexistentParentClassType;
@ -102,6 +105,10 @@ class TypeSpecifier
): SpecifiedTypes ): SpecifiedTypes
{ {
if ($expr instanceof Instanceof_) { if ($expr instanceof Instanceof_) {
$exprNode = $expr->expr;
if ($exprNode instanceof Expr\Assign) {
$exprNode = $exprNode->var;
}
if ($expr->class instanceof Name) { if ($expr->class instanceof Name) {
$className = (string) $expr->class; $className = (string) $expr->class;
$lowercasedClassName = strtolower($className); $lowercasedClassName = strtolower($className);
@ -121,17 +128,20 @@ class TypeSpecifier
} else { } else {
$type = new ObjectType($className); $type = new ObjectType($className);
} }
return $this->create($expr->expr, $type, $context); return $this->create($exprNode, $type, $context);
} }
if ($context->true()) { 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) { } elseif ($expr instanceof Node\Expr\BinaryOp\Identical) {
$expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr); $expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr);
if ($expressions !== null) { if ($expressions !== null) {
/** @var Expr $exprNode */ /** @var Expr $exprNode */
$exprNode = $expressions[0]; $exprNode = $expressions[0];
if ($exprNode instanceof Expr\Assign) {
$exprNode = $exprNode->var;
}
/** @var \PHPStan\Type\ConstantScalarType $constantType */ /** @var \PHPStan\Type\ConstantScalarType $constantType */
$constantType = $expressions[1]; $constantType = $expressions[1];
if ($constantType->getValue() === false) { 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) { } elseif ($expr instanceof Node\Expr\BinaryOp\NotEqual) {
return $this->specifyTypesInCondition( return $this->specifyTypesInCondition(
$scope, $scope,
@ -373,6 +443,10 @@ class TypeSpecifier
} }
} }
if (count($vars) === 0) {
throw new \PHPStan\ShouldNotHappenException();
}
$types = null; $types = null;
foreach ($vars as $var) { foreach ($vars as $var) {
if ($expr instanceof Expr\Isset_) { if ($expr instanceof Expr\Isset_) {
@ -401,6 +475,26 @@ class TypeSpecifier
TypeSpecifierContext::createFalse() 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) { if ($types === null) {
$types = $type; $types = $type;
} else { } else {
@ -408,9 +502,6 @@ class TypeSpecifier
} }
} }
/** @var SpecifiedTypes $types */
$types = $types;
if ( if (
$expr instanceof Expr\Empty_ $expr instanceof Expr\Empty_
&& (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($scope->getType($expr->expr))->yes()) { && (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 public function create(Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes
{ {
if ($expr instanceof New_) { if ($expr instanceof New_ || $expr instanceof Instanceof_) {
return new SpecifiedTypes(); return new SpecifiedTypes();
} }
@ -500,7 +591,7 @@ class TypeSpecifier
/** /**
* @return \PHPStan\Type\FunctionTypeSpecifyingExtension[] * @return \PHPStan\Type\FunctionTypeSpecifyingExtension[]
*/ */
public function getFunctionTypeSpecifyingExtensions(): array private function getFunctionTypeSpecifyingExtensions(): array
{ {
return $this->functionTypeSpecifyingExtensions; return $this->functionTypeSpecifyingExtensions;
} }
@ -509,7 +600,7 @@ class TypeSpecifier
* @param string $className * @param string $className
* @return \PHPStan\Type\MethodTypeSpecifyingExtension[] * @return \PHPStan\Type\MethodTypeSpecifyingExtension[]
*/ */
public function getMethodTypeSpecifyingExtensionsForClass(string $className): array private function getMethodTypeSpecifyingExtensionsForClass(string $className): array
{ {
if ($this->methodTypeSpecifyingExtensionsByClass === null) { if ($this->methodTypeSpecifyingExtensionsByClass === null) {
$byClass = []; $byClass = [];
@ -526,7 +617,7 @@ class TypeSpecifier
* @param string $className * @param string $className
* @return \PHPStan\Type\StaticMethodTypeSpecifyingExtension[] * @return \PHPStan\Type\StaticMethodTypeSpecifyingExtension[]
*/ */
public function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array private function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array
{ {
if ($this->staticMethodTypeSpecifyingExtensionsByClass === null) { if ($this->staticMethodTypeSpecifyingExtensionsByClass === null) {
$byClass = []; $byClass = [];

View file

@ -36,8 +36,13 @@ class VariableTypeHolder
public function and(self $other): self 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( return new self(
TypeCombinator::union($this->getType(), $other->getType()), $type,
$this->getCertainty()->and($other->getCertainty()) $this->getCertainty()->and($other->getCertainty())
); );
} }

View file

@ -24,11 +24,11 @@ class AnonymousClassNameHelper
} }
public function getAnonymousClassName( public function getAnonymousClassName(
\PhpParser\Node\Expr\New_ $node, \PhpParser\Node\Stmt\Class_ $classNode,
string $filename string $filename
): string ): string
{ {
if (!$node->class instanceof \PhpParser\Node\Stmt\Class_) { if (isset($classNode->namespacedName)) {
throw new \PHPStan\ShouldNotHappenException(); throw new \PHPStan\ShouldNotHappenException();
} }
@ -38,7 +38,7 @@ class AnonymousClassNameHelper
return sprintf( return sprintf(
'AnonymousClass%s', 'AnonymousClass%s',
md5(sprintf('%s:%s', $filename, $node->class->getLine())) md5(sprintf('%s:%s', $filename, $classNode->getLine()))
); );
} }

View file

@ -269,11 +269,11 @@ class Broker
} }
public function getAnonymousClassReflection( public function getAnonymousClassReflection(
\PhpParser\Node\Expr\New_ $node, \PhpParser\Node\Stmt\Class_ $classNode,
Scope $scope Scope $scope
): ClassReflection ): ClassReflection
{ {
if (!$node->class instanceof \PhpParser\Node\Stmt\Class_) { if (isset($classNode->namespacedName)) {
throw new \PHPStan\ShouldNotHappenException(); throw new \PHPStan\ShouldNotHappenException();
} }
@ -289,22 +289,20 @@ class Broker
$filename = $this->relativePathHelper->getRelativePath($scopeFile); $filename = $this->relativePathHelper->getRelativePath($scopeFile);
$className = $this->anonymousClassNameHelper->getAnonymousClassName( $className = $this->anonymousClassNameHelper->getAnonymousClassName(
$node, $classNode,
$filename $filename
); );
$classNode->name = new \PhpParser\Node\Identifier($className);
if (isset(self::$anonymousClasses[$className])) { if (isset(self::$anonymousClasses[$className])) {
return self::$anonymousClasses[$className]; return self::$anonymousClasses[$className];
} }
$classNode = $node->class;
$classNode->name = new \PhpParser\Node\Identifier($className);
eval($this->printer->prettyPrint([$classNode])); eval($this->printer->prettyPrint([$classNode]));
unset($classNode);
self::$anonymousClasses[$className] = $this->getClassFromReflection( self::$anonymousClasses[$className] = $this->getClassFromReflection(
new \ReflectionClass('\\' . $className), new \ReflectionClass('\\' . $className),
sprintf('class@anonymous/%s:%s', $filename, $node->getLine()), sprintf('class@anonymous/%s:%s', $filename, $classNode->getLine()),
$scopeFile $scopeFile
); );
$this->classReflections[$className] = self::$anonymousClasses[$className]; $this->classReflections[$className] = self::$anonymousClasses[$className];

View file

@ -173,6 +173,11 @@ class ParametersAcceptorSelector
*/ */
public static function combineAcceptors(array $acceptors): ParametersAcceptor 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) { if (count($acceptors) === 1) {
return $acceptors[0]; return $acceptors[0];
} }
@ -236,9 +241,6 @@ class ParametersAcceptorSelector
} }
} }
/** @var \PHPStan\Type\Type $returnType */
$returnType = $returnType;
return new FunctionVariant($parameters, $isVariadic, $returnType); return new FunctionVariant($parameters, $isVariadic, $returnType);
} }

View file

@ -2174,7 +2174,7 @@ return [
'Ds\Set::contains' => ['bool', '...values='=>'mixed'], 'Ds\Set::contains' => ['bool', '...values='=>'mixed'],
'Ds\Set::diff' => ['Ds\Set', 'set'=>'Ds\Set'], 'Ds\Set::diff' => ['Ds\Set', 'set'=>'Ds\Set'],
'Ds\Set::filter' => ['Ds\Set', 'callback='=>'callable'], 'Ds\Set::filter' => ['Ds\Set', 'callback='=>'callable'],
'Ds\Set::first' => ['void'], 'Ds\Set::first' => ['mixed'],
'Ds\Set::get' => ['mixed', 'index'=>'int'], 'Ds\Set::get' => ['mixed', 'index'=>'int'],
'Ds\Set::intersect' => ['Ds\Set', 'set'=>'Ds\Set'], 'Ds\Set::intersect' => ['Ds\Set', 'set'=>'Ds\Set'],
'Ds\Set::join' => ['void', 'glue='=>'string'], 'Ds\Set::join' => ['void', 'glue='=>'string'],

View file

@ -96,8 +96,8 @@ class InvalidBinaryOperationRule implements \PHPStan\Rules\Rule
} }
$scope = $scope $scope = $scope
->assignVariable($leftName, $leftType, \PHPStan\TrinaryLogic::createYes()) ->assignVariable($leftName, $leftType)
->assignVariable($rightName, $rightType, \PHPStan\TrinaryLogic::createYes()); ->assignVariable($rightName, $rightType);
if (!$scope->getType($newNode) instanceof ErrorType) { if (!$scope->getType($newNode) instanceof ErrorType) {
return []; return [];

View file

@ -0,0 +1,39 @@
<?php declare(strict_types = 1);
namespace PHPStan\Rules\Properties;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
class AccessPropertiesInAssignRule implements Rule
{
/** @var \PHPStan\Rules\Properties\AccessPropertiesRule */
private $accessPropertiesRule;
public function __construct(AccessPropertiesRule $accessPropertiesRule)
{
$this->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);
}
}

View file

@ -63,6 +63,10 @@ class AccessPropertiesRule implements \PHPStan\Rules\Rule
return $typeResult->getUnknownClassErrors(); return $typeResult->getUnknownClassErrors();
} }
if ($scope->isInExpressionAssign($node)) {
return [];
}
if (!$type->canAccessProperties()->yes()) { if (!$type->canAccessProperties()->yes()) {
return [ return [
sprintf('Cannot access property $%s on %s.', $name, $type->describe(VerbosityLevel::typeOnly())), sprintf('Cannot access property $%s on %s.', $name, $type->describe(VerbosityLevel::typeOnly())),

View file

@ -0,0 +1,39 @@
<?php declare(strict_types = 1);
namespace PHPStan\Rules\Properties;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
class AccessStaticPropertiesInAssignRule implements Rule
{
/** @var \PHPStan\Rules\Properties\AccessStaticPropertiesRule */
private $accessStaticPropertiesRule;
public function __construct(AccessStaticPropertiesRule $accessStaticPropertiesRule)
{
$this->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);
}
}

View file

@ -144,6 +144,10 @@ class AccessStaticPropertiesRule implements \PHPStan\Rules\Rule
$typeForDescribe = $classType; $typeForDescribe = $classType;
$classType = TypeCombinator::remove($classType, new StringType()); $classType = TypeCombinator::remove($classType, new StringType());
if ($scope->isInExpressionAssign($node)) {
return [];
}
if (!$classType->canAccessProperties()->yes()) { if (!$classType->canAccessProperties()->yes()) {
return array_merge($messages, [ return array_merge($messages, [
sprintf('Cannot access static property $%s on %s.', $name, $typeForDescribe->describe(VerbosityLevel::typeOnly())), sprintf('Cannot access static property $%s on %s.', $name, $typeForDescribe->describe(VerbosityLevel::typeOnly())),

View file

@ -10,7 +10,6 @@ use PHPStan\TrinaryLogic;
use PHPStan\Type\ArrayType; use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType; use PHPStan\Type\BooleanType;
use PHPStan\Type\CompoundType; use PHPStan\Type\CompoundType;
use PHPStan\Type\ConstantScalarType;
use PHPStan\Type\ConstantType; use PHPStan\Type\ConstantType;
use PHPStan\Type\ErrorType; use PHPStan\Type\ErrorType;
use PHPStan\Type\MixedType; use PHPStan\Type\MixedType;
@ -360,7 +359,8 @@ class ConstantArrayType extends ArrayType implements ConstantType
if (!$preserveKeys) { if (!$preserveKeys) {
$i = 0; $i = 0;
$keyTypes = array_map(static function (ConstantScalarType $keyType) use (&$i): ConstantScalarType { /** @var array<int, ConstantIntegerType|ConstantStringType> $keyTypes */
$keyTypes = array_map(static function ($keyType) use (&$i) {
if ($keyType instanceof ConstantIntegerType) { if ($keyType instanceof ConstantIntegerType) {
$i++; $i++;
return new ConstantIntegerType($i - 1); return new ConstantIntegerType($i - 1);
@ -370,12 +370,14 @@ class ConstantArrayType extends ArrayType implements ConstantType
}, $keyTypes); }, $keyTypes);
} }
/** @var int|float $nextAutoIndex */
$nextAutoIndex = 0; $nextAutoIndex = 0;
foreach ($keyTypes as $keyType) { foreach ($keyTypes as $keyType) {
if (!$keyType instanceof ConstantIntegerType) { if (!$keyType instanceof ConstantIntegerType) {
continue; continue;
} }
/** @var int|float $nextAutoIndex */
$nextAutoIndex = max($nextAutoIndex, $keyType->getValue() + 1); $nextAutoIndex = max($nextAutoIndex, $keyType->getValue() + 1);
} }

View file

@ -178,10 +178,7 @@ class FileTypeMapper
throw new \PHPStan\ShouldNotHappenException(); throw new \PHPStan\ShouldNotHappenException();
} }
$className = $this->anonymousClassNameHelper->getAnonymousClassName( $className = $this->anonymousClassNameHelper->getAnonymousClassName($node, $fileName);
new Node\Expr\New_($node),
$fileName
);
} else { } else {
$className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\'); $className = ltrim(sprintf('%s\\%s', $namespace, $node->name->name), '\\');
} }
@ -198,6 +195,9 @@ class FileTypeMapper
if ($traitReflection->getFileName() === false) { if ($traitReflection->getFileName() === false) {
continue; continue;
} }
if (!file_exists($traitReflection->getFileName())) {
continue;
}
$className = $classStack[count($classStack) - 1] ?? null; $className = $classStack[count($classStack) - 1] ?? null;
if ($className === null) { if ($className === null) {

View file

@ -20,10 +20,7 @@ class ObjectType implements TypeWithClassName
use TruthyBooleanTypeTrait; use TruthyBooleanTypeTrait;
private const EXTRA_OFFSET_CLASSES = [ private const EXTRA_OFFSET_CLASSES = ['SimpleXMLElement', 'DOMNodeList'];
'SimpleXMLElement' => true,
'DOMNodeList' => true,
];
/** @var string */ /** @var string */
private $className; private $className;
@ -454,9 +451,14 @@ class ObjectType implements TypeWithClassName
$classReflection = $broker->getClass($this->className); $classReflection = $broker->getClass($this->className);
if (array_key_exists($classReflection->getName(), self::EXTRA_OFFSET_CLASSES)) { foreach (self::EXTRA_OFFSET_CLASSES as $extraOffsetClass) {
if ($classReflection->getName() === $extraOffsetClass) {
return TrinaryLogic::createYes(); return TrinaryLogic::createYes();
} }
if ($classReflection->isSubclassOf($extraOffsetClass)) {
return TrinaryLogic::createYes();
}
}
return TrinaryLogic::createNo(); return TrinaryLogic::createNo();
} }

View file

@ -8,7 +8,6 @@ use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\Return_; use PhpParser\Node\Stmt\Return_;
use PHPStan\Analyser\Scope; use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\FunctionReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ArrayType; use PHPStan\Type\ArrayType;
use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantArrayType;
@ -63,7 +62,7 @@ class ArrayFilterFunctionReturnTypeReturnTypeExtension implements \PHPStan\Type\
throw new \PHPStan\ShouldNotHappenException(); throw new \PHPStan\ShouldNotHappenException();
} }
$itemVariableName = $callbackArg->params[0]->var->name; $itemVariableName = $callbackArg->params[0]->var->name;
$scope = $scope->assignVariable($itemVariableName, $itemType, TrinaryLogic::createYes()); $scope = $scope->assignVariable($itemVariableName, $itemType);
$scope = $scope->filterByTruthyValue($statement->expr); $scope = $scope->filterByTruthyValue($statement->expr);
$itemType = $scope->getVariableType($itemVariableName); $itemType = $scope->getVariableType($itemVariableName);
} }

View file

@ -61,13 +61,6 @@ class ArraySliceFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunc
$constantArrays = TypeUtils::getConstantArrays($valueType); $constantArrays = TypeUtils::getConstantArrays($valueType);
if (count($constantArrays) === 0) { if (count($constantArrays) === 0) {
if (!$valueType instanceof ArrayType) {
return new ArrayType(
new MixedType(),
new MixedType()
);
}
return $valueType; return $valueType;
} }

View file

@ -0,0 +1,80 @@
<?php declare(strict_types = 1);
namespace PHPStan\Type\Php;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
class FilterVarDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{
/** @var array<string, Type> */
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;
}
}

View file

@ -77,7 +77,13 @@ class StaticType implements StaticResolvableType, TypeWithClassName
public function equals(Type $type): bool 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 public function describe(VerbosityLevel $level): string

View file

@ -77,9 +77,13 @@ class TypeCombinator
public static function removeNull(Type $type): Type public static function removeNull(Type $type): Type
{ {
if (self::containsNull($type)) {
return self::remove($type, new NullType()); return self::remove($type, new NullType());
} }
return $type;
}
public static function containsNull(Type $type): bool public static function containsNull(Type $type): bool
{ {
if ($type instanceof UnionType) { if ($type instanceof UnionType) {
@ -146,14 +150,14 @@ class TypeCombinator
continue; continue;
} }
if ($innerType instanceof AccessoryType || $innerType instanceof CallableType) { if ($innerType instanceof AccessoryType || $innerType instanceof CallableType) {
$intermediateAccessoryTypes[] = $innerType; $intermediateAccessoryTypes[$innerType->describe(VerbosityLevel::precise())] = $innerType;
continue; continue;
} }
} }
if ($intermediateArrayType !== null) { if ($intermediateArrayType !== null) {
$arrayTypes[] = $intermediateArrayType; $arrayTypes[] = $intermediateArrayType;
$arrayAccessoryTypes = array_merge($arrayAccessoryTypes, $intermediateAccessoryTypes); $arrayAccessoryTypes[] = $intermediateAccessoryTypes;
unset($types[$i]); unset($types[$i]);
continue; continue;
} }
@ -163,14 +167,25 @@ class TypeCombinator
} }
$arrayTypes[] = $types[$i]; $arrayTypes[] = $types[$i];
$arrayAccessoryTypes[] = [];
unset($types[$i]); unset($types[$i]);
} }
/** @var ArrayType[] $arrayTypes */ /** @var ArrayType[] $arrayTypes */
$arrayTypes = $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( $types = array_values(
array_merge($types, self::processArrayTypes($arrayTypes, $arrayAccessoryTypes)) array_merge(
$types,
self::processArrayTypes($arrayTypes, $arrayAccessoryTypesToProcess)
)
); );
// simplify string[] | int[] to (string|int)[] // simplify string[] | int[] to (string|int)[]
@ -322,18 +337,18 @@ class TypeCombinator
$constantKeyTypesNumbered = $constantKeyTypesNumbered; $constantKeyTypesNumbered = $constantKeyTypesNumbered;
$constantArraysBuckets = []; $constantArraysBuckets = [];
foreach ($arrayTypes as $arrayType) { foreach ($arrayTypes as $arrayTypeAgain) {
$arrayIndex = 0; $arrayIndex = 0;
foreach ($arrayType->getKeyTypes() as $keyType) { foreach ($arrayTypeAgain->getKeyTypes() as $keyType) {
$arrayIndex += $constantKeyTypesNumbered[$keyType->getValue()]; $arrayIndex += $constantKeyTypesNumbered[$keyType->getValue()];
} }
if (!array_key_exists($arrayIndex, $constantArraysBuckets)) { if (!array_key_exists($arrayIndex, $constantArraysBuckets)) {
$bucket = []; $bucket = [];
foreach ($arrayType->getKeyTypes() as $i => $keyType) { foreach ($arrayTypeAgain->getKeyTypes() as $i => $keyType) {
$bucket[$keyType->getValue()] = [ $bucket[$keyType->getValue()] = [
'keyType' => $keyType, 'keyType' => $keyType,
'valueType' => $arrayType->getValueTypes()[$i], 'valueType' => $arrayTypeAgain->getValueTypes()[$i],
]; ];
} }
$constantArraysBuckets[$arrayIndex] = $bucket; $constantArraysBuckets[$arrayIndex] = $bucket;
@ -341,10 +356,10 @@ class TypeCombinator
} }
$bucket = $constantArraysBuckets[$arrayIndex]; $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'] = self::union(
$bucket[$keyType->getValue()]['valueType'], $bucket[$keyType->getValue()]['valueType'],
$arrayType->getValueTypes()[$i] $arrayTypeAgain->getValueTypes()[$i]
); );
} }

View file

@ -45,6 +45,24 @@ class TypeUtils
return self::map(ConstantType::class, $type, false); 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 public static function generalizeType(Type $type): Type
{ {
if ($type instanceof ConstantType) { if ($type instanceof ConstantType) {
@ -97,12 +115,14 @@ class TypeUtils
* @param string $typeClass * @param string $typeClass
* @param Type $type * @param Type $type
* @param bool $inspectIntersections * @param bool $inspectIntersections
* @param bool $stopOnUnmatched
* @return mixed[] * @return mixed[]
*/ */
private static function map( private static function map(
string $typeClass, string $typeClass,
Type $type, Type $type,
bool $inspectIntersections bool $inspectIntersections,
bool $stopOnUnmatched = true
): array ): array
{ {
if ($type instanceof $typeClass) { if ($type instanceof $typeClass) {
@ -113,9 +133,13 @@ class TypeUtils
$matchingTypes = []; $matchingTypes = [];
foreach ($type->getTypes() as $innerType) { foreach ($type->getTypes() as $innerType) {
if (!$innerType instanceof $typeClass) { if (!$innerType instanceof $typeClass) {
if ($stopOnUnmatched) {
return []; return [];
} }
continue;
}
$matchingTypes[] = $innerType; $matchingTypes[] = $innerType;
} }
@ -126,6 +150,10 @@ class TypeUtils
$matchingTypes = []; $matchingTypes = [];
foreach ($type->getTypes() as $innerType) { foreach ($type->getTypes() as $innerType) {
if (!$innerType instanceof $typeClass) { if (!$innerType instanceof $typeClass) {
if ($stopOnUnmatched) {
return [];
}
continue; continue;
} }

View file

@ -199,6 +199,13 @@ class Application
return 0; 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); $name = $this->getCommandName($input);
if (true === $input->hasParameterOption(['--help', '-h'], true)) { if (true === $input->hasParameterOption(['--help', '-h'], true)) {
if (!$name) { if (!$name) {

View file

@ -381,20 +381,17 @@ final class ProgressBar
$lines = floor(Helper::strlen($message) / $this->terminal->getWidth()) + $this->formatLineCount + 1; $lines = floor(Helper::strlen($message) / $this->terminal->getWidth()) + $this->formatLineCount + 1;
$this->output->clear($lines); $this->output->clear($lines);
} else { } 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 // Erase previous lines
if ($this->formatLineCount > 0) { 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) { } elseif ($this->step > 0) {
$this->output->writeln(''); $message = PHP_EOL.$message;
} }
$this->firstRun = false; $this->firstRun = false;

View file

@ -126,7 +126,7 @@ class QuestionHelper extends Helper
if (false === $ret) { if (false === $ret) {
$ret = fgets($inputStream, 4096); $ret = fgets($inputStream, 4096);
if (false === $ret) { if (false === $ret) {
throw new RuntimeException('Aborted'); throw new RuntimeException('Aborted.');
} }
$ret = trim($ret); $ret = trim($ret);
} }
@ -213,8 +213,10 @@ class QuestionHelper extends Helper
while (!feof($inputStream)) { while (!feof($inputStream)) {
$c = fread($inputStream, 1); $c = fread($inputStream, 1);
// Backspace Character // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false.
if ("\177" === $c) { if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) {
throw new RuntimeException('Aborted.');
} elseif ("\177" === $c) { // Backspace Character
if (0 === $numMatches && 0 !== $i) { if (0 === $numMatches && 0 !== $i) {
--$i; --$i;
// Move cursor backwards // Move cursor backwards
@ -267,6 +269,10 @@ class QuestionHelper extends Helper
continue; continue;
} else { } else {
if ("\x80" <= $c) {
$c .= fread($inputStream, ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3][$c & "\xF0"]);
}
$output->write($c); $output->write($c);
$ret .= $c; $ret .= $c;
++$i; ++$i;
@ -339,7 +345,7 @@ class QuestionHelper extends Helper
shell_exec(sprintf('stty %s', $sttyMode)); shell_exec(sprintf('stty %s', $sttyMode));
if (false === $value) { if (false === $value) {
throw new RuntimeException('Aborted'); throw new RuntimeException('Aborted.');
} }
$value = trim($value); $value = trim($value);

View file

@ -257,8 +257,27 @@ class ArgvInput extends Input
*/ */
public function getFirstArgument() public function getFirstArgument()
{ {
foreach ($this->tokens as $token) { $isOption = false;
foreach ($this->tokens as $i => $token) {
if ($token && '-' === $token[0]) { 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; continue;
} }

View file

@ -19,7 +19,7 @@ use Symfony\Component\Console\Exception\InvalidOptionException;
* *
* Usage: * Usage:
* *
* $input = new ArrayInput(['name' => 'foo', '--bar' => 'foobar']); * $input = new ArrayInput(['command' => 'foo:bar', 'foo' => 'bar', '--bar' => 'foobar']);
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */

View file

@ -69,7 +69,7 @@ abstract class Input implements InputInterface, StreamableInputInterface
$givenArguments = $this->arguments; $givenArguments = $this->arguments;
$missingArguments = array_filter(array_keys($definition->getArguments()), function ($argument) use ($definition, $givenArguments) { $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) { 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)); 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();
} }
/** /**

View file

@ -338,8 +338,10 @@ class InputDefinition
* @return string The InputOption name * @return string The InputOption name
* *
* @throws InvalidArgumentException When option given does not exist * @throws InvalidArgumentException When option given does not exist
*
* @internal
*/ */
private function shortcutToName($shortcut) public function shortcutToName($shortcut)
{ {
if (!isset($this->shortcuts[$shortcut])) { if (!isset($this->shortcuts[$shortcut])) {
throw new InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); throw new InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut));

View file

@ -60,9 +60,8 @@ class CommandTester
} }
$this->input = new ArrayInput($input); $this->input = new ArrayInput($input);
if ($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)); $this->input->setStream(self::createStream($this->inputs));
}
if (isset($options['interactive'])) { if (isset($options['interactive'])) {
$this->input->setInteractive($options['interactive']); $this->input->setInteractive($options['interactive']);

View file

@ -126,7 +126,7 @@ trait TesterTrait
*/ */
private function initOutput(array $options) 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) { if (!$this->captureStreamsIndependently) {
$this->output = new StreamOutput(fopen('php://memory', 'w', false)); $this->output = new StreamOutput(fopen('php://memory', 'w', false));
if (isset($options['decorated'])) { if (isset($options['decorated'])) {

View file

@ -968,6 +968,19 @@ class ApplicationTest extends TestCase
$this->assertSame('called'.PHP_EOL, $tester->getDisplay(), '->run() does not call interact() if -n is passed'); $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. * Issue #9285.
* *

View file

@ -237,6 +237,43 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
$this->assertSame('b', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question)); $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() public function testAutocompleteWithTrailingBackslash()
{ {
if (!$this->hasSttyAvailable()) { if (!$this->hasSttyAvailable()) {
@ -549,7 +586,7 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
/** /**
* @expectedException \Symfony\Component\Console\Exception\RuntimeException * @expectedException \Symfony\Component\Console\Exception\RuntimeException
* @expectedExceptionMessage Aborted * @expectedExceptionMessage Aborted.
*/ */
public function testAskThrowsExceptionOnMissingInput() public function testAskThrowsExceptionOnMissingInput()
{ {
@ -559,7 +596,17 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
/** /**
* @expectedException \Symfony\Component\Console\Exception\RuntimeException * @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() public function testAskThrowsExceptionOnMissingInputWithValidator()
{ {

View file

@ -124,7 +124,7 @@ class SymfonyQuestionHelperTest extends AbstractQuestionHelperTest
/** /**
* @expectedException \Symfony\Component\Console\Exception\RuntimeException * @expectedException \Symfony\Component\Console\Exception\RuntimeException
* @expectedExceptionMessage Aborted * @expectedExceptionMessage Aborted.
*/ */
public function testAskThrowsExceptionOnMissingInput() public function testAskThrowsExceptionOnMissingInput()
{ {

View file

@ -312,6 +312,14 @@ class ArgvInputTest extends TestCase
$input = new ArgvInput(['cli.php', '-fbbar', 'foo']); $input = new ArgvInput(['cli.php', '-fbbar', 'foo']);
$this->assertEquals('foo', $input->getFirstArgument(), '->getFirstArgument() returns the first argument from the raw input'); $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() public function testHasParameterOption()

View file

@ -17,6 +17,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Output\Output; use Symfony\Component\Console\Output\Output;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question; use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
@ -139,7 +140,7 @@ class CommandTesterTest extends TestCase
/** /**
* @expectedException \RuntimeException * @expectedException \RuntimeException
* @expectedMessage Aborted * @expectedExceptionMessage Aborted.
*/ */
public function testCommandWithWrongInputsNumber() public function testCommandWithWrongInputsNumber()
{ {
@ -153,13 +154,40 @@ class CommandTesterTest extends TestCase
$command->setHelperSet(new HelperSet([new QuestionHelper()])); $command->setHelperSet(new HelperSet([new QuestionHelper()]));
$command->setCode(function ($input, $output) use ($questions, $command) { $command->setCode(function ($input, $output) use ($questions, $command) {
$helper = $command->getHelper('question'); $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[0]));
$helper->ask($input, $output, new Question($questions[1])); $helper->ask($input, $output, new Question($questions[1]));
$helper->ask($input, $output, new Question($questions[2])); $helper->ask($input, $output, new Question($questions[2]));
}); });
$tester = new CommandTester($command); $tester = new CommandTester($command);
$tester->setInputs(['Bobby', 'Fine']);
$tester->execute([]); $tester->execute([]);
} }

View file

@ -29,7 +29,7 @@ use Symfony\Component\Finder\Iterator\SortableIterator;
* *
* All rules may be invoked several times. * 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__); * $finder = Finder::create()->files()->name('*.php')->in(__DIR__);
* *
@ -674,12 +674,15 @@ class Finder implements \IteratorAggregate, \Countable
private function searchInDirectory(string $dir): \Iterator private function searchInDirectory(string $dir): \Iterator
{ {
$exclude = $this->exclude;
$notPaths = $this->notPaths;
if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) { 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)) { if (static::IGNORE_DOT_FILES === (static::IGNORE_DOT_FILES & $this->ignore)) {
$this->notPaths[] = '#(^|/)\..+(/|$)#'; $notPaths[] = '#(^|/)\..+(/|$)#';
} }
$minDepth = 0; $minDepth = 0;
@ -712,8 +715,8 @@ class Finder implements \IteratorAggregate, \Countable
$iterator = new Iterator\RecursiveDirectoryIterator($dir, $flags, $this->ignoreUnreadableDirs); $iterator = new Iterator\RecursiveDirectoryIterator($dir, $flags, $this->ignoreUnreadableDirs);
if ($this->exclude) { if ($exclude) {
$iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude); $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $exclude);
} }
$iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST);
@ -746,8 +749,8 @@ class Finder implements \IteratorAggregate, \Countable
$iterator = new Iterator\CustomFilterIterator($iterator, $this->filters); $iterator = new Iterator\CustomFilterIterator($iterator, $this->filters);
} }
if ($this->paths || $this->notPaths) { if ($this->paths || $notPaths) {
$iterator = new Iterator\PathFilterIterator($iterator, $this->paths, $this->notPaths); $iterator = new Iterator\PathFilterIterator($iterator, $this->paths, $notPaths);
} }
if ($this->sort || $this->reverseSorting) { if ($this->sort || $this->reverseSorting) {

View file

@ -421,6 +421,59 @@ class FinderTest extends Iterator\RealIteratorTestCase
]), $finder->in(self::$tmpDir)->getIterator()); ]), $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() public function testIgnoreDotFiles()
{ {
$finder = $this->buildFinder(); $finder = $this->buildFinder();
@ -496,6 +549,53 @@ class FinderTest extends Iterator\RealIteratorTestCase
]), $finder->in(self::$tmpDir)->getIterator()); ]), $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() public function testSortByName()
{ {
$finder = $this->buildFinder(); $finder = $this->buildFinder();