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

View file

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

View file

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

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)
[![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/finder/blob/master/license.md)
Class `Nette\Utils\Finder` makes browsing the directory structure really easy.
Introduction
------------
Nette Finder makes browsing the directory structure really easy.
Documentation can be found on the [website](https://doc.nette.org/finder).
If you like Nette, **[please make a donation now](https://nette.org/donate)**. Thank you!
All examples assume the following class alias is defined:
Installation
------------
```php
use Nette\Utils\Finder;
The recommended way to install is via Composer:
```
composer require nette/finder
```
It requires PHP version 5.6 and supports PHP up to 7.3. The dev-master version requires PHP 7.1.
Searching for Files
-------------------
Usage
-----
How to find all `*.txt` files in `$dir` directory without recursing subdirectories?
@ -104,13 +117,13 @@ Depth of search can be limited using the `limitDepth()` method.
Searching for directories
----------------
-------------------------
In addition to files, it is possible to search for directories using `Finder::findDirectories('subdir*')`, or to search for files and directories: `Finder::find('file.txt')`.
Filtering
----------
---------
You can also filter results. For example by size. This way we will traverse the files of size between 100B and 200B:
@ -151,7 +164,7 @@ foreach (Finder::findFiles('*')
Connection to Amazon S3
----------------------
-----------------------
It's possible to use custom streams, for example Zend_Service_Amazon_S3:

View file

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

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.
- get rid of all `require`
- only necessary scripts are loaded
- requires no strict file naming conventions
- allows more classes in single file
- extremely fast
- no manual cache updates, everything runs automatically
- highly mature, stable and widely used library
RobotLoader is extremely comfortable and addictive!
RobotLoader is incredibly comfortable and addictive!
If you like Nette, **[please make a donation now](https://nette.org/donate)**. Thank you!
@ -46,7 +48,7 @@ The recommended way to install is via Composer:
composer require nette/robot-loader
```
It requires PHP version 5.6 and supports PHP up to 7.2.
It requires PHP version 5.6 and supports PHP up to 7.3.
Usage
@ -76,3 +78,36 @@ This feature should be disabled on production server.
If you want RobotLoader to skip some directory, use `$loader->excludeDirectory('temp')`.
By default, RobotLoader reports errors in PHP files by throwing exception `ParseError` (since PHP 7.0). It can be disabled via `$loader->reportParseErrors(false)`.
PHP files analyzer
------------------
RobotLoader can also be used to find classes, interfaces, and trait in PHP files without using the autoloading feature:
```php
$loader = new Nette\Loaders\RobotLoader;
$loader->addDirectory(__DIR__ . '/app');
// Scans directories for classes / intefaces / traits
$loader->rebuild();
// Returns array of class => filename pairs
$res = $loader->getIndexedClasses();
```
When scanning files again, we can use the cache and unmodified files will not be analyzed repeatedly:
```php
$loader = new Nette\Loaders\RobotLoader;
$loader->addDirectory(__DIR__ . '/app');
$loader->setTempDirectory(__DIR__ . '/temp');
// Scans directories using a cache
$loader->refresh();
// Returns array of class => filename pairs
$res = $loader->getIndexedClasses();
```
Enjoy RobotLoader!

View file

@ -28,10 +28,10 @@ class RobotLoader
const RETRY_LIMIT = 3;
/** @var array comma separated wildcards */
/** @var array */
public $ignoreDirs = ['.*', '*.old', '*.bak', '*.tmp', 'temp'];
/** @var array comma separated wildcards */
/** @var array */
public $acceptFiles = ['*.php'];
/** @var bool */
@ -95,7 +95,7 @@ class RobotLoader
$missing = &$this->missing[$type];
$missing++;
if (!$this->refreshed && $missing <= self::RETRY_LIMIT) {
$this->refresh();
$this->refreshClasses();
$this->saveCache();
} elseif ($info) {
unset($this->classes[$type]);
@ -171,7 +171,8 @@ class RobotLoader
*/
public function rebuild()
{
$this->refresh();
$this->classes = $this->missing = [];
$this->refreshClasses();
if ($this->tempDirectory) {
$this->saveCache();
}
@ -179,12 +180,26 @@ class RobotLoader
/**
* Refreshes class list.
* Refreshes class list cache.
* @return void
*/
private function refresh()
public function refresh()
{
$this->refreshed = true; // prevents calling refresh() or updateFile() in tryLoad()
$this->loadCache();
if (!$this->refreshed) {
$this->refreshClasses();
$this->saveCache();
}
}
/**
* Refreshes $classes.
* @return void
*/
private function refreshClasses()
{
$this->refreshed = true; // prevents calling refreshClasses() or updateFile() in tryLoad()
$files = [];
foreach ($this->classes as $class => $info) {
$files[$info['file']]['time'] = $info['time'];
@ -193,7 +208,8 @@ class RobotLoader
$this->classes = [];
foreach ($this->scanPaths as $path) {
foreach (is_file($path) ? [new SplFileInfo($path)] : $this->createFileIterator($path) as $file) {
$iterator = is_file($path) ? [new SplFileInfo($path)] : $this->createFileIterator($path);
foreach ($iterator as $file) {
$file = $file->getPathname();
if (isset($files[$file]) && $files[$file]['time'] == filemtime($file)) {
$classes = $files[$file]['classes'];
@ -234,7 +250,8 @@ class RobotLoader
}
}
$iterator = Nette\Utils\Finder::findFiles(is_array($this->acceptFiles) ? $this->acceptFiles : preg_split('#[,\s]+#', $this->acceptFiles))
$acceptFiles = is_array($this->acceptFiles) ? $this->acceptFiles : preg_split('#[,\s]+#', $this->acceptFiles);
$iterator = Nette\Utils\Finder::findFiles($acceptFiles)
->filter(function (SplFileInfo $file) use (&$disallow) {
return !isset($disallow[str_replace('\\', '/', $file->getRealPath())]);
})
@ -419,9 +436,7 @@ class RobotLoader
list($this->classes, $this->missing) = @include $file; // @ file may not exist
if (!is_array($this->classes)) {
$this->classes = [];
$this->refresh();
$this->saveCache();
$this->rebuild();
}
flock($handle, LOCK_UN);

View file

@ -19,22 +19,22 @@ final class Versions
'jean85/pretty-package-versions' => '1.2@75c7effcf3f77501d0e0caa75111aff4daa0dd48',
'nette/bootstrap' => 'v2.4.6@268816e3f1bb7426c3a4ceec2bd38a036b532543',
'nette/di' => 'v2.4.15@d0561b8f77e8ef2ed6d83328860e16c81a5a8649',
'nette/finder' => 'v2.4.2@ee951a656cb8ac622e5dd33474a01fd2470505a0',
'nette/finder' => 'v2.5.0@6be1b83ea68ac558aff189d640abe242e0306fe2',
'nette/neon' => 'v3.0.0@cbff32059cbdd8720deccf9e9eace6ee516f02eb',
'nette/php-generator' => 'v3.2.1@9de4e093a130f7a1bd175198799ebc0efbac6924',
'nette/robot-loader' => 'v3.1.0@fc76c70e740b10f091e502b2e393d0be912f38d4',
'nette/robot-loader' => 'v3.1.1@3e8d75d6d976e191bdf46752ca40a286671219d2',
'nette/utils' => 'v2.5.3@17b9f76f2abd0c943adfb556e56f2165460b15ce',
'nikic/php-parser' => 'v4.2.1@5221f49a608808c1e4d436df32884cbc1b821ac0',
'ocramius/package-versions' => '1.4.0@a4d4b60d0e60da2487bd21a2c6ac089f85570dbb',
'phpstan/phpdoc-parser' => '0.3.1@2cc49f47c69b023eaf05b48e6529389893b13d74',
'phpstan/phpstan' => '0.11.2@8e185a74004920419ee97bf9dc62e6a175e8dca5',
'phpstan/phpstan' => '0.11.3@e4644b4a8fd393c346f1137305fb2f76a7dc20a7',
'psr/log' => '1.1.0@6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd',
'symfony/console' => 'v4.2.3@1f0ad51dfde4da8a6070f06adc58b4e37cbb37a4',
'symfony/console' => 'v4.2.4@9dc2299a016497f9ee620be94524e6c0af0280a9',
'symfony/contracts' => 'v1.0.2@1aa7ab2429c3d594dd70689604b5cf7421254cdf',
'symfony/finder' => 'v4.2.3@ef71816cbb264988bb57fe6a73f610888b9aa70c',
'symfony/finder' => 'v4.2.4@267b7002c1b70ea80db0833c3afe05f0fbde580a',
'symfony/polyfill-mbstring' => 'v1.10.0@c79c051f5b3a46be09205c73b80b346e4153e494',
'thecodingmachine/phpstan-safe-rule' => 'v0.1.3@00f4845905feb5240ca62fb799e3c51ba85c9230',
'__root__' => 'dev-master@04a956886ab327ddbe5eec546b911b9e55a0e5ef',
'__root__' => 'dev-master@58cc098058143344a846f01a5d2252a45e2be9ba',
);
private function __construct()

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>
&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>
&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.
BTC: bc1qd5s06wjtf8rzag08mk3s264aekn52jze9zeapt
<br>LTC: LSU5xLsWEfrVx1P9yJwmhziHAXikiE8xtC
## Prerequisites
PHPStan requires PHP >= 7.1. You have to run it in environment with PHP 7.x but the actual code does not have to use
@ -119,6 +120,7 @@ Unofficial extensions for other frameworks and libraries are also available:
* [Yii2](https://github.com/proget-hq/phpstan-yii2)
* [PhpSpec](https://github.com/proget-hq/phpstan-phpspec)
* [TYPO3](https://github.com/sascha-egerer/phpstan-typo3)
* [moneyphp/money](https://github.com/JohnstonCode/phpstan-moneyphp)
New extensions are becoming available on a regular basis!
@ -406,6 +408,8 @@ You can pass the following keywords to the `--error-format=X` parameter in order
- `table`: Default. Grouped errors by file, colorized. For human consumption.
- `raw`: Contains one error per line, with path to file, line number, and error description
- `checkstyle`: Creates a checkstyle.xml compatible output. Note that you'd have to redirect output into a file in order to capture the results for later processing.
- `json`: Creates minified .json output without whitespaces. Note that you'd have to redirect output into a file in order to capture the results for later processing.
- `prettyJson`: Creates human readable .json output with whitespaces and indentations. Note that you'd have to redirect output into a file in order to capture the results for later processing.
## Class reflection extensions

View file

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

View file

@ -379,6 +379,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: PHPStan\Type\Php\FilterVarDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: PHPStan\Type\Php\GetParentClassDynamicFunctionReturnTypeExtension
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);
}
public function equals(self $otherContext): bool
{
if ($this->file !== $otherContext->file) {
return false;
}
if ($this->getClassReflection() === null) {
return $otherContext->getClassReflection() === null;
} elseif ($otherContext->getClassReflection() === null) {
return false;
}
$isSameClass = $this->getClassReflection()->getName() === $otherContext->getClassReflection()->getName();
if ($this->getTraitReflection() === null) {
return $otherContext->getTraitReflection() === null && $isSameClass;
} elseif ($otherContext->getTraitReflection() === null) {
return false;
}
$isSameTrait = $this->getTraitReflection()->getName() === $otherContext->getTraitReflection()->getName();
return $isSameClass && $isSameTrait;
}
public function getFile(): string
{
return $this->file;

View file

@ -51,7 +51,7 @@ class ScopeFactory
* @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|null $inFunctionCall
* @param bool $negated
* @param bool $inFirstLevelStatement
* @param string[] $currentlyAssignedExpressions
* @param array<string, true> $currentlyAssignedExpressions
*
* @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 PHPStan\Broker\Broker;
use PHPStan\Type\Accessory\HasOffsetType;
use PHPStan\Type\Accessory\HasPropertyType;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantFloatType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NonexistentParentClassType;
@ -102,6 +105,10 @@ class TypeSpecifier
): SpecifiedTypes
{
if ($expr instanceof Instanceof_) {
$exprNode = $expr->expr;
if ($exprNode instanceof Expr\Assign) {
$exprNode = $exprNode->var;
}
if ($expr->class instanceof Name) {
$className = (string) $expr->class;
$lowercasedClassName = strtolower($className);
@ -121,17 +128,20 @@ class TypeSpecifier
} else {
$type = new ObjectType($className);
}
return $this->create($expr->expr, $type, $context);
return $this->create($exprNode, $type, $context);
}
if ($context->true()) {
return $this->create($expr->expr, new ObjectWithoutClassType(), $context);
return $this->create($exprNode, new ObjectWithoutClassType(), $context);
}
} elseif ($expr instanceof Node\Expr\BinaryOp\Identical) {
$expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr);
if ($expressions !== null) {
/** @var Expr $exprNode */
$exprNode = $expressions[0];
if ($exprNode instanceof Expr\Assign) {
$exprNode = $exprNode->var;
}
/** @var \PHPStan\Type\ConstantScalarType $constantType */
$constantType = $expressions[1];
if ($constantType->getValue() === false) {
@ -250,6 +260,66 @@ class TypeSpecifier
);
}
}
$leftType = $scope->getType($expr->left);
$leftBooleanType = $leftType->toBoolean();
$rightType = $scope->getType($expr->right);
if ($leftBooleanType instanceof ConstantBooleanType && $rightType instanceof BooleanType) {
return $this->specifyTypesInCondition(
$scope,
new Expr\BinaryOp\Identical(
new ConstFetch(new Name($leftBooleanType->getValue() ? 'true' : 'false')),
$expr->right
),
$context
);
}
$rightBooleanType = $rightType->toBoolean();
if ($rightBooleanType instanceof ConstantBooleanType && $leftType instanceof BooleanType) {
return $this->specifyTypesInCondition(
$scope,
new Expr\BinaryOp\Identical(
$expr->left,
new ConstFetch(new Name($rightBooleanType->getValue() ? 'true' : 'false'))
),
$context
);
}
if (
$expr->left instanceof FuncCall
&& $expr->left->name instanceof Name
&& strtolower($expr->left->name->toString()) === 'get_class'
&& isset($expr->left->args[0])
&& $rightType instanceof ConstantStringType
) {
return $this->specifyTypesInCondition(
$scope,
new Instanceof_(
$expr->left->args[0]->value,
new Name($rightType->getValue())
),
$context
);
}
if (
$expr->right instanceof FuncCall
&& $expr->right->name instanceof Name
&& strtolower($expr->right->name->toString()) === 'get_class'
&& isset($expr->right->args[0])
&& $leftType instanceof ConstantStringType
) {
return $this->specifyTypesInCondition(
$scope,
new Instanceof_(
$expr->right->args[0]->value,
new Name($leftType->getValue())
),
$context
);
}
} elseif ($expr instanceof Node\Expr\BinaryOp\NotEqual) {
return $this->specifyTypesInCondition(
$scope,
@ -373,6 +443,10 @@ class TypeSpecifier
}
}
if (count($vars) === 0) {
throw new \PHPStan\ShouldNotHappenException();
}
$types = null;
foreach ($vars as $var) {
if ($expr instanceof Expr\Isset_) {
@ -401,6 +475,26 @@ class TypeSpecifier
TypeSpecifierContext::createFalse()
);
}
if (
$var instanceof PropertyFetch
&& $var->name instanceof Node\Identifier
) {
$type = $type->unionWith($this->create($var->var, new IntersectionType([
new ObjectWithoutClassType(),
new HasPropertyType($var->name->toString()),
]), TypeSpecifierContext::createTruthy()));
} elseif (
$var instanceof StaticPropertyFetch
&& $var->class instanceof Expr
&& $var->name instanceof Node\VarLikeIdentifier
) {
$type = $type->unionWith($this->create($var->class, new IntersectionType([
new ObjectWithoutClassType(),
new HasPropertyType($var->name->toString()),
]), TypeSpecifierContext::createTruthy()));
}
if ($types === null) {
$types = $type;
} else {
@ -408,9 +502,6 @@ class TypeSpecifier
}
}
/** @var SpecifiedTypes $types */
$types = $types;
if (
$expr instanceof Expr\Empty_
&& (new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($scope->getType($expr->expr))->yes()) {
@ -480,7 +571,7 @@ class TypeSpecifier
public function create(Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes
{
if ($expr instanceof New_) {
if ($expr instanceof New_ || $expr instanceof Instanceof_) {
return new SpecifiedTypes();
}
@ -500,7 +591,7 @@ class TypeSpecifier
/**
* @return \PHPStan\Type\FunctionTypeSpecifyingExtension[]
*/
public function getFunctionTypeSpecifyingExtensions(): array
private function getFunctionTypeSpecifyingExtensions(): array
{
return $this->functionTypeSpecifyingExtensions;
}
@ -509,7 +600,7 @@ class TypeSpecifier
* @param string $className
* @return \PHPStan\Type\MethodTypeSpecifyingExtension[]
*/
public function getMethodTypeSpecifyingExtensionsForClass(string $className): array
private function getMethodTypeSpecifyingExtensionsForClass(string $className): array
{
if ($this->methodTypeSpecifyingExtensionsByClass === null) {
$byClass = [];
@ -526,7 +617,7 @@ class TypeSpecifier
* @param string $className
* @return \PHPStan\Type\StaticMethodTypeSpecifyingExtension[]
*/
public function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array
private function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array
{
if ($this->staticMethodTypeSpecifyingExtensionsByClass === null) {
$byClass = [];

View file

@ -36,8 +36,13 @@ class VariableTypeHolder
public function and(self $other): self
{
if ($this->getType()->equals($other->getType())) {
$type = $this->getType();
} else {
$type = TypeCombinator::union($this->getType(), $other->getType());
}
return new self(
TypeCombinator::union($this->getType(), $other->getType()),
$type,
$this->getCertainty()->and($other->getCertainty())
);
}

View file

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

View file

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

View file

@ -173,6 +173,11 @@ class ParametersAcceptorSelector
*/
public static function combineAcceptors(array $acceptors): ParametersAcceptor
{
if (count($acceptors) === 0) {
throw new \PHPStan\ShouldNotHappenException(
'getVariants() must return at least one variant.'
);
}
if (count($acceptors) === 1) {
return $acceptors[0];
}
@ -236,9 +241,6 @@ class ParametersAcceptorSelector
}
}
/** @var \PHPStan\Type\Type $returnType */
$returnType = $returnType;
return new FunctionVariant($parameters, $isVariadic, $returnType);
}

View file

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

View file

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

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();
}
if ($scope->isInExpressionAssign($node)) {
return [];
}
if (!$type->canAccessProperties()->yes()) {
return [
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;
$classType = TypeCombinator::remove($classType, new StringType());
if ($scope->isInExpressionAssign($node)) {
return [];
}
if (!$classType->canAccessProperties()->yes()) {
return array_merge($messages, [
sprintf('Cannot access static property $%s on %s.', $name, $typeForDescribe->describe(VerbosityLevel::typeOnly())),

View file

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

View file

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

View file

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

View file

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

View file

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

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
{
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

View file

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

View file

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

View file

@ -199,6 +199,13 @@ class Application
return 0;
}
try {
// Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument.
$input->bind($this->getDefinition());
} catch (ExceptionInterface $e) {
// Errors must be ignored, full binding/validation happens later when the command is known.
}
$name = $this->getCommandName($input);
if (true === $input->hasParameterOption(['--help', '-h'], true)) {
if (!$name) {

View file

@ -381,20 +381,17 @@ final class ProgressBar
$lines = floor(Helper::strlen($message) / $this->terminal->getWidth()) + $this->formatLineCount + 1;
$this->output->clear($lines);
} else {
// Move the cursor to the beginning of the line
$this->output->write("\x0D");
// Erase the line
$this->output->write("\x1B[2K");
// Erase previous lines
if ($this->formatLineCount > 0) {
$this->output->write(str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount));
$message = str_repeat("\x1B[1A\x1B[2K", $this->formatLineCount).$message;
}
// Move the cursor to the beginning of the line and erase the line
$message = "\x0D\x1B[2K$message";
}
}
} elseif ($this->step > 0) {
$this->output->writeln('');
$message = PHP_EOL.$message;
}
$this->firstRun = false;

View file

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

View file

@ -257,8 +257,27 @@ class ArgvInput extends Input
*/
public function getFirstArgument()
{
foreach ($this->tokens as $token) {
$isOption = false;
foreach ($this->tokens as $i => $token) {
if ($token && '-' === $token[0]) {
if (false !== strpos($token, '=') || !isset($this->tokens[$i + 1])) {
continue;
}
// If it's a long option, consider that everything after "--" is the option name.
// Otherwise, use the last char (if it's a short option set, only the last one can take a value with space separator)
$name = '-' === $token[1] ? substr($token, 2) : substr($token, -1);
if (!isset($this->options[$name]) && !$this->definition->hasShortcut($name)) {
// noop
} elseif ((isset($this->options[$name]) || isset($this->options[$name = $this->definition->shortcutToName($name)])) && $this->tokens[$i + 1] === $this->options[$name]) {
$isOption = true;
}
continue;
}
if ($isOption) {
$isOption = false;
continue;
}

View file

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

View file

@ -69,7 +69,7 @@ abstract class Input implements InputInterface, StreamableInputInterface
$givenArguments = $this->arguments;
$missingArguments = array_filter(array_keys($definition->getArguments()), function ($argument) use ($definition, $givenArguments) {
return !array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired();
return !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired();
});
if (\count($missingArguments) > 0) {
@ -150,7 +150,7 @@ abstract class Input implements InputInterface, StreamableInputInterface
throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name));
}
return array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault();
return \array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault();
}
/**

View file

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

View file

@ -60,9 +60,8 @@ class CommandTester
}
$this->input = new ArrayInput($input);
if ($this->inputs) {
$this->input->setStream(self::createStream($this->inputs));
}
// Use an in-memory input stream even if no inputs are set so that QuestionHelper::ask() does not rely on the blocking STDIN.
$this->input->setStream(self::createStream($this->inputs));
if (isset($options['interactive'])) {
$this->input->setInteractive($options['interactive']);

View file

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

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');
}
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.
*

View file

@ -237,6 +237,43 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
$this->assertSame('b', $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
}
public function getInputs()
{
return [
['$'], // 1 byte character
['¢'], // 2 bytes character
['€'], // 3 bytes character
['𐍈'], // 4 bytes character
];
}
/**
* @dataProvider getInputs
*/
public function testAskWithAutocompleteWithMultiByteCharacter($character)
{
if (!$this->hasSttyAvailable()) {
$this->markTestSkipped('`stty` is required to test autocomplete functionality');
}
$inputStream = $this->getInputStream("$character\n");
$possibleChoices = [
'$' => '1 byte character',
'¢' => '2 bytes character',
'€' => '3 bytes character',
'𐍈' => '4 bytes character',
];
$dialog = new QuestionHelper();
$dialog->setHelperSet(new HelperSet([new FormatterHelper()]));
$question = new ChoiceQuestion('Please select a character', $possibleChoices);
$question->setMaxAttempts(1);
$this->assertSame($character, $dialog->ask($this->createStreamableInputInterfaceMock($inputStream), $this->createOutputInterface(), $question));
}
public function testAutocompleteWithTrailingBackslash()
{
if (!$this->hasSttyAvailable()) {
@ -549,7 +586,7 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
/**
* @expectedException \Symfony\Component\Console\Exception\RuntimeException
* @expectedExceptionMessage Aborted
* @expectedExceptionMessage Aborted.
*/
public function testAskThrowsExceptionOnMissingInput()
{
@ -559,7 +596,17 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
/**
* @expectedException \Symfony\Component\Console\Exception\RuntimeException
* @expectedExceptionMessage Aborted
* @expectedExceptionMessage Aborted.
*/
public function testAskThrowsExceptionOnMissingInputForChoiceQuestion()
{
$dialog = new QuestionHelper();
$dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), new ChoiceQuestion('Choice', ['a', 'b']));
}
/**
* @expectedException \Symfony\Component\Console\Exception\RuntimeException
* @expectedExceptionMessage Aborted.
*/
public function testAskThrowsExceptionOnMissingInputWithValidator()
{

View file

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

View file

@ -312,6 +312,14 @@ class ArgvInputTest extends TestCase
$input = new ArgvInput(['cli.php', '-fbbar', 'foo']);
$this->assertEquals('foo', $input->getFirstArgument(), '->getFirstArgument() returns the first argument from the raw input');
$input = new ArgvInput(['cli.php', '--foo', 'fooval', 'bar']);
$input->bind(new InputDefinition([new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL), new InputArgument('arg')]));
$this->assertSame('bar', $input->getFirstArgument());
$input = new ArgvInput(['cli.php', '-bf', 'fooval', 'argval']);
$input->bind(new InputDefinition([new InputOption('bar', 'b', InputOption::VALUE_NONE), new InputOption('foo', 'f', InputOption::VALUE_OPTIONAL), new InputArgument('arg')]));
$this->assertSame('argval', $input->getFirstArgument());
}
public function testHasParameterOption()

View file

@ -17,6 +17,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Output\Output;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Tester\CommandTester;
@ -139,7 +140,7 @@ class CommandTesterTest extends TestCase
/**
* @expectedException \RuntimeException
* @expectedMessage Aborted
* @expectedExceptionMessage Aborted.
*/
public function testCommandWithWrongInputsNumber()
{
@ -153,13 +154,40 @@ class CommandTesterTest extends TestCase
$command->setHelperSet(new HelperSet([new QuestionHelper()]));
$command->setCode(function ($input, $output) use ($questions, $command) {
$helper = $command->getHelper('question');
$helper->ask($input, $output, new ChoiceQuestion('choice', ['a', 'b']));
$helper->ask($input, $output, new Question($questions[0]));
$helper->ask($input, $output, new Question($questions[1]));
$helper->ask($input, $output, new Question($questions[2]));
});
$tester = new CommandTester($command);
$tester->setInputs(['a', 'Bobby', 'Fine']);
$tester->execute([]);
}
/**
* @expectedException \RuntimeException
* @expectedExceptionMessage Aborted.
*/
public function testCommandWithQuestionsButNoInputs()
{
$questions = [
'What\'s your name?',
'How are you?',
'Where do you come from?',
];
$command = new Command('foo');
$command->setHelperSet(new HelperSet([new QuestionHelper()]));
$command->setCode(function ($input, $output) use ($questions, $command) {
$helper = $command->getHelper('question');
$helper->ask($input, $output, new ChoiceQuestion('choice', ['a', 'b']));
$helper->ask($input, $output, new Question($questions[0]));
$helper->ask($input, $output, new Question($questions[1]));
$helper->ask($input, $output, new Question($questions[2]));
});
$tester = new CommandTester($command);
$tester->setInputs(['Bobby', 'Fine']);
$tester->execute([]);
}

View file

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

View file

@ -421,6 +421,59 @@ class FinderTest extends Iterator\RealIteratorTestCase
]), $finder->in(self::$tmpDir)->getIterator());
}
public function testIgnoreVCSCanBeDisabledAfterFirstIteration()
{
$finder = $this->buildFinder();
$finder->in(self::$tmpDir);
$finder->ignoreDotFiles(false);
$this->assertIterator($this->toAbsolute([
'foo',
'foo/bar.tmp',
'qux',
'qux/baz_100_1.py',
'qux/baz_1_2.py',
'qux_0_1.php',
'qux_1000_1.php',
'qux_1002_0.php',
'qux_10_2.php',
'qux_12_0.php',
'qux_2_0.php',
'test.php',
'test.py',
'toto',
'.bar',
'.foo',
'.foo/.bar',
'.foo/bar',
'foo bar',
]), $finder->getIterator());
$finder->ignoreVCS(false);
$this->assertIterator($this->toAbsolute(['.git',
'foo',
'foo/bar.tmp',
'qux',
'qux/baz_100_1.py',
'qux/baz_1_2.py',
'qux_0_1.php',
'qux_1000_1.php',
'qux_1002_0.php',
'qux_10_2.php',
'qux_12_0.php',
'qux_2_0.php',
'test.php',
'test.py',
'toto',
'toto/.git',
'.bar',
'.foo',
'.foo/.bar',
'.foo/bar',
'foo bar',
]), $finder->getIterator());
}
public function testIgnoreDotFiles()
{
$finder = $this->buildFinder();
@ -496,6 +549,53 @@ class FinderTest extends Iterator\RealIteratorTestCase
]), $finder->in(self::$tmpDir)->getIterator());
}
public function testIgnoreDotFilesCanBeDisabledAfterFirstIteration()
{
$finder = $this->buildFinder();
$finder->in(self::$tmpDir);
$this->assertIterator($this->toAbsolute([
'foo',
'foo/bar.tmp',
'qux',
'qux/baz_100_1.py',
'qux/baz_1_2.py',
'qux_0_1.php',
'qux_1000_1.php',
'qux_1002_0.php',
'qux_10_2.php',
'qux_12_0.php',
'qux_2_0.php',
'test.php',
'test.py',
'toto',
'foo bar',
]), $finder->getIterator());
$finder->ignoreDotFiles(false);
$this->assertIterator($this->toAbsolute([
'foo',
'foo/bar.tmp',
'qux',
'qux/baz_100_1.py',
'qux/baz_1_2.py',
'qux_0_1.php',
'qux_1000_1.php',
'qux_1002_0.php',
'qux_10_2.php',
'qux_12_0.php',
'qux_2_0.php',
'test.php',
'test.py',
'toto',
'.bar',
'.foo',
'.foo/.bar',
'.foo/bar',
'foo bar',
]), $finder->getIterator());
}
public function testSortByName()
{
$finder = $this->buildFinder();