LICENSE 0000666 00000002064 13436751442 0005570 0 ustar 00 MIT License
Copyright (c) 2016-2017 Ondřej Mirtes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
README.md 0000666 00000050737 13436751442 0006054 0 ustar 00 # PHPStan - PHP Static Analysis Tool
[![Build Status](https://travis-ci.org/phpstan/phpstan.svg)](https://travis-ci.org/phpstan/phpstan)
[![Latest Stable Version](https://poser.pugx.org/phpstan/phpstan/v/stable)](https://packagist.org/packages/phpstan/phpstan)
[![License](https://poser.pugx.org/phpstan/phpstan/license)](https://packagist.org/packages/phpstan/phpstan)
[![PHPStan](https://img.shields.io/badge/PHPStan-enabled-brightgreen.svg?style=flat)](https://github.com/phpstan/phpstan)
[![Donate on PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://paypal.me/phpstan)
PHPStan focuses on finding errors in your code without actually running it. It catches whole classes of bugs
even before you write tests for the code.
![PHPStan](build/phpstan.gif)
PHPStan moves PHP closer to compiled languages in the sense that the correctness of each line of the code
can be checked before you run the actual line.
**[Read more about PHPStan on Medium.com »](https://medium.com/@ondrejmirtes/phpstan-2939cd0ad0e3)**
**[Try out PHPStan on the on-line playground! »](https://phpstan.org/)**
## Is PHPStan helping you to avoid bugs in production?
## Consider [supporting it on Patreon](https://www.patreon.com/phpstan) so I'm able to make it even more awesome!
*I offer to issue invoices for contributions on PayPal and Patreon! [Contact me](mailto:ondrej@mirtes.cz) for details.*
It currently performs the following checks on your code:
* Existence of classes and interfaces in `instanceof`, `catch`, typehints, other language constructs and even annotations. PHP does not do this and just stays silent instead.
* Existence of variables while respecting scopes of branches and loops.
* Existence and visibility of called methods and functions.
* Existence and visibility of accessed properties and constants.
* Correct types assigned to properties.
* Correct number and types of parameters passed to constructors, methods and functions.
* Correct types returned from methods and functions.
* Correct number of parameters passed to `sprintf`/`printf` calls based on format strings.
* Useless casts like `(string) 'foo'`.
* Unused constructor parameters - they can either be deleted or the author forgot to
use them in the method body.
* Require calling `parent::__construct()` if the parent constructor exists.
* Only valid array key types are used (only integers, strings, floats, booleans and nulls).
* Duplicate array keys in literal arrays.
* Only iterables are passed to `foreach`.
* Correct case when referencing classes. Class names are case insensitive, but taking advantage of this is dangerous with autoloading on case-insensitive filesystems.
* Impossible checks (dead code) of incompatible types with `instanceof`, `===`, `!==` and various function checks like `is_int` or `is_null`.
* Always-defined and never-defined variables in `isset()` call.
* Validating phpDocs - finding incompatible types between phpDocs and native typehints.
* That only objects are passed to the `clone` keyword.
## Extensibility
Unique feature of PHPStan is the ability to define and statically check "magic" behaviour of classes -
accessing properties that are not defined in the class but are created in `__get` and `__set`
and invoking methods using `__call`.
See [Class reflection extensions](#class-reflection-extensions) and [Dynamic return type extensions](#dynamic-return-type-extensions).
You can also install official framework-specific extensions:
* [Doctrine](https://github.com/phpstan/phpstan-doctrine)
* [PHPUnit](https://github.com/phpstan/phpstan-phpunit)
* [Nette Framework](https://github.com/phpstan/phpstan-nette)
* [Dibi - Database Abstraction Library](https://github.com/phpstan/phpstan-dibi)
* [PHP-Parser](https://github.com/phpstan/phpstan-php-parser)
Unofficial extensions for other frameworks and libraries are also available:
* [Phony](https://github.com/eloquent/phpstan-phony)
New extensions are becoming available on a regular basis!
## Prerequisites
PHPStan requires PHP >= 7.0. You have to run it in environment with PHP 7.x but the actual code does not have to use
PHP 7.x features. (Code written for PHP 5.6 and earlier can run on 7.x mostly unmodified.)
PHPStan works best with modern object-oriented code. The more strongly-typed your code is, the more information
you give PHPStan to work with.
Properly annotated and typehinted code (class properties, function and method arguments, return types) helps
not only static analysis tools but also other people that work with the code to understand it.
## Installation
To start performing analysis on your code, require PHPStan in [Composer](https://getcomposer.org/):
```
composer require --dev phpstan/phpstan
```
Composer will install PHPStan's executable in its `bin-dir` which defaults to `vendor/bin`.
If you have conflicting dependencies or you want to install PHPStan globally, the best way is via a PHAR archive. You will always find the latest stable PHAR archive below the [release notes](https://github.com/phpstan/phpstan/releases). You can also use the [phpstan/phpstan-shim](https://packagist.org/packages/phpstan/phpstan-shim) package to install PHPStan via Composer without the risk of conflicting dependencies.
You can also use [PHPStan via Docker](https://github.com/phpstan/docker-image).
## First run
To let PHPStan analyse your codebase, you have use the `analyse` command and point it to the right directories.
So, for example if you have your classes in directories `src` and `tests`, you can run PHPStan like this:
```
vendor/bin/phpstan analyse src tests
```
PHPStan will probably find some errors, but don't worry, your code might be just fine. Errors found
on the first run tend to be:
* Extra arguments passed to functions (e. g. function requires two arguments, the code passes three)
* Extra arguments passed to print/sprintf functions (e. g. format string contains one placeholder, the code passes two values to replace)
* Obvious errors in dead code
* Magic behaviour that needs to be defined. See [Extensibility](#extensibility).
After fixing the obvious mistakes in the code, look to the following section
for all the configuration options that will bring the number of reported errors to zero
making PHPStan suitable to run as part of your continuous integration script.
## Rule levels
If you want to use PHPStan but your codebase isn't up to speed with strong typing
and PHPStan's strict checks, you can choose from currently 8 levels
(0 is the loosest and 7 is the strictest) by passing `--level` to `analyse` command. Default level is `0`.
This feature enables incremental adoption of PHPStan checks. You can start using PHPStan
with a lower rule level and increase it when you feel like it.
You can also use `--level max` as an alias for the highest level. This will ensure that you will always use the highest level when upgrading to new versions of PHPStan. Please note that this can create a significant obstacle when upgrading to a newer version because you might have to fix a lot of code to bring the number of errors down to zero.
## Configuration
Config file is passed to the `phpstan` executable with `-c` option:
```
vendor/bin/phpstan analyse -l 4 -c phpstan.neon src tests
```
When using a custom project config file, you have to pass the `--level` (`-l`)
option to `analyse` command (default value does not apply here).
[NEON file format](https://ne-on.org/) is very similar to YAML.
All the following options are part of the `parameters` section.
### Autoloading
PHPStan uses Composer autoloader so the easiest way how to autoload classes
is through the `autoload`/`autoload-dev` sections in composer.json.
#### Specify paths to scan
If PHPStan complains about some non-existent classes and you're sure the classes
exist in the codebase AND you don't want to use Composer autoloader for some reason,
you can specify directories to scan and concrete files to include using
`autoload_directories` and `autoload_files` array parameters:
```
parameters:
autoload_directories:
- %rootDir%/../../../build
autoload_files:
- %rootDir%/../../../generated/routes/GeneratedRouteList.php
```
`%rootDir%` is expanded to the root directory where PHPStan resides.
#### Autoloading for global installation
PHPStan supports global installation using [`composer global`](https://getcomposer.org/doc/03-cli.md#global) or via a [PHAR archive](#installation).
In this case, it's not part of the project autoloader, but it supports autodiscovery of the Composer autoloader
from current working directory residing in `vendor/`:
```bash
cd /path/to/project
phpstan analyse src tests # looks for autoloader at /path/to/project/vendor/autoload.php
```
If you have your dependencies installed at a different path
or you're running PHPStan from a different directory,
you can specify the path to the autoloader with the `--autoload-file|-a` option:
```bash
phpstan analyse --autoload-file=/path/to/autoload.php src tests
```
### Exclude files from analysis
If your codebase contains some files that are broken on purpose
(e. g. to test behaviour of your application on files with invalid PHP code),
you can exclude them using the `excludes_analyse` array parameter. String at each line
is used as a pattern for the [`fnmatch`](https://secure.php.net/manual/en/function.fnmatch.php) function.
```
parameters:
excludes_analyse:
- %rootDir%/../../../tests/*/data/*
```
### Include custom extensions
If your codebase contains php files with extensions other than the standard .php extension then you can add them
to the `fileExtensions` array parameter:
```
parameters:
fileExtensions:
- php
- module
- inc
```
### Universal object crates
Classes without predefined structure are common in PHP applications.
They are used as universal holders of data - any property can be set and read on them. Notable examples
include `stdClass`, `SimpleXMLElement` (these are enabled by default), objects with results of database queries etc.
Use `universalObjectCratesClasses` array parameter to let PHPStan know which classes
with these characteristics are used in your codebase:
```
parameters:
universalObjectCratesClasses:
- Dibi\Row
- Ratchet\ConnectionInterface
```
### Add non-obviously assigned variables to scope
If you use some variables from a try block in your catch blocks, set `polluteCatchScopeWithTryAssignments` boolean parameter to `true`.
```php
try {
$author = $this->getLoggedInUser();
$post = $this->postRepository->getById($id);
} catch (PostNotFoundException $e) {
// $author is probably defined here
throw new ArticleByAuthorCannotBePublished($author);
}
```
If you are enumerating over all possible situations in if-elseif branches
and PHPStan complains about undefined variables after the conditions, you can write
an else branch with throwing an exception:
```php
if (somethingIsTrue()) {
$foo = true;
} elseif (orSomethingElseIsTrue()) {
$foo = false;
} else {
throw new ShouldNotHappenException();
}
doFoo($foo);
```
I recommend leaving `polluteCatchScopeWithTryAssignments` set to `false` because it leads to a clearer and more maintainable code.
### Custom early terminating method calls
Previous example showed that if a condition branches end with throwing an exception, that branch does not have
to define a variable used after the condition branches end.
But exceptions are not the only way how to terminate execution of a method early. Some specific method calls
can be perceived by project developers also as early terminating - like a `redirect()` that stops execution
by throwing an internal exception.
```php
if (somethingIsTrue()) {
$foo = true;
} elseif (orSomethingElseIsTrue()) {
$foo = false;
} else {
$this->redirect('homepage');
}
doFoo($foo);
```
These methods can be configured by specifying a class on whose instance they are called like this:
```
parameters:
earlyTerminatingMethodCalls:
Nette\Application\UI\Presenter:
- redirect
- redirectUrl
- sendJson
- sendResponse
```
### Ignore error messages with regular expresions
If some issue in your code base is not easy to fix or just simply want to deal with it later,
you can exclude error messages from the analysis result with regular expressions:
```
parameters:
ignoreErrors:
- '#Call to an undefined method [a-zA-Z0-9\\_]+::method\(\)#'
- '#Call to an undefined method [a-zA-Z0-9\\_]+::expects\(\)#'
- '#Access to an undefined property PHPUnit_Framework_MockObject_MockObject::\$[a-zA-Z0-9_]+#'
- '#Call to an undefined method PHPUnit_Framework_MockObject_MockObject::[a-zA-Z0-9_]+\(\)#'
```
If some of the patterns do not occur in the result anymore, PHPStan will let you know
and you will have to remove the pattern from the configuration. You can turn off
this behaviour by setting `reportUnmatchedIgnoredErrors` to `false` in PHPStan configuration.
### Bootstrap file
If you need to initialize something in PHP runtime before PHPStan runs (like your own autoloader),
you can provide your own bootstrap file:
```
parameters:
bootstrap: %rootDir%/../../../phpstan-bootstrap.php
```
### Custom rules
PHPStan allows writing custom rules to check for specific situations in your own codebase. Your rule class
needs to implement the `PHPStan\Rules\Rule` interface and registered as a service in the configuration file:
```
services:
-
class: MyApp\PHPStan\Rules\DefaultValueTypesAssignedToPropertiesRule
tags:
- phpstan.rules.rule
```
For inspiration on how to implement a rule turn to [src/Rules](https://github.com/phpstan/phpstan/tree/master/src/Rules)
to see a lot of built-in rules.
Check out also [phpstan-strict-rules](https://github.com/phpstan/phpstan-strict-rules) repository for extra strict and opinionated rules for PHPStan!
### Custom error formatters
By default, PHPStan outputs found errors into tables grouped by files to be easily human-readable. To change the output, you can use the `--errorFormat` CLI option. There's an additional built-in `raw` format with one-per-line errors intended for easy parsing. You can also create your own error formatter by implementing the `PHPStan\Command\ErrorFormatter\ErrorFormatter` interface:
```php
interface ErrorFormatter
{
/**
* Formats the errors and outputs them to the console.
*
* @param \PHPStan\Command\AnalysisResult $analysisResult
* @param \Symfony\Component\Console\Style\OutputStyle $style
* @return int Error code.
*/
public function formatErrors(
AnalysisResult $analysisResult,
\Symfony\Component\Console\Style\OutputStyle $style
): int;
}
```
Register the formatter in your `phpstan.neon`:
```
errorFormatter.awesome:
class: App\PHPStan\AwesomeErrorFormatter
```
Use the name part after `errorFormatter.` as the CLI option value:
```
vendor/bin/phpstan analyse -c phpstan.neon -l 4 --errorFormat awesome src tests
```
## Class reflection extensions
Classes in PHP can expose "magical" properties and methods decided in run-time using
class methods like `__get`, `__set` and `__call`. Because PHPStan is all about static analysis
(testing code for errors without running it), it has to know about those properties and methods beforehand.
When PHPStan stumbles upon a property or a method that is unknown to built-in class reflection, it iterates
over all registered class reflection extensions until it finds one that defines the property or method.
Class reflection extension cannot have `PHPStan\Broker\Broker` (service for obtaining class reflections) injected in the constructor due to circular reference issue, but the extensions can implement `PHPStan\Reflection\BrokerAwareClassReflectionExtension` interface to obtain Broker via a setter.
### Properties class reflection extensions
This extension type must implement the following interface:
```php
namespace PHPStan\Reflection;
interface PropertiesClassReflectionExtension
{
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool;
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection;
}
```
Most likely you will also have to implement a new `PropertyReflection` class:
```php
namespace PHPStan\Reflection;
interface PropertyReflection
{
public function getType(): Type;
public function getDeclaringClass(): ClassReflection;
public function isStatic(): bool;
public function isPrivate(): bool;
public function isPublic(): bool;
}
```
This is how you register the extension in project's PHPStan config file:
```
services:
-
class: App\PHPStan\PropertiesFromAnnotationsClassReflectionExtension
tags:
- phpstan.broker.propertiesClassReflectionExtension
```
### Methods class reflection extensions
This extension type must implement the following interface:
```php
namespace PHPStan\Reflection;
interface MethodsClassReflectionExtension
{
public function hasMethod(ClassReflection $classReflection, string $methodName): bool;
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection;
}
```
Most likely you will also have to implement a new `MethodReflection` class:
```php
namespace PHPStan\Reflection;
interface MethodReflection
{
public function getDeclaringClass(): ClassReflection;
public function getPrototype(): self;
public function isStatic(): bool;
public function isPrivate(): bool;
public function isPublic(): bool;
public function getName(): string;
/**
* @return \PHPStan\Reflection\ParameterReflection[]
*/
public function getParameters(): array;
public function isVariadic(): bool;
public function getReturnType(): Type;
}
```
This is how you register the extension in project's PHPStan config file:
```
services:
-
class: App\PHPStan\EnumMethodsClassReflectionExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
```
## Dynamic return type extensions
If the return type of a method is not always the same, but depends on an argument passed to the method,
you can specify the return type by writing and registering an extension.
Because you have to write the code with the type-resolving logic, it can be as complex as you want.
After writing the sample extension, the variable `$mergedArticle` will have the correct type:
```php
$mergedArticle = $this->entityManager->merge($article);
// $mergedArticle will have the same type as $article
```
This is the interface for dynamic return type extension:
```php
namespace PHPStan\Type;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
interface DynamicMethodReturnTypeExtension
{
public function getClass(): string;
public function isMethodSupported(MethodReflection $methodReflection): bool;
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type;
}
```
And this is how you'd write the extension that correctly resolves the EntityManager::merge() return type:
```php
public function getClass(): string
{
return \Doctrine\ORM\EntityManager::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'merge';
}
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
if (count($methodCall->args) === 0) {
return $methodReflection->getReturnType();
}
$arg = $methodCall->args[0]->value;
return $scope->getType($arg);
}
```
And finally, register the extension to PHPStan in the project's config file:
```
services:
-
class: App\PHPStan\EntityManagerDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
```
There's also an analogous functionality for:
* **static methods** using `DynamicStaticMethodReturnTypeExtension` interface
and `phpstan.broker.dynamicStaticMethodReturnTypeExtension` service tag.
* **functions** using `DynamicFunctionReturnTypeExtension` interface and `phpstan.broker.dynamicFunctionReturnTypeExtension` service tag.
## Known issues
* If `include` or `require` are used in the analysed code (instead of `include_once` or `require_once`),
PHPStan will throw `Cannot redeclare class` error. Use the `_once` variants to avoid this error.
* If PHPStan crashes without outputting any error, it's quite possible that it's
because of a low memory limit set on your system. **Run PHPStan again** to read a couple of hints
what you can do to prevent the crashes.
## Code of Conduct
This project adheres to a [Contributor Code of Conduct](https://github.com/phpstan/phpstan/blob/master/CODE_OF_CONDUCT.md). By participating in this project and its community, you are expected to uphold this code.
## Contributing
Any contributions are welcome.
### Building
You can either run the whole build including linting and coding standards using
`vendor/bin/phing`
or run only tests using
`vendor/bin/phing tests`
phpcs.xml 0000666 00000004714 13436751442 0006426 0 ustar 00
tests/TestCase.php
tests/PHPStan/Analyser/NodeScopeResolverTest.php
tests/PHPStan/Analyser/NodeScopeResolverTest.php
BACKERS.md 0000666 00000001476 13436751442 0006165 0 ustar 00 # Backers
Development of PHPStan is made possible thanks to these awesome backers!
You can become one of them by [pledging on Patreon](https://www.patreon.com/phpstan).
Check out all the tiers - higher ones include additional goodies like placing
the logo of your company in PHPStan's README.
# $50+
* MessageBird
# $10+
* Scott Arciszewski
* Tomáš Votruba
* Adam Lundrigan
* Jobs Dev
# $5+
* Pavel Vondrášek
* Haralan Dobrev
* Tomas Pesek
* Jan Kuchař
* Sebastian Bergmann
* Oleg Samorai
* Lukas Unger
* Masaru Yamagishi
* Vašek Brychta
* Tommy Muehle
* Stefan Zielke
* Dennis Haarbrink
* Jakub Chábek
* Carlos C Soto
* Jan Endel
* Adam Žurek
# $1+
* Oliver Klee
* Bronek Bialek
* Rudolph Gottesheim
* Ian Den Hartog
* Andrew Barlow
* Michal Mleczko
* Lucas Dos Santos Abreu
* Martin Lukeš
* Ondřej Vodáček
src/Testing/RuleTestCase.php 0000666 00000005145 13436751442 0012046 0 ustar 00 analyser === null) {
$registry = new Registry([
$this->getRule(),
]);
$broker = $this->createBroker();
$printer = new \PhpParser\PrettyPrinter\Standard();
$fileHelper = $this->getFileHelper();
$typeSpecifier = new TypeSpecifier($printer);
$this->analyser = new Analyser(
$broker,
$this->getParser(),
$registry,
new NodeScopeResolver(
$broker,
$this->getParser(),
$printer,
new FileTypeMapper($this->getParser(), $this->getContainer()->getByType(PhpDocStringResolver::class), $this->createMock(Cache::class)),
$fileHelper,
$this->shouldPolluteScopeWithLoopInitialAssignments(),
$this->shouldPolluteCatchScopeWithTryAssignments(),
[]
),
$printer,
$typeSpecifier,
$fileHelper,
[],
null,
true,
50
);
}
return $this->analyser;
}
public function analyse(array $files, array $expectedErrors)
{
$files = array_map([$this->getFileHelper(), 'normalizePath'], $files);
$actualErrors = $this->getAnalyser()->analyse($files, false);
$this->assertInternalType('array', $actualErrors);
$strictlyTypedSprintf = function (int $line, string $message): string {
return sprintf('%02d: %s', $line, $message);
};
$expectedErrors = array_map(
function (array $error) use ($strictlyTypedSprintf): string {
if (!isset($error[0])) {
throw new \InvalidArgumentException('Missing expected error message.');
}
if (!isset($error[1])) {
throw new \InvalidArgumentException('Missing expected file line.');
}
return $strictlyTypedSprintf($error[1], $error[0]);
},
$expectedErrors
);
$actualErrors = array_map(
function (Error $error): string {
return sprintf('%02d: %s', $error->getLine(), $error->getMessage());
},
$actualErrors
);
$this->assertSame(implode("\n", $expectedErrors), implode("\n", $actualErrors));
}
protected function shouldPolluteScopeWithLoopInitialAssignments(): bool
{
return false;
}
protected function shouldPolluteCatchScopeWithTryAssignments(): bool
{
return false;
}
}
src/Testing/TestCase.php 0000666 00000014211 13436751442 0011210 0 ustar 00 create($rootDir . '/tmp', [
$containerFactory->getConfigDirectory() . '/config.level7.neon',
]);
}
return self::$container;
}
public function getParser(): \PHPStan\Parser\Parser
{
/** @var \PHPStan\Parser\Parser $parser */
$parser = $this->getContainer()->getService('directParser');
return $parser;
}
/**
* @param \PHPStan\Type\DynamicMethodReturnTypeExtension[] $dynamicMethodReturnTypeExtensions
* @param \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] $dynamicStaticMethodReturnTypeExtensions
* @return \PHPStan\Broker\Broker
*/
public function createBroker(
array $dynamicMethodReturnTypeExtensions = [],
array $dynamicStaticMethodReturnTypeExtensions = []
): Broker
{
$functionCallStatementFinder = new FunctionCallStatementFinder();
$parser = $this->getParser();
$cache = new Cache(new MemoryCacheStorage());
$methodReflectionFactory = new class($parser, $functionCallStatementFinder, $cache) implements PhpMethodReflectionFactory {
/** @var \PHPStan\Parser\Parser */
private $parser;
/** @var \PHPStan\Parser\FunctionCallStatementFinder */
private $functionCallStatementFinder;
/** @var \PHPStan\Cache\Cache */
private $cache;
/** @var \PHPStan\Broker\Broker */
public $broker;
public function __construct(
Parser $parser,
FunctionCallStatementFinder $functionCallStatementFinder,
Cache $cache
)
{
$this->parser = $parser;
$this->functionCallStatementFinder = $functionCallStatementFinder;
$this->cache = $cache;
}
public function create(
ClassReflection $declaringClass,
\ReflectionMethod $reflection,
array $phpDocParameterTypes,
Type $phpDocReturnType = null
): PhpMethodReflection
{
return new PhpMethodReflection(
$declaringClass,
$reflection,
$this->broker,
$this->parser,
$this->functionCallStatementFinder,
$this->cache,
$phpDocParameterTypes,
$phpDocReturnType
);
}
};
$phpDocStringResolver = $this->getContainer()->getByType(PhpDocStringResolver::class);
$fileTypeMapper = new FileTypeMapper($parser, $phpDocStringResolver, $cache);
$annotationsPropertiesClassReflectionExtension = new AnnotationsPropertiesClassReflectionExtension($fileTypeMapper);
$phpExtension = new PhpClassReflectionExtension($methodReflectionFactory, $fileTypeMapper, new AnnotationsMethodsClassReflectionExtension($fileTypeMapper), $annotationsPropertiesClassReflectionExtension);
$functionReflectionFactory = new class($this->getParser(), $functionCallStatementFinder, $cache) implements FunctionReflectionFactory {
/** @var \PHPStan\Parser\Parser */
private $parser;
/** @var \PHPStan\Parser\FunctionCallStatementFinder */
private $functionCallStatementFinder;
/** @var \PHPStan\Cache\Cache */
private $cache;
public function __construct(
Parser $parser,
FunctionCallStatementFinder $functionCallStatementFinder,
Cache $cache
)
{
$this->parser = $parser;
$this->functionCallStatementFinder = $functionCallStatementFinder;
$this->cache = $cache;
}
public function create(
\ReflectionFunction $function,
array $phpDocParameterTypes,
Type $phpDocReturnType = null
): FunctionReflection
{
return new FunctionReflection(
$function,
$this->parser,
$this->functionCallStatementFinder,
$this->cache,
$phpDocParameterTypes,
$phpDocReturnType
);
}
};
$broker = new Broker(
[
$phpExtension,
$annotationsPropertiesClassReflectionExtension,
new UniversalObjectCratesClassReflectionExtension([\stdClass::class]),
new PhpDefectClassReflectionExtension($this->getContainer()->getByType(TypeStringResolver::class)),
],
[$phpExtension],
$dynamicMethodReturnTypeExtensions,
$dynamicStaticMethodReturnTypeExtensions,
[
new AllArgumentBasedFunctionReturnTypeExtension(),
new ArgumentBasedArrayFunctionReturnTypeExtension(),
new ArgumentBasedFunctionReturnTypeExtension(),
new ArrayFilterFunctionReturnTypeReturnTypeExtension(),
new CallbackBasedArrayFunctionReturnTypeExtension(),
new CallbackBasedFunctionReturnTypeExtension(),
],
$functionReflectionFactory,
new FileTypeMapper($this->getParser(), $phpDocStringResolver, $cache)
);
$methodReflectionFactory->broker = $broker;
return $broker;
}
public function getFileHelper(): FileHelper
{
return $this->getContainer()->getByType(FileHelper::class);
}
}
src/File/FileHelper.php 0000666 00000003062 13436751442 0010760 0 ustar 00 workingDirectory = $this->normalizePath($workingDirectory);
}
public function getWorkingDirectory(): string
{
return $this->workingDirectory;
}
public function absolutizePath(string $path): string
{
if (DIRECTORY_SEPARATOR === '/') {
if (substr($path, 0, 1) === '/') {
return $path;
}
} else {
if (substr($path, 1, 1) === ':') {
return $path;
}
}
return rtrim($this->getWorkingDirectory(), '/\\') . DIRECTORY_SEPARATOR . ltrim($path, '/\\');
}
public function normalizePath(string $originalPath): string
{
if (preg_match('~^([a-z]+)\\:\\/\\/(.+)~', $originalPath, $m)) {
list(, $scheme, $path) = $m;
} else {
$scheme = null;
$path = $originalPath;
}
$path = str_replace('\\', '/', $path);
$path = preg_replace('~/{2,}~', '/', $path);
$pathRoot = strpos($path, '/') === 0 ? DIRECTORY_SEPARATOR : '';
$pathParts = explode('/', trim($path, '/'));
$normalizedPathParts = [];
foreach ($pathParts as $pathPart) {
if ($pathPart === '.') {
continue;
}
if ($pathPart === '..') {
$removedPart = array_pop($normalizedPathParts);
if ($scheme === 'phar' && substr($removedPart, -5) === '.phar') {
$scheme = null;
}
} else {
$normalizedPathParts[] = $pathPart;
}
}
return ($scheme !== null ? $scheme . '://' : '') . $pathRoot . implode(DIRECTORY_SEPARATOR, $normalizedPathParts);
}
}
src/File/FileExcluder.php 0000666 00000002177 13436751442 0011322 0 ustar 00 analyseExcludes = array_map(function (string $exclude) use ($fileHelper): string {
$normalized = $fileHelper->normalizePath($exclude);
if ($this->isFnmatchPattern($normalized)) {
return $normalized;
}
return $fileHelper->absolutizePath($normalized);
}, $analyseExcludes);
}
public function isExcludedFromAnalysing(string $file): bool
{
foreach ($this->analyseExcludes as $exclude) {
if (strpos($file, $exclude) === 0) {
return true;
}
$isWindows = DIRECTORY_SEPARATOR === '\\';
if ($isWindows) {
$fnmatchFlags = FNM_NOESCAPE | FNM_CASEFOLD;
} else {
$fnmatchFlags = 0;
}
if ($this->isFnmatchPattern($exclude) && fnmatch($exclude, $file, $fnmatchFlags)) {
return true;
}
}
return false;
}
private function isFnmatchPattern(string $path): bool
{
return preg_match('~[*?[\]]~', $path) > 0;
}
}
src/Cache/CacheStorage.php 0000666 00000000460 13436751442 0011414 0 ustar 00 directory = $directory;
if (@mkdir($this->directory) && !is_dir($this->directory)) {
throw new \InvalidArgumentException(sprintf('Directory "%s" doesn\'t exist.', $this->directory));
}
}
/**
* @param string $key
* @return mixed|null
*/
public function load(string $key)
{
return (function (string $key) {
$filePath = $this->getFilePath($key);
return is_file($filePath) ? require $this->getFilePath($key) : null;
})($key);
}
/**
* @param string $key
* @param mixed $data
* @return bool
*/
public function save(string $key, $data): bool
{
$writtenBytes = @file_put_contents(
$this->getFilePath($key),
sprintf("directory, preg_replace('~[^-\\w]~', '_', $key));
}
}
src/Cache/MemoryCacheStorage.php 0000666 00000001000 13436751442 0012574 0 ustar 00 storage) ? $this->storage[$key] : null;
}
/**
* @param string $key
* @param mixed $data
* @return bool
*/
public function save(string $key, $data): bool
{
$this->storage[$key] = $data;
return true;
}
}
src/Cache/Cache.php 0000666 00000001025 13436751442 0010065 0 ustar 00 storage = $storage;
}
/**
* @param string $key
* @return mixed|null
*/
public function load(string $key)
{
return $this->storage->load($key);
}
/**
* @param string $key
* @param mixed $data
* @return bool
*/
public function save(string $key, $data): bool
{
return $this->storage->save($key, $data);
}
}
src/Type/ResourceType.php 0000666 00000002657 13436751442 0011445 0 ustar 00 hasClass($selfClass)) {
$classReflection = $broker->getClass($selfClass);
if ($classReflection->getParentClass() !== false) {
return new ObjectType($classReflection->getParentClass()->getName());
}
}
return new NonexistentParentClassType();
default:
return new ObjectType($typeString);
}
}
public static function decideTypeFromReflection(
\ReflectionType $reflectionType = null,
Type $phpDocType = null,
string $selfClass = null,
bool $isVariadic = false
): Type
{
if ($reflectionType === null) {
return $phpDocType ?? new MixedType();
}
$reflectionTypeString = (string) $reflectionType;
if (\Nette\Utils\Strings::endsWith(strtolower($reflectionTypeString), '\\object')) {
$reflectionTypeString = 'object';
}
$type = self::getTypeObjectFromTypehint($reflectionTypeString, $selfClass);
if ($reflectionType->allowsNull()) {
$type = TypeCombinator::addNull($type);
}
if ($isVariadic) {
$type = new ArrayType(new IntegerType(), $type);
}
return self::decideType($type, $phpDocType);
}
public static function decideType(
Type $type,
Type $phpDocType = null
): Type
{
if ($phpDocType !== null) {
if ($type instanceof VoidType || $phpDocType instanceof VoidType) {
return new VoidType();
}
if (TypeCombinator::removeNull($type) instanceof IterableIterableType) {
if ($phpDocType instanceof UnionType) {
$innerTypes = [];
foreach ($phpDocType->getTypes() as $innerType) {
if ($innerType instanceof ArrayType) {
$innerTypes[] = new IterableIterableType(
$innerType->getIterableKeyType(),
$innerType->getIterableValueType()
);
} else {
$innerTypes[] = $innerType;
}
}
$phpDocType = new UnionType($innerTypes);
} elseif ($phpDocType instanceof ArrayType) {
$phpDocType = new IterableIterableType(
$phpDocType->getIterableKeyType(),
$phpDocType->getIterableValueType()
);
}
}
return $type->isSuperTypeOf($phpDocType)->yes() ? $phpDocType : $type;
}
return $type;
}
}
src/Type/NonexistentParentClassType.php 0000666 00000002673 13436751442 0014332 0 ustar 00 isSubTypeOf($this);
}
return TrinaryLogic::createNo();
}
public function isDocumentableNatively(): bool
{
return true;
}
public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getIterableKeyType(): Type
{
return new ErrorType();
}
public function getIterableValueType(): Type
{
return new ErrorType();
}
public function isCallable(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isClonable(): bool
{
return false;
}
public static function __set_state(array $properties): Type
{
return new self();
}
}
src/Type/NeverType.php 0000666 00000004206 13436751442 0010725 0 ustar 00 0,
];
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return isset($this->functionNames[strtolower($functionReflection->getName())]);
}
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
$argumentPosition = $this->functionNames[strtolower($functionReflection->getName())];
if (!isset($functionCall->args[$argumentPosition])) {
return $functionReflection->getReturnType();
}
$argumentValue = $functionCall->args[$argumentPosition]->value;
if (!$argumentValue instanceof Closure) {
return $functionReflection->getReturnType();
}
$anonymousFunctionType = $scope->getFunctionType($argumentValue->returnType, $argumentValue->returnType === null, false);
return new ArrayType(new MixedType(), $anonymousFunctionType, true);
}
}
src/Type/Php/AllArgumentBasedFunctionReturnTypeExtension.php 0000666 00000002627 13436751442 0020357 0 ustar 00 '',
'max' => '',
];
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return isset($this->functionNames[strtolower($functionReflection->getName())]);
}
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
if (!isset($functionCall->args[0])) {
return $functionReflection->getReturnType();
}
if ($functionCall->args[0]->unpack) {
$argumentType = $scope->getType($functionCall->args[0]->value);
if ($argumentType instanceof ArrayType) {
return $argumentType->getItemType();
}
}
if (count($functionCall->args) === 1) {
$argumentType = $scope->getType($functionCall->args[0]->value);
if ($argumentType instanceof ArrayType) {
return $argumentType->getItemType();
}
}
$argumentTypes = [];
foreach ($functionCall->args as $arg) {
$argumentTypes[] = $scope->getType($arg->value);
}
return TypeCombinator::union(...$argumentTypes);
}
}
src/Type/Php/CallbackBasedFunctionReturnTypeExtension.php 0000666 00000002237 13436751442 0017635 0 ustar 00 1,
];
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return isset($this->functionNames[strtolower($functionReflection->getName())]);
}
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
$argumentPosition = $this->functionNames[strtolower($functionReflection->getName())];
if (!isset($functionCall->args[$argumentPosition])) {
return $functionReflection->getReturnType();
}
$argumentValue = $functionCall->args[$argumentPosition]->value;
if (!$argumentValue instanceof Closure) {
return $functionReflection->getReturnType();
}
return $scope->getFunctionType($argumentValue->returnType, $argumentValue->returnType === null, false);
}
}
src/Type/Php/ArgumentBasedFunctionReturnTypeExtension.php 0000666 00000001765 13436751442 0017730 0 ustar 00 0,
'array_reverse' => 0,
];
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return isset($this->functionNames[strtolower($functionReflection->getName())]);
}
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
$argumentPosition = $this->functionNames[strtolower($functionReflection->getName())];
if (!isset($functionCall->args[$argumentPosition])) {
return $functionReflection->getReturnType();
}
$argumentValue = $functionCall->args[$argumentPosition]->value;
return $scope->getType($argumentValue);
}
}
src/Type/Php/ArgumentBasedArrayFunctionReturnTypeExtension.php 0000666 00000002130 13436751442 0020712 0 ustar 00 2,
'array_fill_keys' => 1,
];
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return isset($this->functionNames[strtolower($functionReflection->getName())]);
}
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
$argumentPosition = $this->functionNames[strtolower($functionReflection->getName())];
if (!isset($functionCall->args[$argumentPosition])) {
return $functionReflection->getReturnType();
}
$argumentValue = $functionCall->args[$argumentPosition]->value;
return new ArrayType(new MixedType(), $scope->getType($argumentValue), true);
}
}
src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php 0000666 00000003332 13436751442 0021465 0 ustar 00 getName(), 'array_filter') === 0;
}
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
$arrayArg = $functionCall->args[0]->value ?? null;
$callbackArg = $functionCall->args[1]->value ?? null;
$flagArg = $functionCall->args[2]->value ?? null;
if ($arrayArg !== null) {
$arrayArgType = $scope->getType($arrayArg);
$keyType = $arrayArgType->getIterableKeyType();
$itemType = $arrayArgType->getIterableValueType();
if ($flagArg === null && $callbackArg instanceof Closure && count($callbackArg->stmts) === 1) {
$statement = $callbackArg->stmts[0];
if ($statement instanceof Return_ && $statement->expr !== null && count($callbackArg->params) > 0) {
$itemVariableName = $callbackArg->params[0]->name;
$scope = $scope->assignVariable($itemVariableName, $itemType, TrinaryLogic::createYes());
$scope = $scope->filterByTruthyValue($statement->expr);
$itemType = $scope->getVariableType($itemVariableName);
}
}
} else {
$keyType = new MixedType();
$itemType = new MixedType();
}
return new ArrayType($keyType, $itemType);
}
}
src/Type/ObjectWithoutClassType.php 0000666 00000004462 13436751442 0013432 0 ustar 00 isSubTypeOf($this);
}
if ($type instanceof self || $type instanceof TypeWithClassName) {
return TrinaryLogic::createYes();
}
return TrinaryLogic::createNo();
}
public function describe(): string
{
return 'object';
}
public function canAccessProperties(): bool
{
return true;
}
public function canCallMethods(): bool
{
return true;
}
public function hasMethod(string $methodName): bool
{
return false;
}
public function getMethod(string $methodName, Scope $scope): MethodReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function canAccessConstants(): bool
{
return true;
}
public function hasConstant(string $constantName): bool
{
return false;
}
public function getConstant(string $constantName): ClassConstantReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function isDocumentableNatively(): bool
{
return true;
}
public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
public function getIterableKeyType(): Type
{
return new MixedType();
}
public function getIterableValueType(): Type
{
return new MixedType();
}
public function isCallable(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
public function isClonable(): bool
{
return true;
}
public static function __set_state(array $properties): Type
{
return new self();
}
}
src/Type/StringType.php 0000666 00000003240 13436751442 0011111 0 ustar 00 getDocComment();
if ($phpDoc !== null) {
return $phpDoc->getText();
}
return null;
}
}
src/Type/JustNullableTypeTrait.php 0000666 00000001746 13436751442 0013264 0 ustar 00 isSubTypeOf($this);
}
return TrinaryLogic::createNo();
}
public function isDocumentableNatively(): bool
{
return true;
}
public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getIterableKeyType(): Type
{
return new ErrorType();
}
public function getIterableValueType(): Type
{
return new ErrorType();
}
}
src/Type/ArrayType.php 0000666 00000007146 13436751442 0010732 0 ustar 00 keyType = $keyType;
$this->itemType = $itemType;
$this->itemTypeInferredFromLiteralArray = $itemTypeInferredFromLiteralArray;
$this->callable = $callable ?? TrinaryLogic::createMaybe()->and((new StringType)->isSuperTypeOf($itemType));
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return $this->getItemType()->getReferencedClasses();
}
public static function createDeepArrayType(NestedArrayItemType $nestedItemType, bool $nullable): self
{
$itemType = $nestedItemType->getItemType();
for ($i = 0; $i < $nestedItemType->getDepth() - 1; $i++) {
$itemType = new self(new MixedType(), $itemType, false);
}
return new self(new MixedType(), $itemType, $nullable);
}
public function isItemTypeInferredFromLiteralArray(): bool
{
return $this->itemTypeInferredFromLiteralArray;
}
public function accepts(Type $type): bool
{
if ($type instanceof self) {
return $this->getItemType()->accepts($type->getItemType())
&& $this->keyType->accepts($type->keyType);
}
if ($type instanceof CompoundType) {
return CompoundTypeHelper::accepts($type, $this);
}
return false;
}
public function isSuperTypeOf(Type $type): TrinaryLogic
{
if ($type instanceof self) {
return $this->getItemType()->isSuperTypeOf($type->getItemType())
->and($this->keyType->isSuperTypeOf($type->keyType));
}
if ($type instanceof CompoundType) {
return $type->isSubTypeOf($this);
}
return TrinaryLogic::createNo();
}
public function describe(): string
{
if ($this->keyType instanceof MixedType) {
if ($this->itemType instanceof MixedType) {
return 'array';
}
return sprintf('array<%s>', $this->itemType->describe());
}
return sprintf('array<%s, %s>', $this->keyType->describe(), $this->itemType->describe());
}
public function isDocumentableNatively(): bool
{
return true;
}
public function resolveStatic(string $className): Type
{
if ($this->getItemType() instanceof StaticResolvableType) {
return new self(
$this->keyType,
$this->getItemType()->resolveStatic($className),
$this->isItemTypeInferredFromLiteralArray(),
$this->callable
);
}
return $this;
}
public function changeBaseClass(string $className): StaticResolvableType
{
if ($this->getItemType() instanceof StaticResolvableType) {
return new self(
$this->keyType,
$this->getItemType()->changeBaseClass($className),
$this->isItemTypeInferredFromLiteralArray(),
$this->callable
);
}
return $this;
}
public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createYes();
}
public function getIterableKeyType(): Type
{
return $this->keyType;
}
public function getIterableValueType(): Type
{
return $this->getItemType();
}
public function isCallable(): TrinaryLogic
{
return $this->callable;
}
public static function __set_state(array $properties): Type
{
return new self(
$properties['keyType'],
$properties['itemType'],
$properties['itemTypeInferredFromLiteralArray'],
$properties['callable']
);
}
}
src/Type/TrueBooleanType.php 0000666 00000004543 13436751442 0012071 0 ustar 00 isSubTypeOf($this);
}
return TrinaryLogic::createNo();
}
public function isDocumentableNatively(): bool
{
return true;
}
public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getIterableKeyType(): Type
{
return new ErrorType();
}
public function getIterableValueType(): Type
{
return new ErrorType();
}
public function isCallable(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isClonable(): bool
{
return false;
}
public static function __set_state(array $properties): Type
{
return new self();
}
}
src/Type/IterableTypeTrait.php 0000666 00000002371 13436751442 0012402 0 ustar 00 itemType;
}
public function canAccessProperties(): bool
{
return false;
}
public function hasProperty(string $propertyName): bool
{
return false;
}
public function getProperty(string $propertyName, Scope $scope): PropertyReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function canCallMethods(): bool
{
return false;
}
public function hasMethod(string $methodName): bool
{
return false;
}
public function getMethod(string $methodName, Scope $scope): MethodReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function canAccessConstants(): bool
{
return false;
}
public function hasConstant(string $constantName): bool
{
return false;
}
public function getConstant(string $constantName): ClassConstantReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function isClonable(): bool
{
return false;
}
}
src/Type/IterableIterableType.php 0000666 00000006201 13436751442 0013042 0 ustar 00 keyType = $keyType;
$this->itemType = $itemType;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return $this->getItemType()->getReferencedClasses();
}
public function accepts(Type $type): bool
{
if ($type instanceof CompoundType) {
return CompoundTypeHelper::accepts($type, $this);
}
if ($type->isIterable()->yes()) {
return $this->getIterableValueType()->accepts($type->getIterableValueType())
&& $this->getIterableKeyType()->accepts($type->getIterableKeyType());
}
return false;
}
public function isSuperTypeOf(Type $type): TrinaryLogic
{
return $type->isIterable()
->and($this->getIterableValueType()->isSuperTypeOf($type->getIterableValueType()))
->and($this->getIterableKeyType()->isSuperTypeOf($type->getIterableKeyType()));
}
public function isSubTypeOf(Type $otherType): TrinaryLogic
{
if ($otherType instanceof IntersectionType || $otherType instanceof UnionType) {
return $otherType->isSuperTypeOf(new UnionType([
new ArrayType($this->keyType, $this->itemType),
new IntersectionType([
new ObjectType(\Traversable::class),
$this,
]),
]));
}
if ($otherType instanceof self) {
$limit = TrinaryLogic::createYes();
} else {
$limit = TrinaryLogic::createMaybe();
}
return $limit->and(
$otherType->isIterable(),
$otherType->getIterableValueType()->isSuperTypeOf($this->itemType),
$otherType->getIterableKeyType()->isSuperTypeOf($this->keyType)
);
}
public function describe(): string
{
if ($this->keyType instanceof MixedType) {
if ($this->itemType instanceof MixedType) {
return 'iterable';
}
return sprintf('iterable<%s>', $this->itemType->describe());
}
return sprintf('iterable<%s, %s>', $this->keyType->describe(), $this->itemType->describe());
}
public function isDocumentableNatively(): bool
{
return true;
}
public function resolveStatic(string $className): Type
{
if ($this->getItemType() instanceof StaticResolvableType) {
return new self(
$this->keyType,
$this->getItemType()->resolveStatic($className)
);
}
return $this;
}
public function changeBaseClass(string $className): StaticResolvableType
{
if ($this->getItemType() instanceof StaticResolvableType) {
return new self(
$this->keyType,
$this->getItemType()->changeBaseClass($className)
);
}
return $this;
}
public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createYes();
}
public function getIterableKeyType(): Type
{
return $this->keyType;
}
public function getIterableValueType(): Type
{
return $this->getItemType();
}
public function isCallable(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
public static function __set_state(array $properties): Type
{
return new self($properties['keyType'], $properties['itemType']);
}
}
src/Type/CallableType.php 0000666 00000004672 13436751442 0011354 0 ustar 00 isCallable()->no()) {
return true;
}
return false;
}
public function isSuperTypeOf(Type $type): TrinaryLogic
{
return $type->isCallable();
}
public function isSubTypeOf(Type $otherType): TrinaryLogic
{
if ($otherType instanceof IntersectionType || $otherType instanceof UnionType) {
return $otherType->isSuperTypeOf($this);
}
return $otherType->isCallable()
->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe());
}
public function describe(): string
{
return 'callable';
}
public function canAccessProperties(): bool
{
return false;
}
public function hasProperty(string $propertyName): bool
{
return false;
}
public function getProperty(string $propertyName, Scope $scope): PropertyReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function canCallMethods(): bool
{
return true;
}
public function hasMethod(string $methodName): bool
{
return false;
}
public function getMethod(string $methodName, Scope $scope): MethodReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function canAccessConstants(): bool
{
return false;
}
public function hasConstant(string $constantName): bool
{
return false;
}
public function getConstant(string $constantName): ClassConstantReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function isDocumentableNatively(): bool
{
return true;
}
public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
public function getIterableKeyType(): Type
{
return new MixedType();
}
public function getIterableValueType(): Type
{
return new MixedType();
}
public function isCallable(): TrinaryLogic
{
return TrinaryLogic::createYes();
}
public function isClonable(): bool
{
return false;
}
public static function __set_state(array $properties): Type
{
return new self();
}
}
src/Type/StaticType.php 0000666 00000012716 13436751442 0011102 0 ustar 00 baseClass = $baseClass;
$this->staticObjectType = new ObjectType($baseClass);
}
public function getClassName(): string
{
return $this->baseClass;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return $this->staticObjectType->getReferencedClasses();
}
public function getBaseClass(): string
{
return $this->baseClass;
}
public function accepts(Type $type): bool
{
return $this->staticObjectType->accepts($type);
}
public function isSuperTypeOf(Type $type): TrinaryLogic
{
if ($type instanceof self) {
return $this->staticObjectType->isSuperTypeOf($type);
}
if ($type instanceof ObjectType) {
return TrinaryLogic::createMaybe()->and($this->staticObjectType->isSuperTypeOf($type));
}
if ($type instanceof CompoundType) {
return $type->isSubTypeOf($this);
}
return TrinaryLogic::createNo();
}
public function describe(): string
{
return sprintf('static(%s)', $this->baseClass);
}
public function canAccessProperties(): bool
{
return $this->staticObjectType->canAccessProperties();
}
public function hasProperty(string $propertyName): bool
{
return $this->staticObjectType->hasProperty($propertyName);
}
public function getProperty(string $propertyName, Scope $scope): PropertyReflection
{
return $this->staticObjectType->getProperty($propertyName, $scope);
}
public function canCallMethods(): bool
{
return $this->staticObjectType->canCallMethods();
}
public function hasMethod(string $methodName): bool
{
return $this->staticObjectType->hasMethod($methodName);
}
public function getMethod(string $methodName, Scope $scope): MethodReflection
{
return $this->staticObjectType->getMethod($methodName, $scope);
}
public function canAccessConstants(): bool
{
return $this->staticObjectType->canAccessConstants();
}
public function hasConstant(string $constantName): bool
{
return $this->staticObjectType->hasConstant($constantName);
}
public function getConstant(string $constantName): ClassConstantReflection
{
return $this->staticObjectType->getConstant($constantName);
}
public function isDocumentableNatively(): bool
{
return $this->staticObjectType->isDocumentableNatively();
}
public function resolveStatic(string $className): Type
{
return new ObjectType($className);
}
public function changeBaseClass(string $className): StaticResolvableType
{
$thisClass = get_class($this);
return new $thisClass($className);
}
public function isIterable(): TrinaryLogic
{
$broker = Broker::getInstance();
if (!$broker->hasClass($this->baseClass)) {
return TrinaryLogic::createMaybe();
}
$classReflection = $broker->getClass($this->baseClass);
if ($classReflection->isSubclassOf(\Traversable::class) || $classReflection->getName() === \Traversable::class) {
return TrinaryLogic::createYes();
}
return TrinaryLogic::createNo();
}
public function getIterableKeyType(): Type
{
$broker = Broker::getInstance();
if (!$broker->hasClass($this->baseClass)) {
return new ErrorType();
}
$classReflection = $broker->getClass($this->baseClass);
if ($classReflection->isSubclassOf(\Iterator::class) && $classReflection->hasNativeMethod('key')) {
return $classReflection->getNativeMethod('key')->getReturnType();
}
if ($classReflection->isSubclassOf(\IteratorAggregate::class) && $classReflection->hasNativeMethod('getIterator')) {
return RecursionGuard::run($this, function () use ($classReflection) {
return $classReflection->getNativeMethod('getIterator')->getReturnType()->getIterableKeyType();
});
}
if ($classReflection->isSubclassOf(\Traversable::class)) {
return new MixedType();
}
return new ErrorType();
}
public function getIterableValueType(): Type
{
$broker = Broker::getInstance();
if (!$broker->hasClass($this->baseClass)) {
return new ErrorType();
}
$classReflection = $broker->getClass($this->baseClass);
if ($classReflection->isSubclassOf(\Iterator::class) && $classReflection->hasNativeMethod('current')) {
return $classReflection->getNativeMethod('current')->getReturnType();
}
if ($classReflection->isSubclassOf(\IteratorAggregate::class) && $classReflection->hasNativeMethod('getIterator')) {
return RecursionGuard::run($this, function () use ($classReflection) {
return $classReflection->getNativeMethod('getIterator')->getReturnType()->getIterableValueType();
});
}
if ($classReflection->isSubclassOf(\Traversable::class)) {
return new MixedType();
}
return new ErrorType();
}
public function isCallable(): TrinaryLogic
{
$broker = Broker::getInstance();
if (!$broker->hasClass($this->baseClass)) {
return TrinaryLogic::createMaybe();
}
if ($broker->getClass($this->baseClass)->hasMethod('__invoke')) {
return TrinaryLogic::createYes();
}
return TrinaryLogic::createNo();
}
public function isClonable(): bool
{
return true;
}
public static function __set_state(array $properties): Type
{
return new static($properties['baseClass']);
}
}
src/Type/TypeWithClassName.php 0000666 00000000223 13436751442 0012343 0 ustar 00 describe();
}, $types))
));
};
if (count($types) < 2) {
$throwException();
}
foreach ($types as $type) {
if ($type instanceof UnionType) {
$throwException();
}
}
$this->types = UnionTypeHelper::sortTypes($types);
}
/**
* @return \PHPStan\Type\Type[]
*/
public function getTypes(): array
{
return $this->types;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return UnionTypeHelper::getReferencedClasses($this->getTypes());
}
public function accepts(Type $type): bool
{
if ($type instanceof CompoundType) {
return CompoundTypeHelper::accepts($type, $this);
}
if (TypeCombinator::shouldSkipUnionTypeAccepts($this)) {
return true;
}
foreach ($this->getTypes() as $otherType) {
if ($otherType->accepts($type)) {
return true;
}
}
return false;
}
public function isSuperTypeOf(Type $otherType): TrinaryLogic
{
if ($otherType instanceof self || $otherType instanceof IterableIterableType) {
return $otherType->isSubTypeOf($this);
}
$results = [];
foreach ($this->getTypes() as $innerType) {
$results[] = $innerType->isSuperTypeOf($otherType);
}
return TrinaryLogic::createNo()->or(...$results);
}
public function isSubTypeOf(Type $otherType): TrinaryLogic
{
$results = [];
foreach ($this->getTypes() as $innerType) {
$results[] = $otherType->isSuperTypeOf($innerType);
}
return TrinaryLogic::extremeIdentity(...$results);
}
public function describe(): string
{
$typeNames = [];
foreach ($this->types as $type) {
if ($type instanceof IntersectionType) {
$typeNames[] = sprintf('(%s)', $type->describe());
} else {
$typeNames[] = $type->describe();
}
}
return implode('|', $typeNames);
}
public function canAccessProperties(): bool
{
foreach ($this->types as $type) {
if (!$type->canAccessProperties()) {
return false;
}
}
return true;
}
public function hasProperty(string $propertyName): bool
{
foreach ($this->types as $type) {
if ($type instanceof NullType) {
continue;
}
if (!$type->hasProperty($propertyName)) {
return false;
}
}
return true;
}
public function getProperty(string $propertyName, Scope $scope): PropertyReflection
{
foreach ($this->types as $type) {
if ($type instanceof NullType) {
continue;
}
return $type->getProperty($propertyName, $scope);
}
throw new \PHPStan\ShouldNotHappenException();
}
public function canCallMethods(): bool
{
foreach ($this->types as $type) {
if (!$type->canCallMethods()) {
return false;
}
}
return true;
}
public function hasMethod(string $methodName): bool
{
foreach ($this->types as $type) {
if ($type instanceof NullType) {
continue;
}
if (!$type->hasMethod($methodName)) {
return false;
}
}
return true;
}
public function getMethod(string $methodName, Scope $scope): MethodReflection
{
foreach ($this->types as $type) {
if ($type instanceof NullType) {
continue;
}
return $type->getMethod($methodName, $scope);
}
throw new \PHPStan\ShouldNotHappenException();
}
public function canAccessConstants(): bool
{
foreach ($this->types as $type) {
if (!$type->canAccessConstants()) {
return false;
}
}
return true;
}
public function hasConstant(string $constantName): bool
{
foreach ($this->types as $type) {
if ($type instanceof NullType) {
continue;
}
if (!$type->hasConstant($constantName)) {
return false;
}
}
return true;
}
public function getConstant(string $constantName): ClassConstantReflection
{
foreach ($this->types as $type) {
if ($type instanceof NullType) {
continue;
}
return $type->getConstant($constantName);
}
throw new \PHPStan\ShouldNotHappenException();
}
public function isDocumentableNatively(): bool
{
return false;
}
public function resolveStatic(string $className): Type
{
return new self(UnionTypeHelper::resolveStatic($className, $this->getTypes()));
}
public function changeBaseClass(string $className): StaticResolvableType
{
return new self(UnionTypeHelper::changeBaseClass($className, $this->getTypes()));
}
public function isIterable(): TrinaryLogic
{
return $this->unionResults(function (Type $type): TrinaryLogic {
return $type->isIterable();
});
}
public function getIterableKeyType(): Type
{
return $this->unionTypes(function (Type $type): Type {
return $type->getIterableKeyType();
});
}
public function getIterableValueType(): Type
{
return $this->unionTypes(function (Type $type): Type {
return $type->getIterableValueType();
});
}
public function isCallable(): TrinaryLogic
{
return $this->unionResults(function (Type $type): TrinaryLogic {
return $type->isCallable();
});
}
public function isClonable(): bool
{
foreach ($this->types as $type) {
if (!$type->isClonable()) {
return false;
}
}
return true;
}
public static function __set_state(array $properties): Type
{
return new self($properties['types']);
}
private function unionResults(callable $getResult): TrinaryLogic
{
return TrinaryLogic::extremeIdentity(...array_map($getResult, $this->types));
}
private function unionTypes(callable $getType): Type
{
return TypeCombinator::union(...array_map($getType, $this->types));
}
}
src/Type/UnionTypeHelper.php 0000666 00000002713 13436751442 0012077 0 ustar 00 $type) {
if ($type instanceof StaticResolvableType) {
$types[$i] = $type->resolveStatic($className);
}
}
return $types;
}
/**
* @param string $className
* @param \PHPStan\Type\Type[] $types
* @return \PHPStan\Type\Type[]
*/
public static function changeBaseClass(string $className, array $types): array
{
foreach ($types as $i => $type) {
if ($type instanceof StaticResolvableType) {
$types[$i] = $type->changeBaseClass($className);
}
}
return $types;
}
/**
* @param \PHPStan\Type\Type[] $types
* @return string[]
*/
public static function getReferencedClasses(array $types): array
{
$subTypeClasses = [];
foreach ($types as $type) {
$subTypeClasses[] = $type->getReferencedClasses();
}
return array_merge(...$subTypeClasses);
}
/**
* @param \PHPStan\Type\Type[] $types
* @return \PHPStan\Type\Type[]
*/
public static function sortTypes(array $types): array
{
usort($types, function (Type $a, Type $b): int {
if ($a instanceof NullType) {
return 1;
} elseif ($b instanceof NullType) {
return -1;
}
return strcasecmp($a->describe(), $b->describe());
});
return $types;
}
}
src/Type/DynamicStaticMethodReturnTypeExtension.php 0000666 00000000725 13436751442 0016642 0 ustar 00 className = $className;
}
public function getClassName(): string
{
return $this->className;
}
public function hasProperty(string $propertyName): bool
{
$broker = Broker::getInstance();
if (!$broker->hasClass($this->className)) {
return false;
}
return $broker->getClass($this->className)->hasProperty($propertyName);
}
public function getProperty(string $propertyName, Scope $scope): PropertyReflection
{
$broker = Broker::getInstance();
return $broker->getClass($this->className)->getProperty($propertyName, $scope);
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return [$this->className];
}
public function accepts(Type $type): bool
{
if ($type instanceof StaticType) {
return $this->checkSubclassAcceptability($type->getBaseClass());
}
if ($type instanceof CompoundType) {
return CompoundTypeHelper::accepts($type, $this);
}
if (!$type instanceof TypeWithClassName) {
return false;
}
return $this->checkSubclassAcceptability($type->getClassName());
}
public function isSuperTypeOf(Type $type): TrinaryLogic
{
if ($type instanceof CompoundType) {
return $type->isSubTypeOf($this);
}
if ($type instanceof ObjectWithoutClassType) {
return TrinaryLogic::createMaybe();
}
if (!$type instanceof TypeWithClassName) {
return TrinaryLogic::createNo();
}
$thisClassName = $this->className;
$thatClassName = $type->getClassName();
if ($thatClassName === $thisClassName) {
return TrinaryLogic::createYes();
}
$broker = Broker::getInstance();
if (!$broker->hasClass($thisClassName) || !$broker->hasClass($thatClassName)) {
return TrinaryLogic::createMaybe();
}
$thisClassReflection = $broker->getClass($thisClassName);
$thatClassReflection = $broker->getClass($thatClassName);
if ($thisClassReflection->getName() === $thatClassReflection->getName()) {
return TrinaryLogic::createYes();
}
if ($thatClassReflection->isSubclassOf($thisClassName)) {
return TrinaryLogic::createYes();
}
if ($thisClassReflection->isSubclassOf($thatClassName)) {
return TrinaryLogic::createMaybe();
}
if ($thisClassReflection->isInterface() && !$thatClassReflection->getNativeReflection()->isFinal()) {
return TrinaryLogic::createMaybe();
}
if ($thatClassReflection->isInterface() && !$thisClassReflection->getNativeReflection()->isFinal()) {
return TrinaryLogic::createMaybe();
}
return TrinaryLogic::createNo();
}
private function checkSubclassAcceptability(string $thatClass): bool
{
if ($this->className === $thatClass) {
return true;
}
$broker = Broker::getInstance();
if (!$broker->hasClass($this->className) || !$broker->hasClass($thatClass)) {
return false;
}
$thisReflection = $broker->getClass($this->className);
$thatReflection = $broker->getClass($thatClass);
if ($thisReflection->getName() === $thatReflection->getName()) {
// class alias
return true;
}
if ($thisReflection->isInterface() && $thatReflection->isInterface()) {
return $thatReflection->getNativeReflection()->implementsInterface($this->className);
}
return $thatReflection->isSubclassOf($this->className);
}
public function describe(): string
{
return $this->className;
}
public function canAccessProperties(): bool
{
return true;
}
public function canCallMethods(): bool
{
return strtolower($this->className) !== 'stdclass';
}
public function hasMethod(string $methodName): bool
{
$broker = Broker::getInstance();
if (!$broker->hasClass($this->className)) {
return false;
}
return $broker->getClass($this->className)->hasMethod($methodName);
}
public function getMethod(string $methodName, Scope $scope): MethodReflection
{
$broker = Broker::getInstance();
return $broker->getClass($this->className)->getMethod($methodName, $scope);
}
public function canAccessConstants(): bool
{
return true;
}
public function hasConstant(string $constantName): bool
{
$broker = Broker::getInstance();
if (!$broker->hasClass($this->className)) {
return false;
}
return $broker->getClass($this->className)->hasConstant($constantName);
}
public function getConstant(string $constantName): ClassConstantReflection
{
$broker = Broker::getInstance();
return $broker->getClass($this->className)->getConstant($constantName);
}
public function isDocumentableNatively(): bool
{
return true;
}
public function isIterable(): TrinaryLogic
{
$broker = Broker::getInstance();
if (!$broker->hasClass($this->className)) {
return TrinaryLogic::createMaybe();
}
$classReflection = $broker->getClass($this->className);
if ($classReflection->isSubclassOf(\Traversable::class) || $classReflection->getName() === \Traversable::class) {
return TrinaryLogic::createYes();
}
if ($classReflection->isInterface()) {
return TrinaryLogic::createMaybe();
}
return TrinaryLogic::createNo();
}
public function getIterableKeyType(): Type
{
$broker = Broker::getInstance();
if (!$broker->hasClass($this->className)) {
return new ErrorType();
}
$classReflection = $broker->getClass($this->className);
if ($classReflection->isSubclassOf(\Iterator::class) && $classReflection->hasNativeMethod('key')) {
return $classReflection->getNativeMethod('key')->getReturnType();
}
if ($classReflection->isSubclassOf(\IteratorAggregate::class) && $classReflection->hasNativeMethod('getIterator')) {
return RecursionGuard::run($this, function () use ($classReflection) {
return $classReflection->getNativeMethod('getIterator')->getReturnType()->getIterableKeyType();
});
}
if ($classReflection->isSubclassOf(\Traversable::class)) {
return new MixedType();
}
return new ErrorType();
}
public function getIterableValueType(): Type
{
$broker = Broker::getInstance();
if (!$broker->hasClass($this->className)) {
return new ErrorType();
}
$classReflection = $broker->getClass($this->className);
if ($classReflection->isSubclassOf(\Iterator::class) && $classReflection->hasNativeMethod('current')) {
return $classReflection->getNativeMethod('current')->getReturnType();
}
if ($classReflection->isSubclassOf(\IteratorAggregate::class) && $classReflection->hasNativeMethod('getIterator')) {
return RecursionGuard::run($this, function () use ($classReflection) {
return $classReflection->getNativeMethod('getIterator')->getReturnType()->getIterableValueType();
});
}
if ($classReflection->isSubclassOf(\Traversable::class)) {
return new MixedType();
}
return new ErrorType();
}
public function isCallable(): TrinaryLogic
{
$broker = Broker::getInstance();
if (!$broker->hasClass($this->className)) {
return TrinaryLogic::createMaybe();
}
if ($broker->getClass($this->className)->hasNativeMethod('__invoke')) {
return TrinaryLogic::createYes();
}
return TrinaryLogic::createNo();
}
public function isClonable(): bool
{
return true;
}
public static function __set_state(array $properties): Type
{
return new self($properties['className']);
}
}
src/Type/IntegerType.php 0000666 00000002651 13436751442 0011245 0 ustar 00 isSubTypeOf($this);
}
return TrinaryLogic::createNo();
}
public function isDocumentableNatively(): bool
{
return true;
}
public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getIterableKeyType(): Type
{
return new ErrorType();
}
public function getIterableValueType(): Type
{
return new ErrorType();
}
public function isCallable(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isClonable(): bool
{
return false;
}
public static function __set_state(array $properties): Type
{
return new self();
}
}
src/Type/CompoundType.php 0000666 00000000275 13436751442 0011434 0 ustar 00 isExplicitMixed = $isExplicitMixed;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return [];
}
public function accepts(Type $type): bool
{
return true;
}
public function isSuperTypeOf(Type $type): TrinaryLogic
{
return TrinaryLogic::createYes();
}
public function isSubTypeOf(Type $otherType): TrinaryLogic
{
if ($otherType instanceof self) {
return TrinaryLogic::createYes();
}
return TrinaryLogic::createMaybe();
}
public function describe(): string
{
return 'mixed';
}
public function canAccessProperties(): bool
{
return true;
}
public function hasProperty(string $propertyName): bool
{
return false;
}
public function getProperty(string $propertyName, Scope $scope): PropertyReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function canCallMethods(): bool
{
return true;
}
public function hasMethod(string $methodName): bool
{
return false;
}
public function getMethod(string $methodName, Scope $scope): MethodReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function canAccessConstants(): bool
{
return true;
}
public function hasConstant(string $constantName): bool
{
return false;
}
public function getConstant(string $constantName): ClassConstantReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function isDocumentableNatively(): bool
{
return true;
}
public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
public function getIterableKeyType(): Type
{
return new MixedType();
}
public function getIterableValueType(): Type
{
return new MixedType();
}
public function isCallable(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
public function isClonable(): bool
{
return true;
}
public function isExplicitMixed(): bool
{
return $this->isExplicitMixed;
}
public static function __set_state(array $properties): Type
{
return new self($properties['isExplicitMixed']);
}
}
src/Type/VoidType.php 0000666 00000004163 13436751442 0010551 0 ustar 00 isSubTypeOf($this);
}
return TrinaryLogic::createNo();
}
public function describe(): string
{
return 'void';
}
public function canAccessProperties(): bool
{
return false;
}
public function hasProperty(string $propertyName): bool
{
return false;
}
public function getProperty(string $propertyName, Scope $scope): PropertyReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function canCallMethods(): bool
{
return false;
}
public function hasMethod(string $methodName): bool
{
return false;
}
public function getMethod(string $methodName, Scope $scope): MethodReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function canAccessConstants(): bool
{
return false;
}
public function hasConstant(string $constantName): bool
{
return false;
}
public function getConstant(string $constantName): ClassConstantReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function isDocumentableNatively(): bool
{
return true;
}
public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getIterableKeyType(): Type
{
return new ErrorType();
}
public function getIterableValueType(): Type
{
return new ErrorType();
}
public function isCallable(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isClonable(): bool
{
return false;
}
public static function __set_state(array $properties): Type
{
return new self();
}
}
src/Type/FloatType.php 0000666 00000004514 13436751442 0010715 0 ustar 00 isSubTypeOf($this);
}
return TrinaryLogic::createNo();
}
public function describe(): string
{
return 'float';
}
public function canAccessProperties(): bool
{
return false;
}
public function hasProperty(string $propertyName): bool
{
return false;
}
public function getProperty(string $propertyName, Scope $scope): PropertyReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function canCallMethods(): bool
{
return false;
}
public function hasMethod(string $methodName): bool
{
return false;
}
public function getMethod(string $methodName, Scope $scope): MethodReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function canAccessConstants(): bool
{
return false;
}
public function hasConstant(string $constantName): bool
{
return false;
}
public function getConstant(string $constantName): ClassConstantReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function isDocumentableNatively(): bool
{
return true;
}
public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getIterableKeyType(): Type
{
return new ErrorType();
}
public function getIterableValueType(): Type
{
return new ErrorType();
}
public function isCallable(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isClonable(): bool
{
return false;
}
public static function __set_state(array $properties): Type
{
return new self();
}
}
src/Type/CompoundTypeHelper.php 0000666 00000000454 13436751442 0012573 0 ustar 00 isSubTypeOf($otherType)->yes();
}
}
src/Type/FileTypeMapper.php 0000666 00000014164 13436751442 0011676 0 ustar 00 phpParser = $phpParser;
$this->phpDocStringResolver = $phpDocStringResolver;
$this->cache = $cache;
}
public function getResolvedPhpDoc(string $filename, string $className = null, string $docComment): ResolvedPhpDocBlock
{
$key = md5($docComment);
if (isset($this->inProcess[$filename])) {
if (isset($this->inProcess[$filename][$className][$key])) {
$data = $this->inProcess[$filename][$className][$key];
if (is_callable($data)) {
$this->inProcess[$filename][$className][$key] = false;
$this->inProcess[$filename][$className][$key] = $data();
} elseif ($data === false) { // PHPDoc has cyclic dependency
return $this->phpDocStringResolver->resolve('/** nothing */', new NameScope(null, []));
}
assert($this->inProcess[$filename][$className][$key] instanceof ResolvedPhpDocBlock);
return $this->inProcess[$filename][$className][$key];
}
}
$map = $this->getResolvedPhpDocMap($filename, $className);
if (!isset($map[$key])) { // most likely wrong $fileName due to traits
return $this->phpDocStringResolver->resolve('/** nothing */', new NameScope(null, []));
}
return $map[$key];
}
/**
* @param string $fileName
* @param string|null $className
* @return \PHPStan\PhpDoc\ResolvedPhpDocBlock[]
*/
private function getResolvedPhpDocMap(string $fileName, string $className = null): array
{
if (!isset($this->memoryCache[$fileName])) {
$cacheKey = sprintf('%s-%d-v26', $fileName, filemtime($fileName));
$map = $this->cache->load($cacheKey);
if ($map === null) {
$map = $this->createResolvedPhpDocMap($fileName);
$this->cache->save($cacheKey, $map);
}
$this->memoryCache[$fileName] = $map;
}
if (!array_key_exists($className, $this->memoryCache[$fileName])) {
// class with traits - class has no phpDocs but trait has some
return [];
}
return $this->memoryCache[$fileName][$className];
}
/**
* @param string $fileName
* @return \PHPStan\PhpDoc\ResolvedPhpDocBlock[][]
*/
private function createResolvedPhpDocMap(string $fileName): array
{
$phpDocMap = [];
/** @var \PhpParser\Node\Stmt\ClassLike[] $classStack */
$classStack = [];
$namespace = null;
$uses = [];
$this->processNodes(
$this->phpParser->parseFile($fileName),
function (\PhpParser\Node $node) use (&$phpDocMap, &$classStack, &$namespace, &$uses) {
if ($node instanceof Node\Stmt\ClassLike) {
$classStack[] = $node;
} elseif ($node instanceof \PhpParser\Node\Stmt\Namespace_) {
$namespace = (string) $node->name;
} elseif ($node instanceof \PhpParser\Node\Stmt\Use_ && $node->type === \PhpParser\Node\Stmt\Use_::TYPE_NORMAL) {
foreach ($node->uses as $use) {
$uses[$use->alias] = (string) $use->name;
}
} elseif ($node instanceof \PhpParser\Node\Stmt\GroupUse) {
$prefix = (string) $node->prefix;
foreach ($node->uses as $use) {
if ($node->type === \PhpParser\Node\Stmt\Use_::TYPE_NORMAL || $use->type === \PhpParser\Node\Stmt\Use_::TYPE_NORMAL) {
$uses[$use->alias] = sprintf('%s\\%s', $prefix, $use->name);
}
}
} elseif (!in_array(get_class($node), [
Node\Stmt\Property::class,
Node\Stmt\ClassMethod::class,
Node\Stmt\Function_::class,
Node\Stmt\Foreach_::class,
Node\Expr\Assign::class,
Node\Expr\AssignRef::class,
Node\Stmt\Class_::class,
], true)) {
return;
}
$phpDocString = CommentHelper::getDocComment($node);
if ($phpDocString === null) {
return;
}
$className = count($classStack) > 0 ? $classStack[count($classStack) - 1]->name : null;
if ($className !== null && $namespace !== null) {
$className = sprintf('%s\\%s', $namespace, $className);
}
$nameScope = new NameScope($namespace, $uses, $className);
$phpDocMap[$className][md5($phpDocString)] = function () use ($phpDocString, $nameScope): ResolvedPhpDocBlock {
return $this->phpDocStringResolver->resolve($phpDocString, $nameScope);
};
},
function (\PhpParser\Node $node) use (&$namespace, &$classStack, &$uses) {
if ($node instanceof Node\Stmt\ClassLike) {
if (count($classStack) === 0) {
throw new \PHPStan\ShouldNotHappenException();
}
array_pop($classStack);
} elseif ($node instanceof \PhpParser\Node\Stmt\Namespace_) {
$namespace = null;
$uses = [];
}
}
);
try {
$this->inProcess[$fileName] = $phpDocMap;
foreach ($phpDocMap as $className => $classMap) {
foreach ($classMap as $phpDocKey => $resolveCallback) {
$this->inProcess[$fileName][$className][$phpDocKey] = false;
$this->inProcess[$fileName][$className][$phpDocKey] = $resolveCallback();
$phpDocMap[$className][$phpDocKey] = $this->inProcess[$fileName][$className][$phpDocKey];
}
}
} finally {
unset($this->inProcess[$fileName]);
}
return $phpDocMap;
}
/**
* @param \PhpParser\Node[]|\PhpParser\Node $node
* @param \Closure $nodeCallback
* @param \Closure $endNodeCallback
*/
private function processNodes($node, \Closure $nodeCallback, \Closure $endNodeCallback)
{
if ($node instanceof Node) {
$nodeCallback($node);
foreach ($node->getSubNodeNames() as $subNodeName) {
$subNode = $node->{$subNodeName};
$this->processNodes($subNode, $nodeCallback, $endNodeCallback);
}
$endNodeCallback($node);
} elseif (is_array($node)) {
foreach ($node as $subNode) {
$this->processNodes($subNode, $nodeCallback, $endNodeCallback);
}
}
}
}
src/Type/TypeCombinator.php 0000666 00000013643 13436751442 0011750 0 ustar 00 getTypes() as $unionTypeToRemove) {
$fromType = self::remove($fromType, $unionTypeToRemove);
}
return $fromType;
}
if ($fromType instanceof TrueOrFalseBooleanType) {
if ($typeToRemove instanceof TrueBooleanType) {
return new FalseBooleanType();
} elseif ($typeToRemove instanceof FalseBooleanType) {
return new TrueBooleanType();
}
} elseif ($fromType instanceof UnionType) {
$innerTypes = [];
foreach ($fromType->getTypes() as $innerType) {
$innerTypes[] = self::remove($innerType, $typeToRemove);
}
return self::union(...$innerTypes);
}
if ($typeToRemove->isSuperTypeOf($fromType)->yes()) {
return new NeverType();
}
return $fromType;
}
public static function removeNull(Type $type): Type
{
return self::remove($type, new NullType());
}
public static function containsNull(Type $type): bool
{
if ($type instanceof UnionType) {
foreach ($type->getTypes() as $innerType) {
if ($innerType instanceof NullType) {
return true;
}
}
return false;
}
return $type instanceof NullType;
}
public static function union(Type ...$types): Type
{
// transform A | (B | C) to A | B | C
for ($i = 0; $i < count($types); $i++) {
if ($types[$i] instanceof UnionType) {
array_splice($types, $i, 1, $types[$i]->getTypes());
}
}
// simplify true | false to bool
// simplify string[] | int[] to (string|int)[]
for ($i = 0; $i < count($types); $i++) {
for ($j = $i + 1; $j < count($types); $j++) {
if ($types[$i] instanceof TrueBooleanType && $types[$j] instanceof FalseBooleanType) {
$types[$i] = new TrueOrFalseBooleanType();
array_splice($types, $j, 1);
continue 2;
} elseif ($types[$i] instanceof FalseBooleanType && $types[$j] instanceof TrueBooleanType) {
$types[$i] = new TrueOrFalseBooleanType();
array_splice($types, $j, 1);
continue 2;
} elseif ($types[$i] instanceof ArrayType && $types[$j] instanceof ArrayType) {
$types[$i] = new ArrayType(
self::union($types[$i]->getIterableKeyType(), $types[$j]->getIterableKeyType()),
self::union($types[$i]->getIterableValueType(), $types[$j]->getIterableValueType()),
$types[$i]->isItemTypeInferredFromLiteralArray() || $types[$j]->isItemTypeInferredFromLiteralArray(),
$types[$i]->isCallable()->and($types[$j]->isCallable())
);
array_splice($types, $j, 1);
continue 2;
} elseif ($types[$i] instanceof IterableIterableType && $types[$j] instanceof IterableIterableType) {
$types[$i] = new IterableIterableType(
self::union($types[$i]->getIterableKeyType(), $types[$j]->getIterableKeyType()),
self::union($types[$i]->getIterableValueType(), $types[$j]->getIterableValueType())
);
array_splice($types, $j, 1);
continue 2;
}
}
}
// transform A | A to A
// transform A | never to A
// transform true | bool to bool
for ($i = 0; $i < count($types); $i++) {
for ($j = $i + 1; $j < count($types); $j++) {
if ($types[$j]->isSuperTypeOf($types[$i])->yes()) {
array_splice($types, $i--, 1);
continue 2;
} elseif ($types[$i]->isSuperTypeOf($types[$j])->yes()) {
array_splice($types, $j--, 1);
continue 1;
}
}
}
if (count($types) === 0) {
return new NeverType();
} elseif (count($types) === 1) {
return $types[0];
}
return new UnionType($types);
}
public static function intersect(Type ...$types): Type
{
// transform A & (B | C) to (A & B) | (A & C)
foreach ($types as $i => $type) {
if ($type instanceof UnionType) {
$topLevelUnionSubTypes = [];
foreach ($type->getTypes() as $innerUnionSubType) {
$topLevelUnionSubTypes[] = self::intersect(
$innerUnionSubType,
...array_slice($types, 0, $i),
...array_slice($types, $i + 1)
);
}
return self::union(...$topLevelUnionSubTypes);
}
}
// transform A & (B & C) to A & B & C
foreach ($types as $i => &$type) {
if ($type instanceof IntersectionType) {
array_splice($types, $i, 1, $type->getTypes());
}
}
// transform IntegerType & ConstantIntegerType to ConstantIntegerType
// transform Child & Parent to Child
// transform Object & ~null to Object
// transform A & A to A
// transform int[] & string to never
// transform callable & int to never
// transform A & ~A to never
// transform int & string to never
for ($i = 0; $i < count($types); $i++) {
for ($j = $i + 1; $j < count($types); $j++) {
$isSuperTypeA = $types[$j]->isSuperTypeOf($types[$i]);
if ($isSuperTypeA->no()) {
return new NeverType();
} elseif ($isSuperTypeA->yes()) {
array_splice($types, $j--, 1);
continue;
}
$isSuperTypeB = $types[$i]->isSuperTypeOf($types[$j]);
if ($isSuperTypeB->maybe()) {
continue;
} elseif ($isSuperTypeB->yes()) {
array_splice($types, $i--, 1);
continue 2;
}
}
}
if (count($types) === 1) {
return $types[0];
} else {
return new IntersectionType($types);
}
}
public static function shouldSkipUnionTypeAccepts(UnionType $unionType): bool
{
$typesLimit = self::containsNull($unionType) ? 2 : 1;
return !self::isUnionTypesEnabled() && count($unionType->getTypes()) > $typesLimit;
}
}
src/Type/ErrorType.php 0000666 00000000527 13436751442 0010741 0 ustar 00 isSubTypeOf($this);
}
return TrinaryLogic::createNo();
}
public function describe(): string
{
return 'null';
}
public function canAccessProperties(): bool
{
return false;
}
public function hasProperty(string $propertyName): bool
{
return false;
}
public function getProperty(string $propertyName, Scope $scope): PropertyReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function canCallMethods(): bool
{
return false;
}
public function hasMethod(string $methodName): bool
{
return false;
}
public function getMethod(string $methodName, Scope $scope): MethodReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function canAccessConstants(): bool
{
return false;
}
public function hasConstant(string $constantName): bool
{
return false;
}
public function getConstant(string $constantName): ClassConstantReflection
{
throw new \PHPStan\ShouldNotHappenException();
}
public function isDocumentableNatively(): bool
{
return true;
}
public function isIterable(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getIterableKeyType(): Type
{
return new ErrorType();
}
public function getIterableValueType(): Type
{
return new ErrorType();
}
public function isCallable(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isClonable(): bool
{
return false;
}
public static function __set_state(array $properties): Type
{
return new self();
}
}
src/Type/IntersectionType.php 0000666 00000012476 13436751442 0012324 0 ustar 00 types = UnionTypeHelper::sortTypes($types);
}
/**
* @return Type[]
*/
public function getTypes(): array
{
return $this->types;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return UnionTypeHelper::getReferencedClasses($this->types);
}
public function accepts(Type $otherType): bool
{
foreach ($this->types as $type) {
if (!$type->accepts($otherType)) {
return false;
}
}
return true;
}
public function isSuperTypeOf(Type $otherType): TrinaryLogic
{
$results = [];
foreach ($this->getTypes() as $innerType) {
$results[] = $innerType->isSuperTypeOf($otherType);
}
return TrinaryLogic::createYes()->and(...$results);
}
public function isSubTypeOf(Type $otherType): TrinaryLogic
{
if ($otherType instanceof self || $otherType instanceof UnionType) {
return $otherType->isSuperTypeOf($this);
}
$results = [];
foreach ($this->getTypes() as $innerType) {
$results[] = $otherType->isSuperTypeOf($innerType);
}
return TrinaryLogic::maxMin(...$results);
}
public function describe(): string
{
$typeNames = [];
foreach ($this->types as $type) {
$typeNames[] = $type->describe();
}
return implode('&', $typeNames);
}
public function canAccessProperties(): bool
{
$result = $this->intersectResults(function (Type $type): TrinaryLogic {
return $type->canAccessProperties() ? TrinaryLogic::createYes() : TrinaryLogic::createNo();
});
return $result->yes();
}
public function hasProperty(string $propertyName): bool
{
foreach ($this->types as $type) {
if ($type->hasProperty($propertyName)) {
return true;
}
}
return false;
}
public function getProperty(string $propertyName, Scope $scope): PropertyReflection
{
foreach ($this->types as $type) {
if ($type->hasProperty($propertyName)) {
return $type->getProperty($propertyName, $scope);
}
}
throw new \PHPStan\ShouldNotHappenException();
}
public function canCallMethods(): bool
{
$result = $this->intersectResults(function (Type $type): TrinaryLogic {
return $type->canCallMethods() ? TrinaryLogic::createYes() : TrinaryLogic::createNo();
});
return $result->yes();
}
public function hasMethod(string $methodName): bool
{
foreach ($this->types as $type) {
if ($type->hasMethod($methodName)) {
return true;
}
}
return false;
}
public function getMethod(string $methodName, Scope $scope): MethodReflection
{
foreach ($this->types as $type) {
if ($type->hasMethod($methodName)) {
return $type->getMethod($methodName, $scope);
}
}
throw new \PHPStan\ShouldNotHappenException();
}
public function canAccessConstants(): bool
{
$result = $this->intersectResults(function (Type $type): TrinaryLogic {
return $type->canAccessConstants() ? TrinaryLogic::createYes() : TrinaryLogic::createNo();
});
return $result->yes();
}
public function hasConstant(string $constantName): bool
{
foreach ($this->types as $type) {
if ($type->hasConstant($constantName)) {
return true;
}
}
return false;
}
public function getConstant(string $constantName): ClassConstantReflection
{
foreach ($this->types as $type) {
if ($type->hasConstant($constantName)) {
return $type->getConstant($constantName);
}
}
throw new \PHPStan\ShouldNotHappenException();
}
public function isDocumentableNatively(): bool
{
return false;
}
public function isIterable(): TrinaryLogic
{
return $this->intersectResults(function (Type $type): TrinaryLogic {
return $type->isIterable();
});
}
public function getIterableKeyType(): Type
{
return $this->intersectTypes(function (Type $type): Type {
return $type->getIterableKeyType();
});
}
public function getIterableValueType(): Type
{
return $this->intersectTypes(function (Type $type): Type {
return $type->getIterableValueType();
});
}
public function isCallable(): TrinaryLogic
{
return $this->intersectResults(function (Type $type): TrinaryLogic {
return $type->isCallable();
});
}
public function isClonable(): bool
{
foreach ($this->types as $type) {
if ($type->isClonable()) {
return true;
}
}
return false;
}
public function resolveStatic(string $className): Type
{
return new self(UnionTypeHelper::resolveStatic($className, $this->getTypes()));
}
public function changeBaseClass(string $className): StaticResolvableType
{
return new self(UnionTypeHelper::changeBaseClass($className, $this->getTypes()));
}
public static function __set_state(array $properties): Type
{
return new self($properties['types']);
}
private function intersectResults(callable $getResult): TrinaryLogic
{
$operands = array_map($getResult, $this->types);
return TrinaryLogic::maxMin(...$operands);
}
private function intersectTypes(callable $getType): Type
{
$operands = array_map($getType, $this->types);
return TypeCombinator::intersect(...$operands);
}
}
src/Type/NestedArrayItemType.php 0000666 00000000657 13436751442 0012714 0 ustar 00 itemType = $itemType;
$this->depth = $depth;
}
public function getItemType(): Type
{
return $this->itemType;
}
public function getDepth(): int
{
return $this->depth;
}
}
src/Type/ThisType.php 0000666 00000000303 13436751442 0010547 0 ustar 00 getBaseClass());
}
}
src/Type/Type.php 0000666 00000002543 13436751442 0007727 0 ustar 00 describe();
if (isset(self::$context[$key])) {
return new ErrorType();
}
try {
self::$context[$key] = true;
return $callback();
} finally {
unset(self::$context[$key]);
}
}
}
src/Analyser/NodeScopeResolver.php 0000666 00000141312 13436751442 0013242 0 ustar 00 methods(string[]) */
private $earlyTerminatingMethodCalls;
/** @var \PHPStan\Reflection\ClassReflection|null */
private $anonymousClassReflection;
/** @var bool[] filePath(string) => bool(true) */
private $analysedFiles;
public function __construct(
Broker $broker,
Parser $parser,
\PhpParser\PrettyPrinter\Standard $printer,
FileTypeMapper $fileTypeMapper,
FileHelper $fileHelper,
bool $polluteScopeWithLoopInitialAssignments,
bool $polluteCatchScopeWithTryAssignments,
array $earlyTerminatingMethodCalls
)
{
$this->broker = $broker;
$this->parser = $parser;
$this->printer = $printer;
$this->fileTypeMapper = $fileTypeMapper;
$this->fileHelper = $fileHelper;
$this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments;
$this->polluteCatchScopeWithTryAssignments = $polluteCatchScopeWithTryAssignments;
$this->earlyTerminatingMethodCalls = $earlyTerminatingMethodCalls;
}
/**
* @param string[] $files
*/
public function setAnalysedFiles(array $files)
{
$this->analysedFiles = array_fill_keys($files, true);
}
/**
* @param \PhpParser\Node[] $nodes
* @param \PHPStan\Analyser\Scope $scope
* @param \Closure $nodeCallback
* @param \PHPStan\Analyser\Scope $closureBindScope
*/
public function processNodes(
array $nodes,
Scope $scope,
\Closure $nodeCallback,
Scope $closureBindScope = null
)
{
/** @var \PhpParser\Node|string $node */
foreach ($nodes as $i => $node) {
if (!($node instanceof \PhpParser\Node)) {
continue;
}
if ($scope->getInFunctionCall() !== null && $node instanceof Arg) {
$functionCall = $scope->getInFunctionCall();
$value = $node->value;
$parametersAcceptor = $this->findParametersAcceptorInFunctionCall($functionCall, $scope);
if ($parametersAcceptor !== null) {
$parameters = $parametersAcceptor->getParameters();
$assignByReference = false;
if (isset($parameters[$i])) {
$assignByReference = $parameters[$i]->isPassedByReference();
} elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
$lastParameter = $parameters[count($parameters) - 1];
$assignByReference = $lastParameter->isPassedByReference();
}
if ($assignByReference && $value instanceof Variable && is_string($value->name)) {
$scope = $scope->assignVariable($value->name, new MixedType(), TrinaryLogic::createYes());
}
}
}
$nodeScope = $scope;
if ($i === 0 && $closureBindScope !== null) {
$nodeScope = $closureBindScope;
}
$this->processNode($node, $nodeScope, $nodeCallback);
$scope = $this->lookForAssigns($scope, $node, TrinaryLogic::createYes());
if ($node instanceof If_) {
if ($this->findEarlyTermination($node->stmts, $scope) !== null) {
$scope = $scope->filterByFalseyValue($node->cond);
$this->processNode($node->cond, $scope, function (Node $node, Scope $inScope) use (&$scope) {
$this->specifyFetchedPropertyForInnerScope($node, $inScope, true, $scope);
});
}
} elseif ($node instanceof Node\Stmt\Declare_) {
foreach ($node->declares as $declare) {
if (
$declare->key === 'strict_types'
&& $declare->value instanceof Node\Scalar\LNumber
&& $declare->value->value === 1
) {
$scope = $scope->enterDeclareStrictTypes();
break;
}
}
} elseif (
$node instanceof FuncCall
&& $node->name instanceof Name
&& (string) $node->name === 'assert'
&& isset($node->args[0])
) {
$scope = $scope->filterByTruthyValue($node->args[0]->value);
}
}
}
private function specifyProperty(Scope $scope, Expr $expr): Scope
{
if ($expr instanceof PropertyFetch) {
return $scope->specifyFetchedPropertyFromIsset($expr);
} elseif (
$expr instanceof Expr\StaticPropertyFetch
&& $expr->class instanceof Name
&& (string) $expr->class === 'static'
) {
return $scope->specifyFetchedStaticPropertyFromIsset($expr);
}
return $scope;
}
private function specifyFetchedPropertyForInnerScope(Node $node, Scope $inScope, bool $inEarlyTermination, Scope &$scope)
{
if ($inEarlyTermination === $inScope->isNegated()) {
if ($node instanceof Isset_) {
foreach ($node->vars as $var) {
$scope = $this->specifyProperty($scope, $var);
}
} elseif (
$node instanceof FuncCall
&& $node->name instanceof Name
&& $this->broker->resolveFunctionName($node->name, $scope) === 'property_exists'
&& count($node->args) === 2
&& $node->args[1]->value instanceof Node\Scalar\String_
) {
$scope = $scope->specifyFetchedPropertyFromIsset(
new PropertyFetch($node->args[0]->value, $node->args[1]->value->value)
);
}
} else {
if ($node instanceof Expr\Empty_) {
$scope = $this->specifyProperty($scope, $node->expr);
$scope = $this->assignVariable($scope, $node->expr, TrinaryLogic::createYes());
}
}
}
private function lookForArrayDestructuringArray(Scope $scope, Node $node): Scope
{
if ($node instanceof Array_) {
foreach ($node->items as $item) {
if ($item === null) {
continue;
}
$scope = $this->lookForArrayDestructuringArray($scope, $item->value);
}
} elseif ($node instanceof Variable && is_string($node->name)) {
$scope = $scope->assignVariable($node->name, new MixedType(), TrinaryLogic::createYes());
} elseif ($node instanceof ArrayDimFetch && $node->var instanceof Variable && is_string($node->var->name)) {
$scope = $scope->assignVariable(
$node->var->name,
new MixedType(),
TrinaryLogic::createYes()
);
} elseif ($node instanceof List_) {
foreach ($node->items as $item) {
/** @var \PhpParser\Node\Expr\ArrayItem|null $itemValue */
$itemValue = $item;
if ($itemValue === null) {
continue;
}
$itemValue = $itemValue->value;
if ($itemValue instanceof Variable && is_string($itemValue->name)) {
$scope = $scope->assignVariable($itemValue->name, new MixedType(), TrinaryLogic::createYes());
} else {
$scope = $this->lookForArrayDestructuringArray($scope, $itemValue);
}
}
}
return $scope;
}
private function enterForeach(Scope $scope, Foreach_ $node): Scope
{
if ($node->keyVar !== null && $node->keyVar instanceof Variable && is_string($node->keyVar->name)) {
$scope = $scope->assignVariable($node->keyVar->name, new MixedType(), TrinaryLogic::createYes());
}
if ($node->valueVar instanceof Variable && is_string($node->valueVar->name)) {
$scope = $scope->enterForeach(
$node->expr,
$node->valueVar->name,
$node->keyVar !== null
&& $node->keyVar instanceof Variable
&& is_string($node->keyVar->name)
? $node->keyVar->name
: null
);
$comment = CommentHelper::getDocComment($node);
if ($comment !== null) {
$scope = $this->processVarAnnotation($scope, $node->valueVar->name, $comment, true);
}
}
if ($node->valueVar instanceof List_ || $node->valueVar instanceof Array_) {
$scope = $this->lookForArrayDestructuringArray($scope, $node->valueVar);
}
return $this->lookForAssigns($scope, $node->valueVar, TrinaryLogic::createYes());
}
private function processNode(\PhpParser\Node $node, Scope $scope, \Closure $nodeCallback, bool $stopImmediately = false)
{
$nodeCallback($node, $scope);
if ($stopImmediately) {
return;
}
if (
$node instanceof \PhpParser\Node\Stmt\ClassLike
) {
if ($node instanceof Node\Stmt\Trait_) {
return;
}
if (isset($node->namespacedName)) {
$scope = $scope->enterClass($this->broker->getClass((string) $node->namespacedName));
} elseif ($this->anonymousClassReflection !== null) {
$scope = $scope->enterAnonymousClass($this->anonymousClassReflection);
} else {
throw new \PHPStan\ShouldNotHappenException();
}
} elseif ($node instanceof Node\Stmt\TraitUse) {
$this->processTraitUse($node, $scope, $nodeCallback);
} elseif ($node instanceof \PhpParser\Node\Stmt\Function_) {
$scope = $this->enterFunction($scope, $node);
} elseif ($node instanceof \PhpParser\Node\Stmt\ClassMethod) {
$scope = $this->enterClassMethod($scope, $node);
} elseif ($node instanceof \PhpParser\Node\Stmt\Namespace_) {
$scope = $scope->enterNamespace((string) $node->name);
} elseif (
$node instanceof \PhpParser\Node\Expr\StaticCall
&& $node->class instanceof \PhpParser\Node\Name
&& is_string($node->name)
&& (string) $node->class === 'Closure'
&& $node->name === 'bind'
) {
$thisType = null;
if (isset($node->args[1])) {
$argValue = $node->args[1]->value;
if ($argValue instanceof Expr\ConstFetch && ((string) $argValue->name === 'null')) {
$thisType = null;
} else {
$thisType = $scope->getType($argValue);
}
}
$scopeClass = 'static';
if (isset($node->args[2])) {
$argValue = $node->args[2]->value;
$argValueType = $scope->getType($argValue);
if (count($argValueType->getReferencedClasses()) === 1) {
$scopeClass = $argValueType->getReferencedClasses()[0];
} elseif (
$argValue instanceof Expr\ClassConstFetch
&& strtolower($argValue->name) === 'class'
&& $argValue->class instanceof Name
) {
$scopeClass = $scope->resolveName($argValue->class);
} elseif ($argValue instanceof Node\Scalar\String_) {
$scopeClass = $argValue->value;
}
}
$closureBindScope = $scope->enterClosureBind($thisType, $scopeClass);
} elseif ($node instanceof Foreach_) {
$scope = $scope->exitFirstLevelStatements();
$this->processNode($node->expr, $scope, $nodeCallback);
$scope = $this->lookForAssigns($scope, $node->expr, TrinaryLogic::createYes());
$scope = $this->enterForeach($scope, $node);
if ($node->keyVar !== null) {
$this->processNode($node->keyVar, $scope, $nodeCallback);
}
$this->processNode($node->valueVar, $scope, $nodeCallback);
$scope = $this->lookForAssignsInBranches($scope, [
new StatementList($scope, $node->stmts),
new StatementList($scope, []),
], LookForAssignsSettings::insideLoop());
$scope = $this->enterForeach($scope, $node);
$this->processNodes($node->stmts, $scope->enterFirstLevelStatements(), $nodeCallback);
return;
} elseif ($node instanceof For_) {
$this->processNodes($node->init, $scope, $nodeCallback);
foreach ($node->init as $initExpr) {
$scope = $this->lookForAssigns($scope, $initExpr, TrinaryLogic::createYes());
}
$this->processNodes($node->cond, $scope, $nodeCallback);
foreach ($node->cond as $condExpr) {
$scope = $this->lookForAssigns($scope, $condExpr, TrinaryLogic::createYes());
$scope = $scope->filterByTruthyValue($condExpr);
}
$scopeLoopMightHaveRun = $this->lookForAssignsInBranches($scope, [
new StatementList($scope, $node->stmts),
new StatementList($scope, []),
], LookForAssignsSettings::insideLoop());
$scopeLoopDefinitelyRan = $this->lookForAssignsInBranches($scope, [
new StatementList($scope, $node->stmts),
], LookForAssignsSettings::insideLoop());
$this->processNodes($node->loop, $scopeLoopDefinitelyRan, $nodeCallback);
foreach ($node->loop as $loopExpr) {
$scopeLoopMightHaveRun = $this->lookForAssigns($scopeLoopMightHaveRun, $loopExpr, TrinaryLogic::createMaybe());
}
foreach ($node->cond as $condExpr) {
$scopeLoopMightHaveRun = $scopeLoopMightHaveRun->filterByTruthyValue($condExpr);
}
$this->processNodes($node->stmts, $scopeLoopMightHaveRun, $nodeCallback);
return;
} elseif ($node instanceof Catch_) {
$scope = $scope->enterCatch(
$node->types,
$node->var
);
} elseif ($node instanceof Array_) {
$scope = $scope->exitFirstLevelStatements();
foreach ($node->items as $item) {
if ($item === null) {
continue;
}
$this->processNode($item, $scope, $nodeCallback);
if ($item->key !== null) {
$scope = $this->lookForAssigns($scope, $item->key, TrinaryLogic::createYes());
}
$scope = $this->lookForAssigns($scope, $item->value, TrinaryLogic::createYes());
}
return;
} elseif ($node instanceof If_) {
$this->processNode($node->cond, $scope->exitFirstLevelStatements(), $nodeCallback);
$scope = $this->lookForAssigns(
$scope,
$node->cond,
TrinaryLogic::createYes()
);
$ifScope = $scope;
$scope = $scope->filterByTruthyValue($node->cond);
$specifyFetchedProperty = function (Node $node, Scope $inScope) use (&$scope) {
$this->specifyFetchedPropertyForInnerScope($node, $inScope, false, $scope);
};
$this->processNode($node->cond, $scope, $specifyFetchedProperty);
$this->processNodes($node->stmts, $scope->enterFirstLevelStatements(), $nodeCallback);
$elseifScope = $ifScope->filterByFalseyValue($node->cond);
foreach ($node->elseifs as $elseif) {
$this->processNode($elseif, $scope, $nodeCallback, true);
$scope = $elseifScope;
$this->processNode($elseif->cond, $scope->exitFirstLevelStatements(), $nodeCallback);
$scope = $this->lookForAssigns(
$scope,
$elseif->cond,
TrinaryLogic::createYes()
);
$scope = $scope->filterByTruthyValue($elseif->cond);
$this->processNode($elseif->cond, $scope, $specifyFetchedProperty);
$this->processNodes($elseif->stmts, $scope->enterFirstLevelStatements(), $nodeCallback);
$elseifScope = $this->lookForAssigns(
$elseifScope,
$elseif->cond,
TrinaryLogic::createYes()
)->filterByFalseyValue($elseif->cond);
}
if ($node->else !== null) {
$this->processNode($node->else, $elseifScope, $nodeCallback);
}
return;
} elseif ($node instanceof Switch_) {
$scope = $scope->exitFirstLevelStatements();
$this->processNode($node->cond, $scope, $nodeCallback);
$scope = $this->lookForAssigns(
$scope,
$node->cond,
TrinaryLogic::createYes()
);
$switchScope = $scope;
$switchConditionIsTrue = $node->cond instanceof Expr\ConstFetch && strtolower((string) $node->cond->name) === 'true';
$switchConditionGetClassExpression = null;
if (
$node->cond instanceof FuncCall
&& $node->cond->name instanceof Name
&& strtolower((string) $node->cond->name) === 'get_class'
&& isset($node->cond->args[0])
) {
$switchConditionGetClassExpression = $node->cond->args[0]->value;
}
foreach ($node->cases as $caseNode) {
$this->processNode($caseNode, $scope, $nodeCallback, true);
if ($caseNode->cond !== null) {
$this->processNode($caseNode->cond, $switchScope, $nodeCallback);
$switchScope = $this->lookForAssigns(
$switchScope,
$caseNode->cond,
TrinaryLogic::createYes()
);
$scope = $this->lookForAssigns(
$scope,
$caseNode->cond,
TrinaryLogic::createYes()
);
$caseScope = $switchScope;
if ($switchConditionIsTrue) {
$caseScope = $caseScope->filterByTruthyValue($caseNode->cond);
} elseif (
$switchConditionGetClassExpression !== null
&& $caseNode->cond instanceof Expr\ClassConstFetch
&& $caseNode->cond->class instanceof Name
&& strtolower($caseNode->cond->name) === 'class'
) {
$caseScope = $caseScope->specifyExpressionType(
$switchConditionGetClassExpression,
new ObjectType($scope->resolveName($caseNode->cond->class))
);
}
} else {
$caseScope = $switchScope;
}
$this->processNodes(
$caseNode->stmts,
$caseScope->enterFirstLevelStatements(),
$nodeCallback
);
if ($this->findEarlyTermination($caseNode->stmts, $switchScope) === null) {
foreach ($caseNode->stmts as $statement) {
$switchScope = $this->lookForAssigns($switchScope, $statement, TrinaryLogic::createMaybe());
}
} else {
$switchScope = $scope;
}
}
return;
} elseif ($node instanceof TryCatch) {
$statements = [];
$this->processNodes($node->stmts, $scope->enterFirstLevelStatements(), $nodeCallback);
$scopeForLookForAssignsInBranches = $scope;
$tryAssignmentsCertainty = $this->polluteCatchScopeWithTryAssignments ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe();
foreach ($node->stmts as $statement) {
$scope = $this->lookForAssigns($scope, $statement, $tryAssignmentsCertainty);
}
if ($node->finally !== null) {
$statements[] = new StatementList($scope, $node->stmts);
}
foreach ($node->catches as $catch) {
$this->processNode($catch, $scope, $nodeCallback);
if ($node->finally !== null) {
$statements[] = new StatementList($scope->enterCatch(
$catch->types,
$catch->var
), $catch->stmts);
}
}
if ($node->finally !== null) {
$finallyScope = $this->lookForAssignsInBranches($scopeForLookForAssignsInBranches, $statements, LookForAssignsSettings::insideFinally());
$this->processNode($node->finally, $finallyScope, $nodeCallback);
}
return;
} elseif ($node instanceof FuncCall) {
$scope = $scope->enterFunctionCall($node);
} elseif ($node instanceof Expr\StaticCall) {
$scope = $scope->enterFunctionCall($node);
} elseif ($node instanceof MethodCall) {
if (
$scope->getType($node->var)->describe() === \Closure::class
&& $node->name === 'call'
&& isset($node->args[0])
) {
$closureCallScope = $scope->enterClosureBind($scope->getType($node->args[0]->value), 'static');
}
$scope = $scope->enterFunctionCall($node);
} elseif ($node instanceof New_ && $node->class instanceof Class_) {
do {
$uniqidClass = 'AnonymousClass' . uniqid();
} while (class_exists('\\' . $uniqidClass));
$classNode = $node->class;
$classNode->name = $uniqidClass;
eval($this->printer->prettyPrint([$classNode]));
unset($classNode);
$classReflection = new \ReflectionClass('\\' . $uniqidClass);
$this->anonymousClassReflection = $this->broker->getClassFromReflection(
$classReflection,
sprintf('class@anonymous%s:%s', $scope->getFile(), $node->getLine()),
true
);
} elseif ($node instanceof BooleanNot) {
$scope = $scope->enterNegation();
} elseif ($node instanceof Unset_ || $node instanceof Isset_) {
foreach ($node->vars as $unsetVar) {
while (
$unsetVar instanceof ArrayDimFetch
|| $unsetVar instanceof PropertyFetch
) {
$unsetVar = $unsetVar->var;
}
while (
$unsetVar instanceof StaticPropertyFetch
&& $unsetVar->class instanceof Expr
) {
$unsetVar = $unsetVar->class;
}
$scope = $scope->enterExpressionAssign($unsetVar);
}
} elseif ($node instanceof Node\Stmt\Global_) {
foreach ($node->vars as $var) {
$scope = $scope->enterExpressionAssign($var);
}
}
$originalScope = $scope;
foreach ($node->getSubNodeNames() as $subNodeName) {
$scope = $originalScope;
$subNode = $node->{$subNodeName};
if (is_array($subNode)) {
$argClosureBindScope = null;
if (isset($closureBindScope) && $subNodeName === 'args') {
$argClosureBindScope = $closureBindScope;
}
if ($subNodeName === 'stmts') {
$scope = $scope->enterFirstLevelStatements();
} else {
$scope = $scope->exitFirstLevelStatements();
}
if ($node instanceof While_ && $subNodeName === 'stmts') {
$scope = $this->lookForAssigns($scope, $node->cond, TrinaryLogic::createYes());
$scope = $scope->filterByTruthyValue($node->cond);
$scope = $this->lookForAssignsInBranches($scope, [
new StatementList($scope, $node->stmts),
new StatementList($scope, []),
], LookForAssignsSettings::insideLoop());
$scope = $this->lookForAssigns($scope, $node->cond, TrinaryLogic::createYes());
$scope = $scope->filterByTruthyValue($node->cond);
}
if ($node instanceof Isset_ && $subNodeName === 'vars') {
foreach ($node->vars as $issetVar) {
$scope = $this->specifyProperty($scope, $issetVar);
}
}
if ($node instanceof MethodCall && $subNodeName === 'args') {
$scope = $this->lookForAssigns($scope, $node->var, TrinaryLogic::createYes());
}
if (
$node instanceof \PhpParser\Node\Expr\Closure
&& $subNodeName === 'stmts'
) {
$scope = $scope->enterAnonymousFunction($node->params, $node->uses, $node->returnType);
}
if ($node instanceof Do_ && $subNodeName === 'stmts') {
$scope = $this->lookForAssignsInBranches($scope, [
new StatementList($scope, $node->stmts),
new StatementList($scope, [$node->cond], true),
new StatementList($scope, []),
], LookForAssignsSettings::insideLoop());
}
$this->processNodes($subNode, $scope, $nodeCallback, $argClosureBindScope);
} elseif ($subNode instanceof \PhpParser\Node) {
if ($node instanceof Coalesce && $subNodeName === 'left') {
$scope = $this->assignVariable($scope, $subNode, TrinaryLogic::createYes());
$nodeToSpecify = $subNode;
while (
$nodeToSpecify instanceof PropertyFetch
|| $nodeToSpecify instanceof MethodCall
) {
$nodeToSpecify = $nodeToSpecify->var;
$scope = $scope->specifyExpressionType(
$nodeToSpecify,
TypeCombinator::removeNull($scope->getType($nodeToSpecify))
);
}
}
if ($node instanceof Ternary) {
if ($subNodeName === 'if') {
$scope = $scope->filterByTruthyValue($node->cond);
$this->processNode($node->cond, $scope, function (Node $node, Scope $inScope) use (&$scope) {
$this->specifyFetchedPropertyForInnerScope($node, $inScope, false, $scope);
});
} elseif ($subNodeName === 'else') {
$scope = $scope->filterByFalseyValue($node->cond);
$this->processNode($node->cond, $scope, function (Node $node, Scope $inScope) use (&$scope) {
$this->specifyFetchedPropertyForInnerScope($node, $inScope, true, $scope);
});
}
}
if ($node instanceof BooleanAnd && $subNodeName === 'right') {
$scope = $scope->filterByTruthyValue($node->left);
}
if ($node instanceof BooleanOr && $subNodeName === 'right') {
$scope = $scope->filterByFalseyValue($node->left);
}
if (($node instanceof Assign || $node instanceof AssignRef) && $subNodeName === 'var') {
$scope = $this->lookForEnterVariableAssign($scope, $node->var);
}
if ($node instanceof BinaryOp && $subNodeName === 'right') {
$scope = $this->lookForAssigns($scope, $node->left, TrinaryLogic::createYes());
}
if ($node instanceof Expr\Empty_ && $subNodeName === 'expr') {
$scope = $this->specifyProperty($scope, $node->expr);
$scope = $this->lookForEnterVariableAssign($scope, $node->expr);
}
if (
$node instanceof ArrayItem
&& $subNodeName === 'value'
&& $node->key !== null
) {
$scope = $this->lookForAssigns($scope, $node->key, TrinaryLogic::createYes());
}
if (
$node instanceof Ternary
&& ($subNodeName === 'if' || $subNodeName === 'else')
) {
$scope = $this->lookForAssigns($scope, $node->cond, TrinaryLogic::createYes());
}
if ($node instanceof Do_ && $subNodeName === 'cond') {
foreach ($node->stmts as $statement) {
$scope = $this->lookForAssigns($scope, $statement, TrinaryLogic::createYes());
}
}
$nodeScope = $scope->exitFirstLevelStatements();
if ($scope->isInFirstLevelStatement()) {
if ($node instanceof Ternary && $subNodeName !== 'cond') {
$nodeScope = $scope->enterFirstLevelStatements();
} elseif (
($node instanceof BooleanAnd || $node instanceof BinaryOp\BooleanOr)
&& $subNodeName === 'right'
) {
$nodeScope = $scope->enterFirstLevelStatements();
}
}
if ($node instanceof MethodCall && $subNodeName === 'var' && isset($closureCallScope)) {
$nodeScope = $closureCallScope->exitFirstLevelStatements();
}
$this->processNode($subNode, $nodeScope, $nodeCallback);
}
}
}
private function lookForEnterVariableAssign(Scope $scope, Expr $node): Scope
{
if ($node instanceof Variable) {
$scope = $scope->enterExpressionAssign($node);
} elseif ($node instanceof ArrayDimFetch) {
while ($node instanceof ArrayDimFetch) {
$node = $node->var;
}
if ($node instanceof Variable) {
$scope = $scope->enterExpressionAssign($node);
}
} elseif ($node instanceof List_ || $node instanceof Array_) {
foreach ($node->items as $listItem) {
if ($listItem === null) {
continue;
}
$listItemValue = $listItem;
if ($listItemValue instanceof Expr\ArrayItem) {
$listItemValue = $listItemValue->value;
}
$scope = $this->lookForEnterVariableAssign($scope, $listItemValue);
}
} else {
$scope = $scope->enterExpressionAssign($node);
}
return $scope;
}
private function lookForAssigns(
Scope $scope,
\PhpParser\Node $node,
TrinaryLogic $certainty,
LookForAssignsSettings $lookForAssignsSettings = null
): Scope
{
if ($lookForAssignsSettings === null) {
$lookForAssignsSettings = LookForAssignsSettings::default();
}
if ($node instanceof StaticVar) {
$scope = $scope->assignVariable(
$node->name,
$node->default !== null ? $scope->getType($node->default) : new MixedType(),
$certainty
);
} elseif ($node instanceof Static_) {
foreach ($node->vars as $var) {
$scope = $this->lookForAssigns($scope, $var, $certainty);
}
} elseif ($node instanceof If_) {
$scope = $this->lookForAssigns($scope, $node->cond, $certainty);
$ifStatement = new StatementList(
$scope->filterByTruthyValue($node->cond),
array_merge([$node->cond], $node->stmts)
);
$elseIfScope = $scope->filterByFalseyValue($node->cond);
$elseIfStatements = [];
foreach ($node->elseifs as $elseIf) {
$elseIfStatements[] = new StatementList($elseIfScope, array_merge([$elseIf->cond], $elseIf->stmts));
$elseIfScope = $elseIfScope->filterByFalseyValue($elseIf->cond);
}
$statements = [
$ifStatement,
new StatementList($elseIfScope, $node->else !== null ? $node->else->stmts : []),
];
$statements = array_merge($statements, $elseIfStatements);
$scope = $this->lookForAssignsInBranches($scope, $statements, $lookForAssignsSettings);
} elseif ($node instanceof TryCatch) {
$statements = [
new StatementList($scope, $node->stmts),
];
foreach ($node->catches as $catch) {
$statements[] = new StatementList($scope->enterCatch(
$catch->types,
$catch->var
), $catch->stmts);
}
$scope = $this->lookForAssignsInBranches($scope, $statements, $lookForAssignsSettings);
if ($node->finally !== null) {
foreach ($node->finally->stmts as $statement) {
$scope = $this->lookForAssigns($scope, $statement, $certainty, $lookForAssignsSettings);
}
}
} elseif ($node instanceof MethodCall || $node instanceof FuncCall || $node instanceof Expr\StaticCall) {
if ($node instanceof MethodCall) {
$scope = $this->lookForAssigns($scope, $node->var, $certainty);
}
foreach ($node->args as $argument) {
$scope = $this->lookForAssigns($scope, $argument, $certainty);
}
$parametersAcceptor = $this->findParametersAcceptorInFunctionCall($node, $scope);
if ($parametersAcceptor !== null) {
$parameters = $parametersAcceptor->getParameters();
foreach ($node->args as $i => $arg) {
$assignByReference = false;
if (isset($parameters[$i])) {
$assignByReference = $parameters[$i]->isPassedByReference();
} elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
$lastParameter = $parameters[count($parameters) - 1];
$assignByReference = $lastParameter->isPassedByReference();
}
if (!$assignByReference) {
continue;
}
$arg = $node->args[$i]->value;
if ($arg instanceof Variable && is_string($arg->name)) {
$scope = $scope->assignVariable($arg->name, new MixedType(), $certainty);
}
}
}
if (
$node instanceof FuncCall
&& $node->name instanceof Name
&& in_array((string) $node->name, [
'fopen',
'file_get_contents',
], true)
) {
$scope = $scope->assignVariable('http_response_header', new ArrayType(new IntegerType(), new StringType(), false), $certainty);
}
} elseif ($node instanceof BinaryOp) {
$scope = $this->lookForAssigns($scope, $node->left, $certainty);
$scope = $this->lookForAssigns($scope, $node->right, $certainty);
} elseif ($node instanceof Arg) {
$scope = $this->lookForAssigns($scope, $node->value, $certainty);
} elseif ($node instanceof BooleanNot) {
$scope = $this->lookForAssigns($scope, $node->expr, $certainty);
} elseif ($node instanceof Ternary) {
$scope = $this->lookForAssigns($scope, $node->cond, $certainty);
} elseif ($node instanceof Array_) {
foreach ($node->items as $item) {
if ($item === null) {
continue;
}
if ($item->key !== null) {
$scope = $this->lookForAssigns($scope, $item->key, $certainty);
}
$scope = $this->lookForAssigns($scope, $item->value, $certainty);
}
} elseif ($node instanceof New_) {
foreach ($node->args as $arg) {
$scope = $this->lookForAssigns($scope, $arg, $certainty);
}
} elseif ($node instanceof Do_) {
$scope = $this->lookForAssignsInBranches($scope, [
new StatementList($scope, $node->stmts),
], LookForAssignsSettings::afterLoop());
$scope = $this->lookForAssigns($scope, $node->cond, TrinaryLogic::createYes());
} elseif ($node instanceof Switch_) {
$statementLists = [];
$tmpStatements = [];
$hasDefault = false;
foreach ($node->cases as $case) {
if ($case->cond === null) {
$hasDefault = true;
}
foreach ($case->stmts as $statement) {
$tmpStatements[] = $statement;
if ($this->findStatementEarlyTermination($statement, $scope) !== null) {
$statementLists[] = new StatementList($scope, $tmpStatements);
$tmpStatements = [];
}
}
}
if (count($tmpStatements) > 0) {
$statementLists[] = new StatementList($scope, $tmpStatements);
}
if (!$hasDefault) {
$statementLists[] = new StatementList($scope, []);
}
$scope = $this->lookForAssignsInBranches($scope, $statementLists, LookForAssignsSettings::afterLoop());
} elseif ($node instanceof Cast) {
$scope = $this->lookForAssigns($scope, $node->expr, $certainty);
} elseif ($node instanceof For_) {
$forAssignmentsCertainty = $this->polluteScopeWithLoopInitialAssignments ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe();
foreach ($node->init as $initExpr) {
$scope = $this->lookForAssigns($scope, $initExpr, $forAssignmentsCertainty);
}
foreach ($node->cond as $condExpr) {
$scope = $this->lookForAssigns($scope, $condExpr, $forAssignmentsCertainty);
}
$statements = [
new StatementList($scope, $node->stmts),
new StatementList($scope, []), // in order not to add variables existing only inside the for loop
];
$scope = $this->lookForAssignsInBranches($scope, $statements, LookForAssignsSettings::afterLoop());
foreach ($node->loop as $loopExpr) {
$scope = $this->lookForAssigns($scope, $loopExpr, TrinaryLogic::createMaybe());
}
} elseif ($node instanceof While_) {
$whileAssignmentsCertainty = $this->polluteScopeWithLoopInitialAssignments ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe();
$scope = $this->lookForAssigns($scope, $node->cond, $whileAssignmentsCertainty);
$statements = [
new StatementList($scope, $node->stmts),
new StatementList($scope, []), // in order not to add variables existing only inside the for loop
];
$scope = $this->lookForAssignsInBranches($scope, $statements, LookForAssignsSettings::afterLoop());
} elseif ($node instanceof ErrorSuppress) {
$scope = $this->lookForAssigns($scope, $node->expr, $certainty);
} elseif ($node instanceof \PhpParser\Node\Stmt\Unset_) {
foreach ($node->vars as $var) {
if ($var instanceof Variable && is_string($var->name)) {
$scope = $scope->unsetVariable($var->name);
}
}
} elseif ($node instanceof Echo_) {
foreach ($node->exprs as $echoedExpr) {
$scope = $this->lookForAssigns($scope, $echoedExpr, $certainty);
}
} elseif ($node instanceof Print_) {
$scope = $this->lookForAssigns($scope, $node->expr, $certainty);
} elseif ($node instanceof Foreach_) {
$scope = $this->lookForAssigns($scope, $node->expr, $certainty);
$initialScope = $scope;
$scope = $this->enterForeach($scope, $node);
$statements = [
new StatementList($scope, $node->stmts),
new StatementList($scope, []), // in order not to add variables existing only inside the for loop
];
$scope = $this->lookForAssignsInBranches($initialScope, $statements, LookForAssignsSettings::afterLoop());
} elseif ($node instanceof Isset_) {
foreach ($node->vars as $var) {
$scope = $this->lookForAssigns($scope, $var, $certainty);
}
} elseif ($node instanceof Expr\Empty_) {
$scope = $this->lookForAssigns($scope, $node->expr, $certainty);
} elseif ($node instanceof ArrayDimFetch && $node->dim !== null) {
$scope = $this->lookForAssigns($scope, $node->dim, $certainty);
} elseif ($node instanceof Expr\Closure) {
foreach ($node->uses as $closureUse) {
if (!$closureUse->byRef || $scope->hasVariableType($closureUse->var)->yes()) {
continue;
}
$scope = $scope->assignVariable($closureUse->var, new MixedType(), $certainty);
}
} elseif ($node instanceof Instanceof_) {
$scope = $this->lookForAssigns($scope, $node->expr, $certainty);
} elseif ($node instanceof Expr\Include_) {
$scope = $this->lookForAssigns($scope, $node->expr, $certainty);
}
$scope = $this->updateScopeForVariableAssign($scope, $node, $certainty);
return $scope;
}
private function updateScopeForVariableAssign(Scope $scope, \PhpParser\Node $node, TrinaryLogic $certainty): Scope
{
if ($node instanceof Assign || $node instanceof AssignRef || $node instanceof Expr\AssignOp || $node instanceof Node\Stmt\Global_) {
if ($node instanceof Assign || $node instanceof AssignRef || $node instanceof Expr\AssignOp) {
$vars = [$node->var];
} elseif ($node instanceof Node\Stmt\Global_) {
$vars = $node->vars;
} else {
throw new \PHPStan\ShouldNotHappenException();
}
foreach ($vars as $var) {
$type = null;
if ($node instanceof Assign || $node instanceof AssignRef) {
$type = $scope->getType($node->expr);
} elseif ($node instanceof Expr\AssignOp) {
if (
$node->var instanceof Variable
&& is_string($node->var->name)
&& !$scope->hasVariableType($node->var->name)->yes()
) {
continue;
}
$type = $scope->getType($node);
} elseif (
$node instanceof Node\Stmt\Global_
&& $var instanceof Variable
&& is_string($var->name)
&& $scope->hasVariableType($var->name)->no()
) {
$this->assignVariable($scope, $var, $certainty);
}
$scope = $this->assignVariable($scope, $var, $certainty, $type);
}
if ($node instanceof Assign || $node instanceof AssignRef) {
if ($node->var instanceof Array_ || $node->var instanceof List_) {
$scope = $this->lookForArrayDestructuringArray($scope, $node->var);
}
}
if (!$node instanceof Node\Stmt\Global_) {
$scope = $this->lookForAssigns($scope, $node->expr, TrinaryLogic::createYes());
}
if ($node instanceof Assign || $node instanceof AssignRef) {
if ($node->var instanceof Variable && is_string($node->var->name)) {
$comment = CommentHelper::getDocComment($node);
if ($comment !== null) {
$scope = $this->processVarAnnotation($scope, $node->var->name, $comment, false);
}
}
}
}
return $scope;
}
private function processVarAnnotation(Scope $scope, string $variableName, string $comment, bool $strict): Scope
{
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$scope->getFile(),
$scope->isInClass() ? $scope->getClassReflection()->getName() : null,
$comment
);
$varTags = $resolvedPhpDoc->getVarTags();
if (isset($varTags[$variableName])) {
$variableType = $varTags[$variableName]->getType();
return $scope->assignVariable($variableName, $variableType, TrinaryLogic::createYes());
} elseif (!$strict && count($varTags) === 1 && isset($varTags[0])) {
$variableType = $varTags[0]->getType();
return $scope->assignVariable($variableName, $variableType, TrinaryLogic::createYes());
} else {
return $scope;
}
}
private function assignVariable(
Scope $scope,
Node $var,
TrinaryLogic $certainty,
Type $subNodeType = null
): Scope
{
if ($var instanceof Variable && is_string($var->name)) {
$scope = $scope->assignVariable($var->name, $subNodeType !== null ? $subNodeType : new MixedType(), $certainty);
} elseif ($var instanceof ArrayDimFetch) {
$depth = 0;
while ($var instanceof ArrayDimFetch) {
$var = $var->var;
$depth++;
}
if (isset($var->dim)) {
$scope = $this->lookForAssigns($scope, $var->dim, TrinaryLogic::createYes());
}
if ($var instanceof Variable && is_string($var->name)) {
if ($scope->hasVariableType($var->name)->yes()) {
$arrayDimFetchVariableType = $scope->getVariableType($var->name);
if (
!$arrayDimFetchVariableType instanceof ArrayType
&& !$arrayDimFetchVariableType instanceof NullType
&& !$arrayDimFetchVariableType instanceof MixedType
) {
return $scope;
}
}
$arrayType = ArrayType::createDeepArrayType(
new NestedArrayItemType($subNodeType !== null ? $subNodeType : new MixedType(), $depth),
false
);
if ($scope->hasVariableType($var->name)->yes()) {
if (
!isset($arrayDimFetchVariableType)
|| !$arrayDimFetchVariableType instanceof NullType
) {
$arrayType = TypeCombinator::union(
$scope->getVariableType($var->name),
$arrayType
);
}
}
$scope = $scope->assignVariable($var->name, $arrayType, $certainty);
}
} elseif ($var instanceof PropertyFetch && $subNodeType !== null) {
$scope = $scope->specifyExpressionType($var, $subNodeType);
} elseif ($var instanceof Expr\StaticPropertyFetch && $subNodeType !== null) {
$scope = $scope->specifyExpressionType($var, $subNodeType);
} else {
$scope = $this->lookForAssigns($scope, $var, TrinaryLogic::createYes());
}
return $scope;
}
/**
* @param \PHPStan\Analyser\Scope $initialScope
* @param \PHPStan\Analyser\StatementList[] $statementsLists
* @param \PHPStan\Analyser\LookForAssignsSettings $lookForAssignsSettings
* @return Scope
*/
private function lookForAssignsInBranches(
Scope $initialScope,
array $statementsLists,
LookForAssignsSettings $lookForAssignsSettings
): Scope
{
/** @var \PHPStan\Analyser\Scope|null $intersectedScope */
$intersectedScope = null;
foreach ($statementsLists as $i => $statementList) {
$statements = $statementList->getStatements();
$branchScope = $statementList->getScope();
$branchScopeWithInitialScopeRemoved = $branchScope->removeVariables($initialScope, true);
$earlyTerminationStatement = null;
foreach ($statements as $statement) {
$branchScope = $this->lookForAssigns($branchScope, $statement, TrinaryLogic::createYes(), $lookForAssignsSettings);
$branchScopeWithInitialScopeRemoved = $branchScope->removeVariables($initialScope, false);
$earlyTerminationStatement = $this->findStatementEarlyTermination($statement, $branchScope);
if ($earlyTerminationStatement !== null) {
if ($lookForAssignsSettings->shouldSkipBranch($earlyTerminationStatement)) {
continue 2;
}
break;
}
}
if ($lookForAssignsSettings->shouldIntersectVariables($earlyTerminationStatement)) {
if ($intersectedScope === null) {
$intersectedScope = $initialScope->createIntersectedScope($branchScopeWithInitialScopeRemoved);
} else {
$intersectedScope = $intersectedScope->intersectVariables($branchScopeWithInitialScopeRemoved);
}
if ($statementList->shouldFilterByTruthyValue()) {
/** @var \PhpParser\Node\Expr $statement */
foreach ($statements as $statement) {
$intersectedScope = $intersectedScope->filterByTruthyValue($statement);
}
}
}
}
if ($intersectedScope !== null) {
$scope = $initialScope->mergeWithIntersectedScope($intersectedScope);
return $scope;
}
return $initialScope;
}
/**
* @param \PhpParser\Node[] $statements
* @param \PHPStan\Analyser\Scope $scope
* @return \PhpParser\Node|null
*/
private function findEarlyTermination(array $statements, Scope $scope)
{
foreach ($statements as $statement) {
$statement = $this->findStatementEarlyTermination($statement, $scope);
if ($statement !== null) {
return $statement;
}
}
return null;
}
/**
* @param \PhpParser\Node $statement
* @param \PHPStan\Analyser\Scope $scope
* @return \PhpParser\Node|null
*/
private function findStatementEarlyTermination(Node $statement, Scope $scope)
{
if (
$statement instanceof Throw_
|| $statement instanceof Return_
|| $statement instanceof Continue_
|| $statement instanceof Break_
|| $statement instanceof Exit_
) {
return $statement;
} elseif ($statement instanceof MethodCall && count($this->earlyTerminatingMethodCalls) > 0) {
if (!is_string($statement->name)) {
return null;
}
$methodCalledOnType = $scope->getType($statement->var);
foreach ($methodCalledOnType->getReferencedClasses() as $referencedClass) {
if (!$this->broker->hasClass($referencedClass)) {
continue;
}
$classReflection = $this->broker->getClass($referencedClass);
foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames()) as $className) {
if (!isset($this->earlyTerminatingMethodCalls[$className])) {
continue;
}
if (in_array($statement->name, $this->earlyTerminatingMethodCalls[$className], true)) {
return $statement;
}
}
}
return null;
} elseif ($statement instanceof If_) {
if ($statement->else === null) {
return null;
}
if ($this->findEarlyTermination($statement->stmts, $scope) === null) {
return null;
}
foreach ($statement->elseifs as $elseIfStatement) {
if ($this->findEarlyTermination($elseIfStatement->stmts, $scope) === null) {
return null;
}
}
if ($this->findEarlyTermination($statement->else->stmts, $scope) === null) {
return null;
}
return $statement;
}
return null;
}
/**
* @param \PhpParser\Node\Expr $functionCall
* @param \PHPStan\Analyser\Scope $scope
* @return null|\PHPStan\Reflection\ParametersAcceptor
*/
private function findParametersAcceptorInFunctionCall(Expr $functionCall, Scope $scope)
{
if ($functionCall instanceof FuncCall && $functionCall->name instanceof Name) {
if ($this->broker->hasFunction($functionCall->name, $scope)) {
return $this->broker->getFunction($functionCall->name, $scope);
}
} elseif ($functionCall instanceof MethodCall && is_string($functionCall->name)) {
$type = $scope->getType($functionCall->var);
$methodName = $functionCall->name;
if ($type->hasMethod($methodName)) {
return $type->getMethod($methodName, $scope);
}
} elseif (
$functionCall instanceof Expr\StaticCall
&& $functionCall->class instanceof Name
&& is_string($functionCall->name)) {
$className = $scope->resolveName($functionCall->class);
if ($this->broker->hasClass($className)) {
$classReflection = $this->broker->getClass($className);
if ($classReflection->hasMethod($functionCall->name)) {
return $classReflection->getMethod($functionCall->name, $scope);
}
}
}
return null;
}
private function processTraitUse(Node\Stmt\TraitUse $node, Scope $classScope, \Closure $nodeCallback)
{
foreach ($node->traits as $trait) {
$traitName = (string) $trait;
if (!$this->broker->hasClass($traitName)) {
continue;
}
$traitReflection = $this->broker->getClass($traitName);
$traitFileName = $traitReflection->getFileName();
if ($traitFileName === false) {
throw new \PHPStan\ShouldNotHappenException();
}
$fileName = $this->fileHelper->normalizePath($traitFileName);
if (!isset($this->analysedFiles[$fileName])) {
return;
}
$parserNodes = $this->parser->parseFile($fileName);
$className = sprintf('class %s', $classScope->getClassReflection()->getDisplayName());
if ($classScope->getClassReflection()->isAnonymous()) {
$className = 'anonymous class';
}
$classScope = $classScope->changeAnalysedContextFile(
sprintf(
'%s (in context of %s)',
$fileName,
$className
)
);
$this->processNodesForTraitUse($parserNodes, $traitName, $classScope, $nodeCallback);
}
}
/**
* @param \PhpParser\Node[]|\PhpParser\Node $node
* @param string $traitName
* @param \PHPStan\Analyser\Scope $classScope
* @param \Closure $nodeCallback
*/
private function processNodesForTraitUse($node, string $traitName, Scope $classScope, \Closure $nodeCallback)
{
if ($node instanceof Node) {
if ($node instanceof Node\Stmt\Trait_ && $traitName === (string) $node->namespacedName) {
$this->processNodes($node->stmts, $classScope->enterFirstLevelStatements(), $nodeCallback);
return;
}
if ($node instanceof Node\Stmt\ClassLike) {
return;
}
foreach ($node->getSubNodeNames() as $subNodeName) {
$subNode = $node->{$subNodeName};
$this->processNodesForTraitUse($subNode, $traitName, $classScope, $nodeCallback);
}
} elseif (is_array($node)) {
foreach ($node as $subNode) {
$this->processNodesForTraitUse($subNode, $traitName, $classScope, $nodeCallback);
}
}
}
private function enterClassMethod(Scope $scope, Node\Stmt\ClassMethod $classMethod): Scope
{
list($phpDocParameterTypes, $phpDocReturnType) = $this->getPhpDocs($scope, $classMethod);
return $scope->enterClassMethod(
$classMethod,
$phpDocParameterTypes,
$phpDocReturnType
);
}
private function getPhpDocs(Scope $scope, Node\FunctionLike $functionLike): array
{
$phpDocParameterTypes = [];
$phpDocReturnType = null;
if ($functionLike->getDocComment() !== null) {
$docComment = $functionLike->getDocComment()->getText();
$file = $scope->getFile();
$class = $scope->isInClass() ? $scope->getClassReflection()->getName() : null;
if ($functionLike instanceof Node\Stmt\ClassMethod) {
$phpDocBlock = PhpDocBlock::resolvePhpDocBlockForMethod(
$this->broker,
$docComment,
$scope->getClassReflection()->getName(),
$functionLike->name,
$file
);
$docComment = $phpDocBlock->getDocComment();
$file = $phpDocBlock->getFile();
$class = $phpDocBlock->getClass();
}
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$file,
$class,
$docComment
);
$phpDocParameterTypes = array_map(function (ParamTag $tag): Type {
return $tag->getType();
}, $resolvedPhpDoc->getParamTags());
$phpDocReturnType = $resolvedPhpDoc->getReturnTag() !== null ? $resolvedPhpDoc->getReturnTag()->getType() : null;
}
return [$phpDocParameterTypes, $phpDocReturnType];
}
private function enterFunction(Scope $scope, Node\Stmt\Function_ $function): Scope
{
list($phpDocParameterTypes, $phpDocReturnType) = $this->getPhpDocs($scope, $function);
return $scope->enterFunction(
$function,
$phpDocParameterTypes,
$phpDocReturnType
);
}
}
src/Analyser/Analyser.php 0000666 00000014471 13436751442 0011424 0 ustar 00 broker = $broker;
$this->parser = $parser;
$this->registry = $registry;
$this->nodeScopeResolver = $nodeScopeResolver;
$this->printer = $printer;
$this->typeSpecifier = $typeSpecifier;
$this->ignoreErrors = $ignoreErrors;
$this->bootstrapFile = $bootstrapFile !== null ? $fileHelper->normalizePath($bootstrapFile) : null;
$this->reportUnmatchedIgnoredErrors = $reportUnmatchedIgnoredErrors;
$this->internalErrorsCountLimit = $internalErrorsCountLimit;
}
/**
* @param string[] $files
* @param bool $onlyFiles
* @param \Closure|null $preFileCallback
* @param \Closure|null $postFileCallback
* @param bool $debug
* @return string[]|\PHPStan\Analyser\Error[] errors
*/
public function analyse(
array $files,
bool $onlyFiles,
\Closure $preFileCallback = null,
\Closure $postFileCallback = null,
bool $debug = false
): array
{
$errors = [];
if ($this->bootstrapFile !== null) {
if (!is_file($this->bootstrapFile)) {
return [
sprintf('Bootstrap file %s does not exist.', $this->bootstrapFile),
];
}
try {
require_once $this->bootstrapFile;
} catch (\Throwable $e) {
return [$e->getMessage()];
}
}
foreach ($this->ignoreErrors as $ignoreError) {
try {
\Nette\Utils\Strings::match('', $ignoreError);
} catch (\Nette\Utils\RegexpException $e) {
$errors[] = $e->getMessage();
}
}
if (count($errors) > 0) {
return $errors;
}
$this->nodeScopeResolver->setAnalysedFiles($files);
$internalErrorsCount = 0;
$reachedInternalErrorsCountLimit = false;
foreach ($files as $file) {
try {
$fileErrors = [];
if ($preFileCallback !== null) {
$preFileCallback($file);
}
$this->nodeScopeResolver->processNodes(
$this->parser->parseFile($file),
new Scope($this->broker, $this->printer, $this->typeSpecifier, $file),
function (\PhpParser\Node $node, Scope $scope) use (&$fileErrors) {
foreach ($this->registry->getRules(get_class($node)) as $rule) {
$ruleErrors = $this->createErrors(
$node,
$scope->getAnalysedContextFile(),
$rule->processNode($node, $scope)
);
$fileErrors = array_merge($fileErrors, $ruleErrors);
}
}
);
if ($postFileCallback !== null) {
$postFileCallback($file);
}
$errors = array_merge($errors, $fileErrors);
} catch (\PhpParser\Error $e) {
$errors[] = new Error($e->getMessage(), $file, $e->getStartLine() !== -1 ? $e->getStartLine() : null, false);
} catch (\PHPStan\AnalysedCodeException $e) {
$errors[] = new Error($e->getMessage(), $file, null, false);
} catch (\Throwable $t) {
if ($debug) {
throw $t;
}
$internalErrorsCount++;
$internalErrorMessage = sprintf('Internal error: %s', $t->getMessage());
$internalErrorMessage .= sprintf(
'%sRun PHPStan with --debug option and post the stack trace to:%s%s',
"\n",
"\n",
'https://github.com/phpstan/phpstan/issues/new'
);
$errors[] = new Error($internalErrorMessage, $file);
if ($internalErrorsCount >= $this->internalErrorsCountLimit) {
$reachedInternalErrorsCountLimit = true;
break;
}
}
}
$unmatchedIgnoredErrors = $this->ignoreErrors;
$addErrors = [];
$errors = array_values(array_filter($errors, function (Error $error) use (&$unmatchedIgnoredErrors, &$addErrors): bool {
foreach ($this->ignoreErrors as $i => $ignore) {
if (\Nette\Utils\Strings::match($error->getMessage(), $ignore) !== null) {
unset($unmatchedIgnoredErrors[$i]);
if (!$error->canBeIgnored()) {
$addErrors[] = sprintf(
'Error message "%s" cannot be ignored, use excludes_analyse instead.',
$error->getMessage()
);
return true;
}
return false;
}
}
return true;
}));
$errors = array_merge($errors, $addErrors);
if (!$onlyFiles && $this->reportUnmatchedIgnoredErrors && !$reachedInternalErrorsCountLimit) {
foreach ($unmatchedIgnoredErrors as $unmatchedIgnoredError) {
$errors[] = sprintf(
'Ignored error pattern %s was not matched in reported errors.',
$unmatchedIgnoredError
);
}
}
if ($reachedInternalErrorsCountLimit) {
$errors[] = sprintf('Reached internal errors count limit of %d, exiting...', $this->internalErrorsCountLimit);
}
return $errors;
}
/**
* @param \PhpParser\Node $node
* @param string $file
* @param string[] $messages
* @return \PHPStan\Analyser\Error[]
*/
private function createErrors(\PhpParser\Node $node, string $file, array $messages): array
{
$errors = [];
foreach ($messages as $message) {
$errors[] = new Error($message, $file, $node->getLine());
}
return $errors;
}
}
src/Analyser/Error.php 0000666 00000001421 13436751442 0010726 0 ustar 00 message = $message;
$this->file = $file;
$this->line = $line;
$this->canBeIgnored = $canBeIgnored;
}
public function getMessage(): string
{
return $this->message;
}
public function getFile(): string
{
return $this->file;
}
/**
* @return int|NULL
*/
public function getLine()
{
return $this->line;
}
public function canBeIgnored(): bool
{
return $this->canBeIgnored;
}
}
src/Analyser/StatementList.php 0000666 00000001354 13436751442 0012442 0 ustar 00 scope = $scope;
$this->statements = $statements;
$this->filterByTruthyValue = $filterByTruthyValue;
}
public function getScope(): Scope
{
return $this->scope;
}
/**
* @return \PhpParser\Node[]
*/
public function getStatements(): array
{
return $this->statements;
}
public function shouldFilterByTruthyValue(): bool
{
return $this->filterByTruthyValue;
}
}
src/Analyser/VariableTypeHolder.php 0000666 00000002240 13436751442 0013362 0 ustar 00 no()) {
throw new \PHPStan\ShouldNotHappenException();
}
$this->type = $type;
$this->certainty = $certainty;
}
public static function createYes(Type $type): self
{
return new self($type, TrinaryLogic::createYes());
}
public static function createMaybe(Type $type): self
{
return new self($type, TrinaryLogic::createMaybe());
}
public function and(self $other): self
{
return new self(
TypeCombinator::union($this->getType(), $other->getType()),
$this->getCertainty()->and($other->getCertainty())
);
}
public function addMaybe(): self
{
return new self(
$this->getType(),
$this->getCertainty()->addMaybe()
);
}
public function getType(): Type
{
return $this->type;
}
public function getCertainty(): TrinaryLogic
{
return $this->certainty;
}
}
src/Analyser/LookForAssignsSettings.php 0000666 00000004233 13436751442 0014265 0 ustar 00 respectEarlyTermination = $respectEarlyTermination;
}
public static function default(): self
{
return new self(
self::EARLY_TERMINATION_ALL
);
}
public static function insideLoop(): self
{
return new self(
self::EARLY_TERMINATION_STOP + self::EARLY_TERMINATION_BREAK
);
}
public static function afterLoop(): self
{
return new self(
self::EARLY_TERMINATION_STOP
);
}
public static function insideFinally(): self
{
return new self(
0
);
}
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 = null): 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;
}
}
src/Analyser/NameScope.php 0000666 00000002140 13436751442 0011506 0 ustar 00 fullName(string)
*/
private $uses;
/**
* @var string|null
*/
private $className;
public function __construct(string $namespace = null, array $uses = [], string $className = null)
{
$this->namespace = $namespace;
$this->uses = $uses;
$this->className = $className;
}
/**
* @return string|null
*/
public function getClassName()
{
return $this->className;
}
public function resolveStringName(string $name): string
{
if (strpos($name, '\\') === 0) {
return ltrim($name, '\\');
}
$nameParts = explode('\\', $name);
$firstNamePart = $nameParts[0];
if (isset($this->uses[$firstNamePart])) {
if (count($nameParts) === 1) {
return $this->uses[$firstNamePart];
}
array_shift($nameParts);
return sprintf('%s\\%s', $this->uses[$firstNamePart], implode('\\', $nameParts));
}
if ($this->namespace !== null) {
return sprintf('%s\\%s', $this->namespace, $name);
}
return $name;
}
}
src/Analyser/TypeSpecifier.php 0000666 00000023667 13436751442 0012430 0 ustar 00 printer = $printer;
}
public function specifyTypesInCondition(Scope $scope, Expr $expr, int $context = self::CONTEXT_TRUTHY): SpecifiedTypes
{
if ($expr instanceof Instanceof_) {
if ($expr->class instanceof Name) {
$className = (string) $expr->class;
if ($className === 'self' && $scope->isInClass()) {
$type = new ObjectType($scope->getClassReflection()->getName());
} elseif ($className === 'static' && $scope->isInClass()) {
$type = new StaticType($scope->getClassReflection()->getName());
} else {
$type = new ObjectType($className);
}
return $this->create($expr->expr, $type, $context);
} elseif ($context & self::CONTEXT_TRUE) {
return $this->create($expr->expr, new ObjectWithoutClassType(), $context);
}
} elseif ($expr instanceof Node\Expr\BinaryOp\Identical) {
$expressions = $this->findTypeExpressionsFromBinaryOperation($expr);
if ($expressions !== null) {
$constantName = strtolower((string) $expressions[1]->name);
if ($constantName === 'false') {
$types = $this->create($expressions[0], new FalseBooleanType(), $context);
return $types->unionWith($this->specifyTypesInCondition(
$scope,
$expressions[0],
($context & self::CONTEXT_TRUE) ? self::CONTEXT_FALSE : ~self::CONTEXT_FALSE
));
} elseif ($constantName === 'true') {
$types = $this->create($expressions[0], new TrueBooleanType(), $context);
return $types->unionWith($this->specifyTypesInCondition(
$scope,
$expressions[0],
($context & self::CONTEXT_TRUE) ? self::CONTEXT_TRUE : ~self::CONTEXT_TRUE
));
} elseif ($constantName === 'null') {
return $this->create($expressions[0], new NullType(), $context);
}
} elseif ($context & self::CONTEXT_TRUE) {
$type = TypeCombinator::intersect($scope->getType($expr->right), $scope->getType($expr->left));
$leftTypes = $this->create($expr->left, $type, $context);
$rightTypes = $this->create($expr->right, $type, $context);
return $leftTypes->unionWith($rightTypes);
}
} elseif ($expr instanceof Node\Expr\BinaryOp\NotIdentical) {
return $this->specifyTypesInCondition(
$scope,
new Node\Expr\BooleanNot(new Node\Expr\BinaryOp\Identical($expr->left, $expr->right)),
$context
);
} elseif ($expr instanceof Node\Expr\BinaryOp\Equal) {
$expressions = $this->findTypeExpressionsFromBinaryOperation($expr);
if ($expressions !== null) {
$constantName = strtolower((string) $expressions[1]->name);
if ($constantName === 'false') {
return $this->specifyTypesInCondition(
$scope,
$expressions[0],
($context & self::CONTEXT_TRUE) ? self::CONTEXT_FALSEY : ~self::CONTEXT_FALSEY
);
} elseif ($constantName === 'true') {
return $this->specifyTypesInCondition(
$scope,
$expressions[0],
($context & self::CONTEXT_TRUE) ? self::CONTEXT_TRUTHY : ~self::CONTEXT_TRUTHY
);
}
}
} elseif ($expr instanceof Node\Expr\BinaryOp\NotEqual) {
return $this->specifyTypesInCondition(
$scope,
new Node\Expr\BooleanNot(new Node\Expr\BinaryOp\Equal($expr->left, $expr->right)),
$context
);
} elseif (
$expr instanceof FuncCall
&& $expr->name instanceof Name
&& isset($expr->args[0])
) {
$functionName = strtolower((string) $expr->name);
$innerExpr = $expr->args[0]->value;
switch ($functionName) {
case 'is_int':
case 'is_integer':
case 'is_long':
return $this->create($innerExpr, new IntegerType(), $context);
case 'is_float':
case 'is_double':
case 'is_real':
return $this->create($innerExpr, new FloatType(), $context);
case 'is_null':
return $this->create($innerExpr, new NullType(), $context);
case 'is_array':
return $this->create($innerExpr, new ArrayType(new MixedType(), new MixedType(), false, TrinaryLogic::createMaybe()), $context);
case 'is_bool':
return $this->create($innerExpr, new TrueOrFalseBooleanType(), $context);
case 'is_callable':
return $this->create($innerExpr, new CallableType(), $context);
case 'is_resource':
return $this->create($innerExpr, new ResourceType(), $context);
case 'is_iterable':
return $this->create($innerExpr, new IterableIterableType(new MixedType(), new MixedType()), $context);
case 'is_string':
return $this->create($innerExpr, new StringType(), $context);
case 'is_object':
return $this->create($innerExpr, new ObjectWithoutClassType(), $context);
case 'is_numeric':
return $this->create($innerExpr, new UnionType([
new StringType(),
new IntegerType(),
new FloatType(),
]), $context);
case 'is_a':
if (isset($expr->args[1])) {
$classNameArgExpr = $expr->args[1]->value;
if ($classNameArgExpr instanceof Node\Scalar\String_) {
$objectType = new ObjectType($classNameArgExpr->value);
$types = $this->create($innerExpr, $objectType, $context);
} elseif (
$classNameArgExpr instanceof Expr\ClassConstFetch
&& $classNameArgExpr->class instanceof Name
&& is_string($classNameArgExpr->name)
&& strtolower($classNameArgExpr->name) === 'class'
) {
$objectType = new ObjectType($scope->resolveName($classNameArgExpr->class));
$types = $this->create($innerExpr, $objectType, $context);
} elseif ($context & self::CONTEXT_TRUE) {
$objectType = new ObjectWithoutClassType();
$types = $this->create($innerExpr, $objectType, $context);
} else {
$types = new SpecifiedTypes();
}
if (isset($expr->args[2]) && ($context & self::CONTEXT_TRUE)) {
if (!$scope->getType($expr->args[2]->value)->isSuperTypeOf(new TrueBooleanType())->no()) {
$types = $types->intersectWith($this->create($innerExpr, new StringType(), $context));
}
}
return $types;
}
}
} elseif ($expr instanceof BooleanAnd) {
$leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context);
$rightTypes = $this->specifyTypesInCondition($scope, $expr->right, $context);
return ($context & self::CONTEXT_TRUE) ? $leftTypes->unionWith($rightTypes) : $leftTypes->intersectWith($rightTypes);
} elseif ($expr instanceof BooleanOr) {
$leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context);
$rightTypes = $this->specifyTypesInCondition($scope, $expr->right, $context);
return ($context & self::CONTEXT_TRUE) ? $leftTypes->intersectWith($rightTypes) : $leftTypes->unionWith($rightTypes);
} elseif ($expr instanceof Node\Expr\BooleanNot) {
return $this->specifyTypesInCondition($scope, $expr->expr, ~$context);
} elseif ($expr instanceof Node\Expr\Assign) {
return $this->specifyTypesInCondition($scope, $expr->var, $context);
} elseif (
$expr instanceof Expr\Isset_
&& count($expr->vars) > 0
&& $context & self::CONTEXT_TRUTHY
) {
$types = null;
foreach ($expr->vars as $var) {
$type = $this->create($var, new NullType(), self::CONTEXT_FALSE);
if ($types === null) {
$types = $type;
} else {
$types = $types->unionWith($type);
}
}
return $types;
} elseif (($context & self::CONTEXT_TRUTHY) === 0) {
$type = new ObjectWithoutClassType();
return $this->create($expr, $type, self::CONTEXT_FALSE);
} elseif (($context & self::CONTEXT_FALSEY) === 0) {
$type = new UnionType([new NullType(), new FalseBooleanType()]);
return $this->create($expr, $type, self::CONTEXT_FALSE);
}
return new SpecifiedTypes();
}
/**
* @param \PhpParser\Node\Expr\BinaryOp $binaryOperation
* @return array|null
*/
private function findTypeExpressionsFromBinaryOperation(Node\Expr\BinaryOp $binaryOperation)
{
if ($binaryOperation->left instanceof ConstFetch) {
return [$binaryOperation->right, $binaryOperation->left];
} elseif ($binaryOperation->right instanceof ConstFetch) {
return [$binaryOperation->left, $binaryOperation->right];
}
return null;
}
private function create(Expr $expr, Type $type, int $context): SpecifiedTypes
{
$sureTypes = [];
$sureNotTypes = [];
if ($expr instanceof Node\Expr\Variable
|| $expr instanceof Node\Expr\FuncCall
|| $expr instanceof Node\Expr\MethodCall
|| $expr instanceof Node\Expr\StaticCall
|| $expr instanceof Node\Expr\PropertyFetch
|| $expr instanceof Node\Expr\StaticPropertyFetch
|| $expr instanceof Node\Expr\ArrayDimFetch
) {
$exprString = $this->printer->prettyPrintExpr($expr);
if ($context & self::CONTEXT_FALSE) {
$sureNotTypes[$exprString] = [$expr, $type];
} elseif ($context & self::CONTEXT_TRUE) {
$sureTypes[$exprString] = [$expr, $type];
}
}
return new SpecifiedTypes($sureTypes, $sureNotTypes);
}
}
src/Analyser/Scope.php 0000666 00000143330 13436751442 0010714 0 ustar 00 broker = $broker;
$this->printer = $printer;
$this->typeSpecifier = $typeSpecifier;
$this->file = $file;
$this->analysedContextFile = $analysedContextFile !== null ? $analysedContextFile : $file;
$this->declareStrictTypes = $declareStrictTypes;
$this->classReflection = $classReflection;
$this->function = $function;
$this->namespace = $namespace;
$this->variableTypes = $variablesTypes;
$this->inClosureBindScopeClass = $inClosureBindScopeClass;
$this->inAnonymousFunctionReturnType = $inAnonymousFunctionReturnType;
$this->inFunctionCall = $inFunctionCall;
$this->negated = $negated;
$this->moreSpecificTypes = $moreSpecificTypes;
$this->inFirstLevelStatement = $inFirstLevelStatement;
$this->currentlyAssignedExpressions = $currentlyAssignedExpressions;
}
public function getFile(): string
{
return $this->file;
}
public function getAnalysedContextFile(): string
{
return $this->analysedContextFile;
}
public function isDeclareStrictTypes(): bool
{
return $this->declareStrictTypes;
}
public function enterDeclareStrictTypes(): self
{
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
true
);
}
public function isInClass(): bool
{
return $this->classReflection !== null;
}
public function getClassReflection(): ClassReflection
{
/** @var \PHPStan\Reflection\ClassReflection $classReflection */
$classReflection = $this->classReflection;
return $classReflection;
}
/**
* @return null|\PHPStan\Reflection\ParametersAcceptor
*/
public function getFunction()
{
return $this->function;
}
/**
* @return null|string
*/
public function getFunctionName()
{
return $this->function !== null ? $this->function->getName() : null;
}
/**
* @return null|string
*/
public function getNamespace()
{
return $this->namespace;
}
/**
* @return \PHPStan\Analyser\VariableTypeHolder[]
*/
private function getVariableTypes(): array
{
return $this->variableTypes;
}
public function hasVariableType(string $variableName): TrinaryLogic
{
if (!isset($this->variableTypes[$variableName])) {
return TrinaryLogic::createNo();
}
return $this->variableTypes[$variableName]->getCertainty();
}
public function getVariableType(string $variableName): Type
{
if ($this->hasVariableType($variableName)->no()) {
throw new \PHPStan\Analyser\UndefinedVariableException($this, $variableName);
}
return $this->variableTypes[$variableName]->getType();
}
public function isInAnonymousFunction(): bool
{
return $this->inAnonymousFunctionReturnType !== null;
}
/**
* @return \PHPStan\Type\Type|null
*/
public function getAnonymousFunctionReturnType()
{
return $this->inAnonymousFunctionReturnType;
}
/**
* @return \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|null
*/
public function getInFunctionCall()
{
return $this->inFunctionCall;
}
public function getType(Expr $node): Type
{
$key = $this->printer->prettyPrintExpr($node);
if (!array_key_exists($key, $this->resolvedTypes)) {
$this->resolvedTypes[$key] = $this->resolveType($node);
}
return $this->resolvedTypes[$key];
}
private function resolveType(Expr $node): Type
{
if (
$node instanceof \PhpParser\Node\Expr\BinaryOp\BooleanAnd
|| $node instanceof \PhpParser\Node\Expr\BinaryOp\BooleanOr
|| $node instanceof \PhpParser\Node\Expr\BooleanNot
|| $node instanceof \PhpParser\Node\Expr\BinaryOp\LogicalXor
|| $node instanceof Expr\BinaryOp\Greater
|| $node instanceof Expr\BinaryOp\GreaterOrEqual
|| $node instanceof Expr\BinaryOp\Smaller
|| $node instanceof Expr\BinaryOp\SmallerOrEqual
|| $node instanceof Expr\BinaryOp\Identical
|| $node instanceof Expr\BinaryOp\NotIdentical
|| $node instanceof Expr\BinaryOp\Equal
|| $node instanceof Expr\BinaryOp\NotEqual
|| $node instanceof Expr\Instanceof_
|| $node instanceof Expr\Isset_
|| $node instanceof Expr\Empty_
) {
return new TrueOrFalseBooleanType();
}
if (
$node instanceof Node\Expr\UnaryMinus
|| $node instanceof Node\Expr\UnaryPlus
|| $node instanceof Expr\ErrorSuppress
|| $node instanceof Expr\Assign
) {
return $this->getType($node->expr);
}
if ($node instanceof Node\Expr\BinaryOp\Mod) {
return new IntegerType();
}
if ($node instanceof Expr\BinaryOp\Concat) {
return new StringType();
}
if ($node instanceof Expr\BinaryOp\Spaceship) {
return new IntegerType();
}
if ($node instanceof Expr\Ternary) {
$elseType = $this->getType($node->else);
if ($node->if === null) {
return TypeCombinator::union(
TypeCombinator::removeNull($this->getType($node->cond)),
$elseType
);
}
$conditionScope = $this->filterByTruthyValue($node->cond);
$ifType = $conditionScope->getType($node->if);
$negatedConditionScope = $this->filterByFalseyValue($node->cond);
$elseType = $negatedConditionScope->getType($node->else);
return TypeCombinator::union($ifType, $elseType);
}
if ($node instanceof Expr\BinaryOp\Coalesce) {
return TypeCombinator::union(
TypeCombinator::removeNull($this->getType($node->left)),
$this->getType($node->right)
);
}
if ($node instanceof Expr\Clone_) {
return $this->getType($node->expr);
}
if ($node instanceof Expr\AssignOp\Concat) {
return new StringType();
}
if (
$node instanceof Expr\AssignOp\ShiftLeft
|| $node instanceof Expr\AssignOp\ShiftRight
|| $node instanceof Expr\AssignOp\Mod
) {
return new IntegerType();
}
if (
$node instanceof Node\Expr\BinaryOp\Plus
|| $node instanceof Node\Expr\BinaryOp\Minus
|| $node instanceof Node\Expr\BinaryOp\Mul
|| $node instanceof Node\Expr\BinaryOp\Pow
|| $node instanceof Node\Expr\BinaryOp\Div
|| $node instanceof Node\Expr\AssignOp
) {
if ($node instanceof Node\Expr\AssignOp) {
$left = $node->var;
$right = $node->expr;
} elseif ($node instanceof Node\Expr\BinaryOp) {
$left = $node->left;
$right = $node->right;
} else {
throw new \PHPStan\ShouldNotHappenException();
}
$leftType = $this->getType($left);
$rightType = $this->getType($right);
if ($leftType instanceof BooleanType) {
$leftType = new IntegerType();
}
if ($rightType instanceof BooleanType) {
$rightType = new IntegerType();
}
if ($node instanceof Expr\AssignOp\Div || $node instanceof Expr\BinaryOp\Div) {
if (!$leftType instanceof MixedType && !$rightType instanceof MixedType) {
return new FloatType();
}
}
if (
($leftType instanceof FloatType && !$rightType instanceof MixedType)
|| ($rightType instanceof FloatType && !$leftType instanceof MixedType)
) {
return new FloatType();
}
if ($leftType instanceof IntegerType && $rightType instanceof IntegerType) {
return new IntegerType();
}
if (
($node instanceof Expr\AssignOp\Plus || $node instanceof Expr\BinaryOp\Plus)
&& $leftType instanceof ArrayType
&& $rightType instanceof ArrayType
) {
return new ArrayType(
TypeCombinator::union($leftType->getIterableKeyType(), $rightType->getIterableKeyType()),
TypeCombinator::union($leftType->getItemType(), $rightType->getItemType()),
$leftType->isItemTypeInferredFromLiteralArray() || $rightType->isItemTypeInferredFromLiteralArray()
);
}
}
if ($node instanceof LNumber) {
return new IntegerType();
} elseif ($node instanceof ConstFetch) {
$constName = strtolower((string) $node->name);
if ($constName === 'true') {
return new \PHPStan\Type\TrueBooleanType();
} elseif ($constName === 'false') {
return new \PHPStan\Type\FalseBooleanType();
} elseif ($constName === 'null') {
return new NullType();
}
if ($this->broker->hasConstant($node->name, $this)) {
$typeFromValue = $this->getTypeFromValue(
constant($this->broker->resolveConstantName($node->name, $this))
);
if ($typeFromValue !== null) {
return $typeFromValue;
}
}
} elseif ($node instanceof String_ || $node instanceof Node\Scalar\Encapsed) {
return new StringType();
} elseif ($node instanceof DNumber) {
return new FloatType();
} elseif ($node instanceof Expr\Closure) {
return new ObjectType('Closure');
} elseif ($node instanceof New_) {
if ($node->class instanceof Name) {
if (
count($node->class->parts) === 1
) {
if ($node->class->parts[0] === 'static') {
return new StaticType($this->getClassReflection()->getName());
} elseif ($node->class->parts[0] === 'self') {
return new ObjectType($this->getClassReflection()->getName());
}
}
return new ObjectType((string) $node->class);
}
} elseif ($node instanceof Array_) {
$itemTypes = array_map(
function (Expr\ArrayItem $item): Type {
return $this->getType($item->value);
},
$node->items
);
$callable = TrinaryLogic::createNo();
if (count($itemTypes) === 2) {
if (
($itemTypes[0]->accepts(new StringType()) || count($itemTypes[0]->getReferencedClasses()) > 0)
&& $itemTypes[1]->accepts(new StringType())
) {
$callable = TrinaryLogic::createYes();
}
}
$arrayWithKeys = [];
$keyExpressionTypes = [];
foreach ($node->items as $arrayItem) {
$itemKey = $arrayItem->key;
if ($itemKey === null) {
$arrayWithKeys[] = 'foo';
continue;
}
if (
$itemKey instanceof \PhpParser\Node\Scalar\String_
|| $itemKey instanceof \PhpParser\Node\Scalar\LNumber
|| $itemKey instanceof \PhpParser\Node\Scalar\DNumber
|| $itemKey instanceof \PhpParser\Node\Expr\ConstFetch
) {
if ($itemKey instanceof \PhpParser\Node\Expr\ConstFetch) {
$constName = strtolower((string) $itemKey->name);
if ($constName === 'true') {
$value = true;
} elseif ($constName === 'false') {
$value = false;
} elseif ($constName === 'null') {
$value = null;
} elseif ($this->broker->hasConstant($itemKey->name, $this)) {
$value = constant($this->broker->resolveConstantName($itemKey->name, $this));
} else {
$keyExpressionTypes[] = new MixedType();
continue;
}
$arrayWithKeys[$value] = 'foo';
} else {
$arrayWithKeys[$itemKey->value] = 'foo';
}
} else {
$keyExpressionTypes[] = $this->getType($itemKey);
}
}
$scalarKeysTypes = array_map(function ($value): Type {
return $this->getTypeFromValue($value);
}, array_keys($arrayWithKeys));
return new ArrayType(
$this->getCombinedType(array_merge($scalarKeysTypes, $keyExpressionTypes)),
$this->getCombinedType($itemTypes),
true,
$callable
);
} elseif ($node instanceof Int_) {
return new IntegerType();
} elseif ($node instanceof Bool_) {
return new TrueOrFalseBooleanType();
} elseif ($node instanceof Double) {
return new FloatType();
} elseif ($node instanceof \PhpParser\Node\Expr\Cast\String_) {
return new StringType();
} elseif ($node instanceof \PhpParser\Node\Expr\Cast\Array_) {
return new ArrayType(new MixedType(), new MixedType());
} elseif ($node instanceof Node\Scalar\MagicConst\Line) {
return new IntegerType();
} elseif ($node instanceof Node\Scalar\MagicConst) {
return new StringType();
} elseif ($node instanceof Object_) {
return new ObjectType('stdClass');
} elseif ($node instanceof Unset_) {
return new NullType();
} elseif ($node instanceof Node\Expr\ClassConstFetch && is_string($node->name)) {
if ($node->class instanceof Name) {
$constantClass = (string) $node->class;
$constantClassType = new ObjectType($constantClass);
if (in_array(strtolower($constantClass), [
'self',
'parent',
], true)) {
$resolvedName = $this->resolveName($node->class);
$constantClassType = new ObjectType($resolvedName);
}
} else {
$constantClassType = $this->getType($node->class);
}
$constantName = $node->name;
if (strtolower($constantName) === 'class') {
return new StringType();
}
if ($constantClassType->hasConstant($constantName)) {
$constant = $constantClassType->getConstant($constantName);
$typeFromValue = $this->getTypeFromValue($constant->getValue());
if ($typeFromValue !== null) {
return $typeFromValue;
}
}
}
$exprString = $this->printer->prettyPrintExpr($node);
if (isset($this->moreSpecificTypes[$exprString])) {
return $this->moreSpecificTypes[$exprString];
}
if ($node instanceof Variable && is_string($node->name)) {
if ($this->hasVariableType($node->name)->no()) {
return new ErrorType();
}
return $this->getVariableType($node->name);
}
if ($node instanceof Expr\ArrayDimFetch && $node->dim !== null) {
$arrayType = $this->getType($node->var);
if ($arrayType instanceof ArrayType) {
return $arrayType->getItemType();
}
}
if ($node instanceof MethodCall && is_string($node->name)) {
$methodCalledOnType = $this->getType($node->var);
$referencedClasses = $methodCalledOnType->getReferencedClasses();
if (
count($referencedClasses) === 1
&& $this->broker->hasClass($referencedClasses[0])
) {
$methodClassReflection = $this->broker->getClass($referencedClasses[0]);
if (!$methodClassReflection->hasMethod($node->name)) {
return new ErrorType();
}
$methodReflection = $methodClassReflection->getMethod($node->name, $this);
foreach ($this->broker->getDynamicMethodReturnTypeExtensionsForClass($methodClassReflection->getName()) as $dynamicMethodReturnTypeExtension) {
if (!$dynamicMethodReturnTypeExtension->isMethodSupported($methodReflection)) {
continue;
}
return $dynamicMethodReturnTypeExtension->getTypeFromMethodCall($methodReflection, $node, $this);
}
}
if (!$methodCalledOnType->hasMethod($node->name)) {
return new ErrorType();
}
$methodReflection = $methodCalledOnType->getMethod($node->name, $this);
$calledOnThis = $node->var instanceof Variable && is_string($node->var->name) && $node->var->name === 'this';
$methodReturnType = $methodReflection->getReturnType();
if ($methodReturnType instanceof StaticResolvableType) {
if ($calledOnThis) {
if ($this->isInClass()) {
return $methodReturnType->changeBaseClass($this->getClassReflection()->getName());
}
} elseif (count($referencedClasses) === 1) {
return $methodReturnType->resolveStatic($referencedClasses[0]);
}
}
return $methodReflection->getReturnType();
}
if ($node instanceof Expr\StaticCall && is_string($node->name)) {
if ($node->class instanceof Name) {
$calleeType = new ObjectType($this->resolveName($node->class));
} else {
$calleeType = $this->getType($node->class);
}
if (!$calleeType->hasMethod($node->name)) {
return new ErrorType();
}
$staticMethodReflection = $calleeType->getMethod($node->name, $this);
$referencedClasses = $calleeType->getReferencedClasses();
if (
count($calleeType->getReferencedClasses()) === 1
&& $this->broker->hasClass($referencedClasses[0])
) {
$staticMethodClassReflection = $this->broker->getClass($referencedClasses[0]);
foreach ($this->broker->getDynamicStaticMethodReturnTypeExtensionsForClass($staticMethodClassReflection->getName()) as $dynamicStaticMethodReturnTypeExtension) {
if (!$dynamicStaticMethodReturnTypeExtension->isStaticMethodSupported($staticMethodReflection)) {
continue;
}
return $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall($staticMethodReflection, $node, $this);
}
}
if ($staticMethodReflection->getReturnType() instanceof StaticResolvableType) {
if ($node->class instanceof Name) {
$nodeClassString = strtolower((string) $node->class);
if (in_array($nodeClassString, [
'self',
'static',
'parent',
], true) && $this->isInClass()) {
return $staticMethodReflection->getReturnType()->changeBaseClass($this->getClassReflection()->getName());
}
}
if (count($referencedClasses) === 1) {
return $staticMethodReflection->getReturnType()->resolveStatic($referencedClasses[0]);
}
}
return $staticMethodReflection->getReturnType();
}
if ($node instanceof PropertyFetch && is_string($node->name)) {
$propertyFetchedOnType = $this->getType($node->var);
if (!$propertyFetchedOnType->hasProperty($node->name)) {
return new ErrorType();
}
return $propertyFetchedOnType->getProperty($node->name, $this)->getType();
}
if ($node instanceof Expr\StaticPropertyFetch && is_string($node->name) && $node->class instanceof Name) {
$staticPropertyHolderClass = $this->resolveName($node->class);
if ($this->broker->hasClass($staticPropertyHolderClass)) {
$staticPropertyClassReflection = $this->broker->getClass(
$staticPropertyHolderClass
);
if (!$staticPropertyClassReflection->hasProperty($node->name)) {
return new ErrorType();
}
return $staticPropertyClassReflection->getProperty($node->name, $this)->getType();
}
}
if ($node instanceof FuncCall && $node->name instanceof Name) {
if (!$this->broker->hasFunction($node->name, $this)) {
return new ErrorType();
}
$functionReflection = $this->broker->getFunction($node->name, $this);
foreach ($this->broker->getDynamicFunctionReturnTypeExtensions() as $dynamicFunctionReturnTypeExtension) {
if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) {
continue;
}
return $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall($functionReflection, $node, $this);
}
return $functionReflection->getReturnType();
}
return new MixedType();
}
public function resolveName(Name $name): string
{
$originalClass = (string) $name;
if ($this->isInClass()) {
if (in_array(strtolower($originalClass), [
'self',
'static',
], true)) {
return $this->getClassReflection()->getName();
} elseif ($originalClass === 'parent') {
$currentClassReflection = $this->getClassReflection();
if ($currentClassReflection->getParentClass() !== false) {
return $currentClassReflection->getParentClass()->getName();
}
}
}
return $originalClass;
}
/**
* @param mixed $value
* @return Type|null
*/
private function getTypeFromValue($value)
{
if (is_int($value)) {
return new IntegerType();
} elseif (is_float($value)) {
return new FloatType();
} elseif (is_bool($value)) {
return new TrueOrFalseBooleanType();
} elseif ($value === null) {
return new NullType();
} elseif (is_string($value)) {
return new StringType();
} elseif (is_array($value)) {
return new ArrayType(
$this->getCombinedType(
array_map(function ($value): Type {
return $this->getTypeFromValue($value);
}, array_keys($value))
),
$this->getCombinedType(
array_map(function ($value): Type {
return $this->getTypeFromValue($value);
}, array_values($value))
),
false
);
}
return null;
}
/**
* @param \PHPStan\Type\Type[] $types
* @return \PHPStan\Type\Type
*/
private function getCombinedType(array $types): Type
{
if (count($types) === 0) {
return new MixedType();
}
return TypeCombinator::union(...$types);
}
public function isSpecified(Expr $node): bool
{
$exprString = $this->printer->prettyPrintExpr($node);
return isset($this->moreSpecificTypes[$exprString]);
}
public function enterClass(ClassReflection $classReflection): self
{
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$classReflection,
null,
$this->getNamespace(),
[
'this' => VariableTypeHolder::createYes(new ThisType($classReflection->getName())),
]
);
}
public function changeAnalysedContextFile(string $fileName): self
{
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$fileName,
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes
);
}
public function enterClassMethod(
Node\Stmt\ClassMethod $classMethod,
array $phpDocParameterTypes,
Type $phpDocReturnType = null
): self
{
return $this->enterFunctionLike(
new PhpMethodFromParserNodeReflection(
$this->getClassReflection(),
$classMethod,
$this->getRealParameterTypes($classMethod),
$phpDocParameterTypes,
$classMethod->returnType !== null,
$this->getFunctionType($classMethod->returnType, $classMethod->returnType === null, false),
$phpDocReturnType
)
);
}
private function getRealParameterTypes(Node\FunctionLike $functionLike): array
{
$realParameterTypes = [];
foreach ($functionLike->getParams() as $parameter) {
$realParameterTypes[$parameter->name] = $this->getFunctionType(
$parameter->type,
$this->isParameterValueNullable($parameter),
$parameter->variadic
);
}
return $realParameterTypes;
}
public function enterFunction(
Node\Stmt\Function_ $function,
array $phpDocParameterTypes,
Type $phpDocReturnType = null
): self
{
return $this->enterFunctionLike(
new PhpFunctionFromParserNodeReflection(
$function,
$this->getRealParameterTypes($function),
$phpDocParameterTypes,
$function->returnType !== null,
$this->getFunctionType($function->returnType, $function->returnType === null, false),
$phpDocReturnType
)
);
}
private function enterFunctionLike(ParametersAcceptor $functionReflection): self
{
$variableTypes = $this->getVariableTypes();
foreach ($functionReflection->getParameters() as $parameter) {
$variableTypes[$parameter->getName()] = VariableTypeHolder::createYes($parameter->getType());
}
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$functionReflection,
$this->getNamespace(),
$variableTypes,
null,
null
);
}
public function enterNamespace(string $namespaceName): self
{
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
null,
null,
$namespaceName
);
}
public function enterClosureBind(Type $thisType = null, string $scopeClass): self
{
$variableTypes = $this->getVariableTypes();
if ($thisType !== null) {
$variableTypes['this'] = VariableTypeHolder::createYes($thisType);
} else {
unset($variableTypes['this']);
}
if ($scopeClass === 'static' && $this->isInClass()) {
$scopeClass = $this->getClassReflection()->getName();
}
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$variableTypes,
$scopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes
);
}
public function isInClosureBind(): bool
{
return $this->inClosureBindScopeClass !== null;
}
public function enterAnonymousClass(ClassReflection $anonymousClass): self
{
return $this->enterClass($anonymousClass);
}
/**
* @param \PhpParser\Node\Param[] $parameters
* @param \PhpParser\Node\Expr\ClosureUse[] $uses
* @param \PhpParser\Node\Name|string|\PhpParser\Node\NullableType|null $returnTypehint
* @return self
*/
public function enterAnonymousFunction(
array $parameters,
array $uses,
$returnTypehint = null
): self
{
$variableTypes = [];
foreach ($parameters as $parameter) {
$isNullable = $this->isParameterValueNullable($parameter);
$variableTypes[$parameter->name] = VariableTypeHolder::createYes(
$this->getFunctionType($parameter->type, $isNullable, $parameter->variadic)
);
}
foreach ($uses as $use) {
if (!$this->hasVariableType($use->var)->yes()) {
if ($use->byRef) {
$variableTypes[$use->var] = VariableTypeHolder::createYes(new MixedType());
}
continue;
}
$variableTypes[$use->var] = VariableTypeHolder::createYes($this->getVariableType($use->var));
}
if ($this->hasVariableType('this')->yes()) {
$variableTypes['this'] = VariableTypeHolder::createYes($this->getVariableType('this'));
}
$returnType = $this->getFunctionType($returnTypehint, $returnTypehint === null, false);
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$variableTypes,
$this->inClosureBindScopeClass,
$returnType,
$this->getInFunctionCall()
);
}
public function isParameterValueNullable(Node\Param $parameter): bool
{
if ($parameter->default instanceof ConstFetch) {
return strtolower((string) $parameter->default->name) === 'null';
}
return false;
}
/**
* @param \PhpParser\Node\Name|string|\PhpParser\Node\NullableType|null $type
* @param bool $isNullable
* @param bool $isVariadic
* @return Type
*/
public function getFunctionType($type = null, bool $isNullable, bool $isVariadic): Type
{
if ($isNullable) {
return TypeCombinator::addNull(
$this->getFunctionType($type, false, $isVariadic)
);
}
if ($isVariadic) {
return new ArrayType(new IntegerType(), $this->getFunctionType(
$type,
false,
false
), false);
}
if ($type === null) {
return new MixedType();
} elseif ($type === 'string') {
return new StringType();
} elseif ($type === 'int') {
return new IntegerType();
} elseif ($type === 'bool') {
return new TrueOrFalseBooleanType();
} elseif ($type === 'float') {
return new FloatType();
} elseif ($type === 'callable') {
return new CallableType();
} elseif ($type === 'array') {
return new ArrayType(new MixedType(), new MixedType());
} elseif ($type instanceof Name) {
$className = (string) $type;
if ($className === 'self') {
$className = $this->getClassReflection()->getName();
} elseif (
$className === 'parent'
) {
if ($this->isInClass() && $this->getClassReflection()->getParentClass() !== false) {
return new ObjectType($this->getClassReflection()->getParentClass()->getName());
}
return new NonexistentParentClassType();
}
return new ObjectType($className);
} elseif ($type === 'iterable') {
return new IterableIterableType(new MixedType(), new MixedType());
} elseif ($type === 'void') {
return new VoidType();
} elseif ($type === 'object') {
return new ObjectWithoutClassType();
} elseif ($type instanceof Node\NullableType) {
return $this->getFunctionType($type->type, true, $isVariadic);
}
return new MixedType();
}
public function enterForeach(Expr $iteratee, string $valueName, string $keyName = null): self
{
$iterateeType = $this->getType($iteratee);
$scope = $this->assignVariable($valueName, $iterateeType->getIterableValueType(), TrinaryLogic::createYes());
if ($keyName !== null) {
$scope = $scope->assignVariable($keyName, $iterateeType->getIterableKeyType(), TrinaryLogic::createYes());
}
return $scope;
}
/**
* @param \PhpParser\Node\Name[] $classes
* @param string $variableName
* @return Scope
*/
public function enterCatch(array $classes, string $variableName): self
{
$type = TypeCombinator::union(...array_map(function (string $class): ObjectType {
return new ObjectType($class);
}, $classes));
return $this->assignVariable(
$variableName,
TypeCombinator::intersect($type, new ObjectType(\Throwable::class)),
TrinaryLogic::createYes()
);
}
/**
* @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall $functionCall
* @return self
*/
public function enterFunctionCall($functionCall): self
{
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$functionCall,
$this->isNegated(),
$this->moreSpecificTypes,
$this->inFirstLevelStatement
);
}
public function enterExpressionAssign(Expr $expr): self
{
$currentlyAssignedExpressions = $this->currentlyAssignedExpressions;
$currentlyAssignedExpressions[] = $this->printer->prettyPrintExpr($expr);
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes,
$this->isInFirstLevelStatement(),
$currentlyAssignedExpressions
);
}
public function isInExpressionAssign(Expr $expr): bool
{
$exprString = $this->printer->prettyPrintExpr($expr);
return in_array($exprString, $this->currentlyAssignedExpressions, true);
}
public function assignVariable(
string $variableName,
Type $type,
TrinaryLogic $certainty
): self
{
if ($certainty->no()) {
throw new \PHPStan\ShouldNotHappenException();
}
$existingCertainty = $this->hasVariableType($variableName);
if (!$existingCertainty->no()) {
$certainty = $certainty->or($existingCertainty);
}
$variableTypes = $this->getVariableTypes();
$variableTypes[$variableName] = new VariableTypeHolder($type, $certainty);
$exprString = $this->printer->prettyPrintExpr(new Variable($variableName));
$moreSpecificTypes = $this->moreSpecificTypes;
if (array_key_exists($exprString, $moreSpecificTypes)) {
unset($moreSpecificTypes[$exprString]);
}
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$variableTypes,
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
$this->isNegated(),
$moreSpecificTypes,
$this->inFirstLevelStatement
);
}
public function unsetVariable(string $variableName): self
{
if ($this->hasVariableType($variableName)->no()) {
return $this;
}
$variableTypes = $this->getVariableTypes();
unset($variableTypes[$variableName]);
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$variableTypes,
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes,
$this->inFirstLevelStatement
);
}
public function intersectVariables(Scope $otherScope): self
{
$ourVariableTypeHolders = $this->getVariableTypes();
$theirVariableTypeHolders = $otherScope->getVariableTypes();
$intersectedVariableTypeHolders = [];
foreach ($theirVariableTypeHolders as $name => $variableTypeHolder) {
if (isset($ourVariableTypeHolders[$name])) {
$intersectedVariableTypeHolders[$name] = $ourVariableTypeHolders[$name]->and($variableTypeHolder);
} else {
$intersectedVariableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType());
}
}
foreach ($ourVariableTypeHolders as $name => $variableTypeHolder) {
$variableNode = new Variable($name);
if ($otherScope->isSpecified($variableNode)) {
$intersectedVariableTypeHolders[$name] = VariableTypeHolder::createYes(
TypeCombinator::union(
$otherScope->getType($variableNode),
$variableTypeHolder->getType()
)
);
continue;
}
if (isset($theirVariableTypeHolders[$name])) {
continue;
}
$intersectedVariableTypeHolders[$name] = VariableTypeHolder::createMaybe($variableTypeHolder->getType());
}
$theirSpecifiedTypes = $otherScope->moreSpecificTypes;
$intersectedSpecifiedTypes = [];
foreach ($this->moreSpecificTypes as $exprString => $specificType) {
if (!isset($theirSpecifiedTypes[$exprString])) {
continue;
}
$intersectedSpecifiedTypes[$exprString] = TypeCombinator::union(
$specificType,
$theirSpecifiedTypes[$exprString]
);
}
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$intersectedVariableTypeHolders,
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
$this->isNegated(),
$intersectedSpecifiedTypes,
$this->inFirstLevelStatement
);
}
public function createIntersectedScope(self $otherScope): self
{
$variableTypes = [];
foreach ($otherScope->getVariableTypes() as $name => $variableTypeHolder) {
$variableTypes[$name] = $variableTypeHolder;
}
$specifiedTypes = [];
foreach ($otherScope->moreSpecificTypes as $exprString => $specificType) {
$specifiedTypes[$exprString] = $specificType;
}
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$variableTypes,
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
$this->isNegated(),
$specifiedTypes,
$this->inFirstLevelStatement
);
}
public function mergeWithIntersectedScope(self $intersectedScope): self
{
$variableTypeHolders = $this->variableTypes;
$specifiedTypes = $this->moreSpecificTypes;
foreach ($intersectedScope->getVariableTypes() as $name => $theirVariableTypeHolder) {
if (isset($variableTypeHolders[$name])) {
$type = $theirVariableTypeHolder->getType();
if ($theirVariableTypeHolder->getCertainty()->maybe()) {
$type = TypeCombinator::union($type, $variableTypeHolders[$name]->getType());
}
$theirVariableTypeHolder = new VariableTypeHolder(
$type,
$theirVariableTypeHolder->getCertainty()->or($variableTypeHolders[$name]->getCertainty())
);
}
$variableTypeHolders[$name] = $theirVariableTypeHolder->addMaybe();
$exprString = $this->printer->prettyPrintExpr(new Variable($name));
unset($specifiedTypes[$exprString]);
}
foreach ($intersectedScope->moreSpecificTypes as $exprString => $specificType) {
if (preg_match('#^\$([a-zA-Z_][a-zA-Z0-9_]*)$#', $exprString, $matches) === 1) {
$variableName = $matches[1];
$variableTypeHolders[$variableName] = VariableTypeHolder::createYes($specificType);
continue;
}
$specifiedTypes[$exprString] = $specificType;
}
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$variableTypeHolders,
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
$this->isNegated(),
$specifiedTypes,
$this->inFirstLevelStatement
);
}
public function removeVariables(self $otherScope, bool $all): self
{
$ourVariableTypeHolders = $this->getVariableTypes();
foreach ($otherScope->getVariableTypes() as $name => $theirVariableTypeHolder) {
if ($all) {
if (
isset($ourVariableTypeHolders[$name])
&& $ourVariableTypeHolders[$name]->getCertainty()->equals($theirVariableTypeHolder->getCertainty())
) {
unset($ourVariableTypeHolders[$name]);
}
} else {
if (
isset($ourVariableTypeHolders[$name])
&& $theirVariableTypeHolder->getType()->describe() === $ourVariableTypeHolders[$name]->getType()->describe()
&& $ourVariableTypeHolders[$name]->getCertainty()->equals($theirVariableTypeHolder->getCertainty())
) {
unset($ourVariableTypeHolders[$name]);
}
}
}
$moreSpecificTypes = $this->moreSpecificTypes;
foreach ($otherScope->moreSpecificTypes as $exprString => $specifiedType) {
if (isset($moreSpecificTypes[$exprString]) && $specifiedType->describe() === $moreSpecificTypes[$exprString]->describe()) {
unset($moreSpecificTypes[$exprString]);
}
}
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$ourVariableTypeHolders,
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
$this->isNegated(),
$moreSpecificTypes,
$this->inFirstLevelStatement
);
}
public function specifyExpressionType(Expr $expr, Type $type): self
{
$exprString = $this->printer->prettyPrintExpr($expr);
$scope = $this->addMoreSpecificTypes([
$exprString => $type,
]);
if ($expr instanceof Variable && is_string($expr->name)) {
$variableName = $expr->name;
$variableTypes = $scope->getVariableTypes();
$variableTypes[$variableName] = VariableTypeHolder::createYes($type);
return new self(
$scope->broker,
$scope->printer,
$scope->typeSpecifier,
$scope->getFile(),
$scope->getAnalysedContextFile(),
$scope->isDeclareStrictTypes(),
$scope->isInClass() ? $scope->getClassReflection() : null,
$scope->getFunction(),
$scope->getNamespace(),
$variableTypes,
$scope->inClosureBindScopeClass,
$scope->getAnonymousFunctionReturnType(),
$scope->getInFunctionCall(),
$scope->isNegated(),
$scope->moreSpecificTypes,
$scope->inFirstLevelStatement
);
}
return $scope;
}
public function unspecifyExpressionType(Expr $expr): self
{
$exprString = $this->printer->prettyPrintExpr($expr);
$moreSpecificTypes = $this->moreSpecificTypes;
if (isset($moreSpecificTypes[$exprString]) && !$moreSpecificTypes[$exprString] instanceof MixedType) {
unset($moreSpecificTypes[$exprString]);
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
$this->isNegated(),
$moreSpecificTypes,
$this->inFirstLevelStatement
);
}
return $this;
}
public function removeTypeFromExpression(Expr $expr, Type $type): self
{
return $this->specifyExpressionType(
$expr,
TypeCombinator::remove($this->getType($expr), $type)
);
}
public function filterByTruthyValue(Expr $expr): self
{
$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifier::CONTEXT_TRUTHY);
return $this->filterBySpecifiedTypes($specifiedTypes);
}
public function filterByFalseyValue(Expr $expr): self
{
$specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifier::CONTEXT_FALSEY);
return $this->filterBySpecifiedTypes($specifiedTypes);
}
private function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
{
$scope = $this;
foreach ($specifiedTypes->getSureTypes() as list($expr, $type)) {
$type = TypeCombinator::intersect($type, $this->getType($expr));
$scope = $scope->specifyExpressionType($expr, $type);
}
foreach ($specifiedTypes->getSureNotTypes() as list($expr, $type)) {
$scope = $scope->removeTypeFromExpression($expr, $type);
}
return $scope;
}
public function specifyFetchedPropertyFromIsset(PropertyFetch $expr): self
{
$exprString = $this->printer->prettyPrintExpr($expr);
return $this->addMoreSpecificTypes([
$exprString => new MixedType(),
]);
}
public function specifyFetchedStaticPropertyFromIsset(Expr\StaticPropertyFetch $expr): self
{
$exprString = $this->printer->prettyPrintExpr($expr);
return $this->addMoreSpecificTypes([
$exprString => new MixedType(),
]);
}
public function enterNegation(): self
{
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
!$this->isNegated(),
$this->moreSpecificTypes,
$this->inFirstLevelStatement
);
}
public function enterFirstLevelStatements(): self
{
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes,
true,
$this->currentlyAssignedExpressions
);
}
public function exitFirstLevelStatements(): self
{
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes,
false,
$this->currentlyAssignedExpressions
);
}
public function isInFirstLevelStatement(): bool
{
return $this->inFirstLevelStatement;
}
public function isNegated(): bool
{
return $this->negated;
}
private function addMoreSpecificTypes(array $types): self
{
$moreSpecificTypes = $this->moreSpecificTypes;
foreach ($types as $exprString => $type) {
$moreSpecificTypes[$exprString] = $type;
}
return new self(
$this->broker,
$this->printer,
$this->typeSpecifier,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->isInClass() ? $this->getClassReflection() : null,
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->getInFunctionCall(),
$this->isNegated(),
$moreSpecificTypes,
$this->inFirstLevelStatement
);
}
public function canAccessProperty(PropertyReflection $propertyReflection): bool
{
return $this->canAccessClassMember($propertyReflection);
}
public function canCallMethod(MethodReflection $methodReflection): bool
{
if ($this->canAccessClassMember($methodReflection)) {
return true;
}
return $this->canAccessClassMember($methodReflection->getPrototype());
}
public function canAccessConstant(ClassConstantReflection $constantReflection): bool
{
return $this->canAccessClassMember($constantReflection);
}
private function canAccessClassMember(ClassMemberReflection $classMemberReflection): bool
{
if ($classMemberReflection->isPublic()) {
return true;
}
if ($this->inClosureBindScopeClass !== null && $this->broker->hasClass($this->inClosureBindScopeClass)) {
$currentClassReflection = $this->broker->getClass($this->inClosureBindScopeClass);
} elseif ($this->isInClass()) {
$currentClassReflection = $this->getClassReflection();
} else {
return false;
}
$classReflectionName = $classMemberReflection->getDeclaringClass()->getName();
if ($classMemberReflection->isPrivate()) {
return $currentClassReflection->getName() === $classReflectionName;
}
// protected
if (
$currentClassReflection->getName() === $classReflectionName
|| $currentClassReflection->isSubclassOf($classReflectionName)
) {
return true;
}
return $classMemberReflection->getDeclaringClass()->isSubclassOf($currentClassReflection->getName());
}
/**
* @return string[]
*/
public function debug(): array
{
$descriptions = [];
foreach ($this->getVariableTypes() as $name => $variableTypeHolder) {
$key = sprintf('$%s (%s)', $name, $variableTypeHolder->getCertainty()->describe());
$descriptions[$key] = $variableTypeHolder->getType()->describe();
}
foreach ($this->moreSpecificTypes as $exprString => $type) {
$key = $exprString;
if (isset($descriptions[$key])) {
$key .= '-specified';
}
$descriptions[$key] = $type->describe();
}
return $descriptions;
}
}
src/Analyser/SpecifiedTypes.php 0000666 00000004143 13436751442 0012561 0 ustar 00 sureTypes = $sureTypes;
$this->sureNotTypes = $sureNotTypes;
}
/**
* @return mixed[]
*/
public function getSureTypes(): array
{
return $this->sureTypes;
}
/**
* @return mixed[]
*/
public function getSureNotTypes(): array
{
return $this->sureNotTypes;
}
public function intersectWith(SpecifiedTypes $other): self
{
$sureTypeUnion = [];
$sureNotTypeUnion = [];
foreach ($this->sureTypes as $exprString => list($exprNode, $type)) {
if (isset($other->sureTypes[$exprString])) {
$sureTypeUnion[$exprString] = [
$exprNode,
TypeCombinator::union($type, $other->sureTypes[$exprString][1]),
];
}
}
foreach ($this->sureNotTypes as $exprString => list($exprNode, $type)) {
if (isset($other->sureNotTypes[$exprString])) {
$sureNotTypeUnion[$exprString] = [
$exprNode,
TypeCombinator::intersect($type, $other->sureNotTypes[$exprString][1]),
];
}
}
return new self($sureTypeUnion, $sureNotTypeUnion);
}
public function unionWith(SpecifiedTypes $other): self
{
$sureTypeUnion = $this->sureTypes + $other->sureTypes;
$sureNotTypeUnion = $this->sureNotTypes + $other->sureNotTypes;
foreach ($this->sureTypes as $exprString => list($exprNode, $type)) {
if (isset($other->sureTypes[$exprString])) {
$sureTypeUnion[$exprString] = [
$exprNode,
TypeCombinator::intersect($type, $other->sureTypes[$exprString][1]),
];
}
}
foreach ($this->sureNotTypes as $exprString => list($exprNode, $type)) {
if (isset($other->sureNotTypes[$exprString])) {
$sureNotTypeUnion[$exprString] = [
$exprNode,
TypeCombinator::union($type, $other->sureNotTypes[$exprString][1]),
];
}
}
return new self($sureTypeUnion, $sureNotTypeUnion);
}
}
src/Analyser/UndefinedVariableException.php 0000666 00000001123 13436751442 0015062 0 ustar 00 scope = $scope;
$this->variableName = $variableName;
}
public function getScope(): Scope
{
return $this->scope;
}
public function getVariableName(): string
{
return $this->variableName;
}
}
src/ShouldNotHappenException.php 0000666 00000000335 13436751442 0013014 0 ustar 00 value = $value;
}
public static function createYes(): self
{
return new self(self::YES);
}
public static function createNo(): self
{
return new self(self::NO);
}
public static function createMaybe(): self
{
return new self(self::MAYBE);
}
public function yes(): bool
{
return $this->value === self::YES;
}
public function maybe(): bool
{
return $this->value === self::MAYBE;
}
public function no(): bool
{
return $this->value === self::NO;
}
public function and(self ...$operands): self
{
$operandValues = array_map(function (self $trinaryLogic): int {
return $trinaryLogic->value;
}, $operands);
$operandValues[] = $this->value;
return new self(min($operandValues));
}
public function or(self ...$operands): self
{
$operandValues = array_map(function (self $trinaryLogic): int {
return $trinaryLogic->value;
}, $operands);
$operandValues[] = $this->value;
return new self(max($operandValues));
}
public static function extremeIdentity(self ...$operands): self
{
$operandValues = array_map(function (self $trinaryLogic): int {
return $trinaryLogic->value;
}, $operands);
$min = min($operandValues);
$max = max($operandValues);
return new self($min === $max ? $min : self::MAYBE);
}
public static function maxMin(self ...$operands): self
{
$operandValues = array_map(function (self $trinaryLogic): int {
return $trinaryLogic->value;
}, $operands);
return new self(max($operandValues) ?: min($operandValues));
}
public function negate(): self
{
return new self(-$this->value);
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
public function addMaybe(): self
{
$value = $this->value;
if ($value === self::NO) {
$value = self::MAYBE;
}
return new self($value);
}
public function describe(): string
{
$labels = [
self::NO => 'No',
self::MAYBE => 'Maybe',
self::YES => 'Yes',
];
return $labels[$this->value];
}
public static function __set_state(array $properties): self
{
return new self($properties['value']);
}
}
src/Broker/FunctionNotFoundException.php 0000666 00000000775 13436751442 0014437 0 ustar 00 functionName = $functionName;
}
public function getFunctionName(): string
{
return $this->functionName;
}
}
src/Broker/Broker.php 0000666 00000025260 13436751442 0010536 0 ustar 00 propertiesClassReflectionExtensions = $propertiesClassReflectionExtensions;
$this->methodsClassReflectionExtensions = $methodsClassReflectionExtensions;
foreach (array_merge($propertiesClassReflectionExtensions, $methodsClassReflectionExtensions, $dynamicMethodReturnTypeExtensions, $dynamicStaticMethodReturnTypeExtensions, $dynamicFunctionReturnTypeExtensions) as $extension) {
if ($extension instanceof BrokerAwareExtension) {
$extension->setBroker($this);
}
}
$this->dynamicMethodReturnTypeExtensions = $dynamicMethodReturnTypeExtensions;
$this->dynamicStaticMethodReturnTypeExtensions = $dynamicStaticMethodReturnTypeExtensions;
foreach ($dynamicFunctionReturnTypeExtensions as $functionReturnTypeExtension) {
$this->dynamicFunctionReturnTypeExtensions[] = $functionReturnTypeExtension;
}
$this->functionReflectionFactory = $functionReflectionFactory;
$this->fileTypeMapper = $fileTypeMapper;
self::$instance = $this;
}
public static function getInstance(): self
{
if (self::$instance === null) {
throw new \PHPStan\ShouldNotHappenException();
}
return self::$instance;
}
/**
* @param string $className
* @return \PHPStan\Type\DynamicMethodReturnTypeExtension[]
*/
public function getDynamicMethodReturnTypeExtensionsForClass(string $className): array
{
if ($this->dynamicMethodReturnTypeExtensionsByClass === null) {
$byClass = [];
foreach ($this->dynamicMethodReturnTypeExtensions as $extension) {
$byClass[$extension->getClass()][] = $extension;
}
$this->dynamicMethodReturnTypeExtensionsByClass = $byClass;
}
return $this->getDynamicExtensionsForType($this->dynamicMethodReturnTypeExtensionsByClass, $className);
}
/**
* @param string $className
* @return \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[]
*/
public function getDynamicStaticMethodReturnTypeExtensionsForClass(string $className): array
{
if ($this->dynamicStaticMethodReturnTypeExtensionsByClass === null) {
$byClass = [];
foreach ($this->dynamicStaticMethodReturnTypeExtensions as $extension) {
$byClass[$extension->getClass()][] = $extension;
}
$this->dynamicStaticMethodReturnTypeExtensionsByClass = $byClass;
}
return $this->getDynamicExtensionsForType($this->dynamicStaticMethodReturnTypeExtensionsByClass, $className);
}
/**
* @return \PHPStan\Type\DynamicFunctionReturnTypeExtension[]
*/
public function getDynamicFunctionReturnTypeExtensions(): array
{
return $this->dynamicFunctionReturnTypeExtensions;
}
/**
* @param \PHPStan\Type\DynamicMethodReturnTypeExtension[]|\PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] $extensions
* @param string $className
* @return mixed[]
*/
private function getDynamicExtensionsForType(array $extensions, string $className): array
{
$extensionsForClass = [];
$class = $this->getClass($className);
foreach (array_merge([$className], $class->getParentClassesNames(), $class->getNativeReflection()->getInterfaceNames()) as $extensionClassName) {
if (!isset($extensions[$extensionClassName])) {
continue;
}
$extensionsForClass = array_merge($extensionsForClass, $extensions[$extensionClassName]);
}
return $extensionsForClass;
}
public function getClass(string $className): \PHPStan\Reflection\ClassReflection
{
if (!$this->hasClass($className)) {
throw new \PHPStan\Broker\ClassNotFoundException($className);
}
if (!isset($this->classReflections[$className])) {
$reflectionClass = new ReflectionClass($className);
$classReflection = $this->getClassFromReflection(
$reflectionClass,
$reflectionClass->getName(),
$reflectionClass->isAnonymous()
);
$this->classReflections[$className] = $classReflection;
if ($className !== $reflectionClass->getName()) {
// class alias optimization
$this->classReflections[$reflectionClass->getName()] = $classReflection;
}
}
return $this->classReflections[$className];
}
public function getClassFromReflection(\ReflectionClass $reflectionClass, string $displayName, bool $anonymous): \PHPStan\Reflection\ClassReflection
{
$className = $reflectionClass->getName();
if (!isset($this->classReflections[$className])) {
$classReflection = new ClassReflection(
$this,
$this->propertiesClassReflectionExtensions,
$this->methodsClassReflectionExtensions,
$displayName,
$reflectionClass,
$anonymous
);
$this->classReflections[$className] = $classReflection;
}
return $this->classReflections[$className];
}
public function hasClass(string $className): bool
{
if (isset($this->hasClassCache[$className])) {
return $this->hasClassCache[$className];
}
spl_autoload_register($autoloader = function (string $autoloadedClassName) use ($className) {
if ($autoloadedClassName !== $className) {
throw new \PHPStan\Broker\ClassAutoloadingException($autoloadedClassName);
}
});
try {
return $this->hasClassCache[$className] = class_exists($className) || interface_exists($className) || trait_exists($className);
} catch (\PHPStan\Broker\ClassAutoloadingException $e) {
throw $e;
} catch (\Throwable $t) {
throw new \PHPStan\Broker\ClassAutoloadingException(
$className,
$t
);
} finally {
spl_autoload_unregister($autoloader);
}
}
public function getFunction(\PhpParser\Node\Name $nameNode, Scope $scope = null): \PHPStan\Reflection\FunctionReflection
{
$functionName = $this->resolveFunctionName($nameNode, $scope);
if ($functionName === null) {
throw new \PHPStan\Broker\FunctionNotFoundException((string) $nameNode);
}
$lowerCasedFunctionName = strtolower($functionName);
if (!isset($this->functionReflections[$lowerCasedFunctionName])) {
$reflectionFunction = new \ReflectionFunction($lowerCasedFunctionName);
$phpDocParameterTags = [];
$phpDocReturnTag = null;
if ($reflectionFunction->getFileName() !== false && $reflectionFunction->getDocComment() !== false) {
$fileName = $reflectionFunction->getFileName();
$docComment = $reflectionFunction->getDocComment();
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, null, $docComment);
$phpDocParameterTags = $resolvedPhpDoc->getParamTags();
$phpDocReturnTag = $resolvedPhpDoc->getReturnTag();
}
$this->functionReflections[$lowerCasedFunctionName] = $this->functionReflectionFactory->create(
$reflectionFunction,
array_map(function (ParamTag $paramTag): Type {
return $paramTag->getType();
}, $phpDocParameterTags),
$phpDocReturnTag !== null ? $phpDocReturnTag->getType() : null
);
}
return $this->functionReflections[$lowerCasedFunctionName];
}
public function hasFunction(\PhpParser\Node\Name $nameNode, Scope $scope = null): bool
{
return $this->resolveFunctionName($nameNode, $scope) !== null;
}
/**
* @param \PhpParser\Node\Name $nameNode
* @param \PHPStan\Analyser\Scope|null $scope
* @return string|null
*/
public function resolveFunctionName(\PhpParser\Node\Name $nameNode, Scope $scope = null)
{
return $this->resolveName($nameNode, function (string $name): bool {
return function_exists($name);
}, $scope);
}
public function hasConstant(\PhpParser\Node\Name $nameNode, Scope $scope = null): bool
{
return $this->resolveConstantName($nameNode, $scope) !== null;
}
/**
* @param \PhpParser\Node\Name $nameNode
* @param \PHPStan\Analyser\Scope|null $scope
* @return string|null
*/
public function resolveConstantName(\PhpParser\Node\Name $nameNode, Scope $scope = null)
{
return $this->resolveName($nameNode, function (string $name): bool {
return defined($name);
}, $scope);
}
/**
* @param \PhpParser\Node\Name $nameNode
* @param \Closure $existsCallback
* @param \PHPStan\Analyser\Scope|null $scope
* @return string|null
*/
private function resolveName(
\PhpParser\Node\Name $nameNode,
\Closure $existsCallback,
Scope $scope = null
)
{
$name = (string) $nameNode;
if ($scope !== null && $scope->getNamespace() !== null && !$nameNode->isFullyQualified()) {
$namespacedName = sprintf('%s\\%s', $scope->getNamespace(), $name);
if ($existsCallback($namespacedName)) {
return $namespacedName;
}
}
if ($existsCallback($name)) {
return $name;
}
return null;
}
}
src/Broker/ClassAutoloadingException.php 0000666 00000001316 13436751442 0014421 0 ustar 00 getMessage(),
$functionName
), 0, $previous);
} else {
parent::__construct(sprintf(
'Class %s not found and could not be autoloaded.',
$functionName
), 0);
}
$this->className = $functionName;
}
public function getClassName(): string
{
return $this->className;
}
}
src/Broker/ClassNotFoundException.php 0000666 00000000757 13436751442 0013717 0 ustar 00 className = $functionName;
}
public function getClassName(): string
{
return $this->className;
}
}
src/Broker/BrokerFactory.php 0000666 00000005171 13436751442 0012065 0 ustar 00 container = $container;
}
public function create(): Broker
{
$tagToService = function (array $tags) {
return array_map(function (string $serviceName) {
return $this->container->getService($serviceName);
}, array_keys($tags));
};
$phpClassReflectionExtension = $this->container->getByType(PhpClassReflectionExtension::class);
$annotationsMethodsClassReflectionExtension = $this->container->getByType(AnnotationsMethodsClassReflectionExtension::class);
$annotationsPropertiesClassReflectionExtension = $this->container->getByType(AnnotationsPropertiesClassReflectionExtension::class);
$phpDefectClassReflectionExtension = $this->container->getByType(PhpDefectClassReflectionExtension::class);
return new Broker(
array_merge([$phpClassReflectionExtension, $phpDefectClassReflectionExtension], $tagToService($this->container->findByTag(self::PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG)), [$annotationsPropertiesClassReflectionExtension]),
array_merge([$phpClassReflectionExtension], $tagToService($this->container->findByTag(self::METHODS_CLASS_REFLECTION_EXTENSION_TAG)), [$annotationsMethodsClassReflectionExtension]),
$tagToService($this->container->findByTag(self::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG)),
$tagToService($this->container->findByTag(self::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG)),
$tagToService($this->container->findByTag(self::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG)),
$this->container->getByType(FunctionReflectionFactory::class),
$this->container->getByType(FileTypeMapper::class)
);
}
}
src/Reflection/FunctionReflectionFactory.php 0000666 00000000426 13436751442 0015305 0 ustar 00 methodReflectionFactory = $methodReflectionFactory;
$this->fileTypeMapper = $fileTypeMapper;
$this->annotationsMethodsClassReflectionExtension = $annotationsMethodsClassReflectionExtension;
$this->annotationsPropertiesClassReflectionExtension = $annotationsPropertiesClassReflectionExtension;
}
public function setBroker(Broker $broker)
{
$this->broker = $broker;
}
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
return $classReflection->getNativeReflection()->hasProperty($propertyName);
}
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
{
if (!isset($this->propertiesIncludingAnnotations[$classReflection->getName()][$propertyName])) {
$this->propertiesIncludingAnnotations[$classReflection->getName()][$propertyName] = $this->createProperty($classReflection, $propertyName, true);
}
return $this->propertiesIncludingAnnotations[$classReflection->getName()][$propertyName];
}
public function getNativeProperty(ClassReflection $classReflection, string $propertyName): PhpPropertyReflection
{
if (!isset($this->nativeProperties[$classReflection->getName()][$propertyName])) {
/** @var \PHPStan\Reflection\Php\PhpPropertyReflection $property */
$property = $this->createProperty($classReflection, $propertyName, false);
$this->nativeProperties[$classReflection->getName()][$propertyName] = $property;
}
return $this->nativeProperties[$classReflection->getName()][$propertyName];
}
private function createProperty(
ClassReflection $classReflection,
string $propertyName,
bool $includingAnnotations
): PropertyReflection
{
$propertyReflection = $classReflection->getNativeReflection()->getProperty($propertyName);
$propertyName = $propertyReflection->getName();
$declaringClassReflection = $this->broker->getClass($propertyReflection->getDeclaringClass()->getName());
if ($includingAnnotations && $this->annotationsPropertiesClassReflectionExtension->hasProperty($classReflection, $propertyName)) {
$hierarchyDistances = $classReflection->getClassHierarchyDistances();
$annotationProperty = $this->annotationsPropertiesClassReflectionExtension->getProperty($classReflection, $propertyName);
if (!isset($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()])) {
throw new \PHPStan\ShouldNotHappenException();
}
if (!isset($hierarchyDistances[$propertyReflection->getDeclaringClass()->getName()])) {
throw new \PHPStan\ShouldNotHappenException();
}
if ($hierarchyDistances[$annotationProperty->getDeclaringClass()->getName()] < $hierarchyDistances[$propertyReflection->getDeclaringClass()->getName()]) {
return $annotationProperty;
}
}
if ($propertyReflection->getDocComment() === false) {
$type = new MixedType();
} elseif (!$classReflection->isAnonymous() && !$declaringClassReflection->isAnonymous() && $declaringClassReflection->getFileName() !== false) {
$phpDocBlock = PhpDocBlock::resolvePhpDocBlockForProperty(
$this->broker,
$propertyReflection->getDocComment(),
$declaringClassReflection->getName(),
$propertyName,
$declaringClassReflection->getFileName()
);
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$phpDocBlock->getFile(),
$phpDocBlock->getClass(),
$phpDocBlock->getDocComment()
);
$varTags = $resolvedPhpDoc->getVarTags();
if (isset($varTags[0]) && count($varTags) === 1) {
$type = $varTags[0]->getType();
} elseif (isset($varTags[$propertyName])) {
$type = $varTags[$propertyName]->getType();
} else {
$type = new MixedType();
}
} else {
$type = new MixedType();
}
return new PhpPropertyReflection(
$declaringClassReflection,
$type,
$propertyReflection
);
}
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
return $classReflection->getNativeReflection()->hasMethod($methodName);
}
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
{
if (isset($this->methodsIncludingAnnotations[$classReflection->getName()][$methodName])) {
return $this->methodsIncludingAnnotations[$classReflection->getName()][$methodName];
}
$nativeMethodReflection = $classReflection->getNativeReflection()->getMethod($methodName);
if (!isset($this->methodsIncludingAnnotations[$classReflection->getName()][$nativeMethodReflection->getName()])) {
/** @var \PHPStan\Reflection\Php\PhpMethodReflection $method */
$method = $this->createMethod($classReflection, $nativeMethodReflection, true);
$this->methodsIncludingAnnotations[$classReflection->getName()][$nativeMethodReflection->getName()] = $method;
if ($nativeMethodReflection->getName() !== $methodName) {
$this->methodsIncludingAnnotations[$classReflection->getName()][$methodName] = $method;
}
}
return $this->methodsIncludingAnnotations[$classReflection->getName()][$nativeMethodReflection->getName()];
}
public function getNativeMethod(ClassReflection $classReflection, string $methodName): PhpMethodReflection
{
if (isset($this->nativeMethods[$classReflection->getName()][$methodName])) {
return $this->nativeMethods[$classReflection->getName()][$methodName];
}
$nativeMethodReflection = $classReflection->getNativeReflection()->getMethod($methodName);
if (!isset($this->nativeMethods[$classReflection->getName()][$nativeMethodReflection->getName()])) {
/** @var \PHPStan\Reflection\Php\PhpMethodReflection $method */
$method = $this->createMethod($classReflection, $nativeMethodReflection, false);
$this->nativeMethods[$classReflection->getName()][$nativeMethodReflection->getName()] = $method;
}
return $this->nativeMethods[$classReflection->getName()][$nativeMethodReflection->getName()];
}
private function createMethod(
ClassReflection $classReflection,
\ReflectionMethod $methodReflection,
bool $includingAnnotations
): MethodReflection
{
if ($includingAnnotations && $this->annotationsMethodsClassReflectionExtension->hasMethod($classReflection, $methodReflection->getName())) {
$hierarchyDistances = $classReflection->getClassHierarchyDistances();
$annotationMethod = $this->annotationsMethodsClassReflectionExtension->getMethod($classReflection, $methodReflection->getName());
if (!isset($hierarchyDistances[$annotationMethod->getDeclaringClass()->getName()])) {
throw new \PHPStan\ShouldNotHappenException();
}
if (!isset($hierarchyDistances[$methodReflection->getDeclaringClass()->getName()])) {
throw new \PHPStan\ShouldNotHappenException();
}
if ($hierarchyDistances[$annotationMethod->getDeclaringClass()->getName()] < $hierarchyDistances[$methodReflection->getDeclaringClass()->getName()]) {
return $annotationMethod;
}
}
$declaringClass = $this->broker->getClass($methodReflection->getDeclaringClass()->getName());
$phpDocParameterTypes = [];
$phpDocReturnType = null;
if (!$classReflection->isAnonymous() && !$declaringClass->isAnonymous() && $declaringClass->getFileName() !== false) {
if ($methodReflection->getDocComment() !== false) {
$phpDocBlock = PhpDocBlock::resolvePhpDocBlockForMethod(
$this->broker,
$methodReflection->getDocComment(),
$declaringClass->getName(),
$methodReflection->getName(),
$declaringClass->getFileName()
);
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$phpDocBlock->getFile(),
$phpDocBlock->getClass(),
$phpDocBlock->getDocComment()
);
$phpDocParameterTypes = array_map(function (ParamTag $tag): Type {
return $tag->getType();
}, $resolvedPhpDoc->getParamTags());
$phpDocReturnType = $resolvedPhpDoc->getReturnTag() !== null ? $resolvedPhpDoc->getReturnTag()->getType() : null;
}
}
return $this->methodReflectionFactory->create(
$declaringClass,
$methodReflection,
$phpDocParameterTypes,
$phpDocReturnType
);
}
}
src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php 0000666 00000002656 13436751442 0022052 0 ustar 00 classes = $classes;
}
public function setBroker(Broker $broker)
{
$this->broker = $broker;
}
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
if ($this->filteredClasses === null) {
$this->filteredClasses = array_values(array_filter($this->classes, function (string $class): bool {
return $this->broker->hasClass($class);
}));
}
if ($classReflection->hasNativeProperty($propertyName)) {
return false;
}
foreach ($this->filteredClasses as $className) {
if (
$classReflection->getName() === $className
|| $classReflection->isSubclassOf($className)
) {
return true;
}
}
return false;
}
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
{
return new UniversalObjectCrateProperty($classReflection);
}
}
src/Reflection/Php/DummyParameter.php 0000666 00000002032 13436751442 0013633 0 ustar 00 name = $name;
$this->type = $type;
$this->optional = $optional;
$this->passedByReference = $passedByReference;
$this->variadic = $variadic;
}
public function getName(): string
{
return $this->name;
}
public function isOptional(): bool
{
return $this->optional;
}
public function getType(): Type
{
return $this->type;
}
public function isPassedByReference(): bool
{
return $this->passedByReference;
}
public function isVariadic(): bool
{
return $this->variadic;
}
}
src/Reflection/Php/PhpFunctionFromParserNodeReflection.php 0000666 00000006322 13436751442 0017764 0 ustar 00 functionLike = $functionLike;
$this->realParameterTypes = $realParameterTypes;
$this->phpDocParameterTypes = $phpDocParameterTypes;
$this->realReturnTypePresent = $realReturnTypePresent;
$this->realReturnType = $realReturnType;
$this->phpDocReturnType = $phpDocReturnType;
}
protected function getFunctionLike(): FunctionLike
{
return $this->functionLike;
}
public function getName(): string
{
if ($this->functionLike instanceof ClassMethod) {
return $this->functionLike->name;
}
return (string) $this->functionLike->namespacedName;
}
/**
* @return \PHPStan\Reflection\ParameterReflection[]
*/
public function getParameters(): array
{
if ($this->parameters === null) {
$parameters = [];
$isOptional = true;
foreach (array_reverse($this->functionLike->getParams()) as $parameter) {
if (!$isOptional || $parameter->default === null) {
$isOptional = false;
}
$parameters[] = new PhpParameterFromParserNodeReflection(
$parameter->name,
$isOptional,
$this->realParameterTypes[$parameter->name],
isset($this->phpDocParameterTypes[$parameter->name]) ? $this->phpDocParameterTypes[$parameter->name] : null,
$parameter->byRef,
$parameter->default,
$parameter->variadic
);
}
$this->parameters = array_reverse($parameters);
}
return $this->parameters;
}
public function isVariadic(): bool
{
if ($this->isVariadic === null) {
$isVariadic = false;
foreach ($this->functionLike->getParams() as $parameter) {
if ($parameter->variadic) {
$isVariadic = true;
break;
}
}
$this->isVariadic = $isVariadic;
}
return $this->isVariadic;
}
public function getReturnType(): Type
{
if ($this->returnType === null) {
$phpDocReturnType = $this->phpDocReturnType;
if (
$this->realReturnTypePresent
&& $phpDocReturnType !== null
&& TypeCombinator::containsNull($this->realReturnType) !== TypeCombinator::containsNull($phpDocReturnType)
) {
$phpDocReturnType = null;
}
$this->returnType = TypehintHelper::decideType($this->realReturnType, $phpDocReturnType);
}
return $this->returnType;
}
}
src/Reflection/Php/PhpParameterFromParserNodeReflection.php 0000666 00000003526 13436751442 0020122 0 ustar 00 name = $name;
$this->optional = $optional;
$this->realType = $realType;
$this->phpDocType = $phpDocType;
$this->passedByReference = $passedByReference;
$this->defaultValue = $defaultValue;
$this->variadic = $variadic;
}
public function getName(): string
{
return $this->name;
}
public function isOptional(): bool
{
return $this->optional;
}
public function getType(): Type
{
if ($this->type === null) {
$phpDocType = $this->phpDocType;
if ($phpDocType !== null && $this->defaultValue !== null) {
if (
$this->defaultValue instanceof ConstFetch
&& strtolower((string) $this->defaultValue->name) === 'null'
) {
$phpDocType = \PHPStan\Type\TypeCombinator::addNull($phpDocType);
}
}
$this->type = TypehintHelper::decideType($this->realType, $phpDocType);
}
return $this->type;
}
public function isPassedByReference(): bool
{
return $this->passedByReference;
}
public function isVariadic(): bool
{
return $this->variadic;
}
}
src/Reflection/Php/PhpMethodReflection.php 0000666 00000025542 13436751442 0014615 0 ustar 00 declaringClass = $declaringClass;
$this->reflection = $reflection;
$this->broker = $broker;
$this->parser = $parser;
$this->functionCallStatementFinder = $functionCallStatementFinder;
$this->cache = $cache;
$this->phpDocParameterTypes = $phpDocParameterTypes;
$this->phpDocReturnType = $phpDocReturnType;
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
/**
* @return string|false
*/
public function getDocComment()
{
return $this->reflection->getDocComment();
}
public function getPrototype(): MethodReflection
{
try {
$prototypeReflection = $this->reflection->getPrototype();
$prototypeDeclaringClass = $this->broker->getClassFromReflection(
$prototypeReflection->getDeclaringClass(),
$prototypeReflection->getDeclaringClass()->getName(),
$prototypeReflection->getDeclaringClass()->isAnonymous()
);
return new self(
$prototypeDeclaringClass,
$prototypeReflection,
$this->broker,
$this->parser,
$this->functionCallStatementFinder,
$this->cache,
$this->phpDocParameterTypes,
$this->phpDocReturnType
);
} catch (\ReflectionException $e) {
return $this;
}
}
public function isStatic(): bool
{
return $this->reflection->isStatic();
}
public function getName(): string
{
$name = $this->reflection->getName();
$lowercaseName = strtolower($name);
if ($lowercaseName === $name) {
// fix for https://bugs.php.net/bug.php?id=74939
foreach ($this->getDeclaringClass()->getNativeReflection()->getTraitAliases() as $traitTarget) {
$correctName = $this->getMethodNameWithCorrectCase($name, $traitTarget);
if ($correctName !== null) {
$name = $correctName;
break;
}
}
}
return $name;
}
/**
* @param string $lowercaseMethodName
* @param string $traitTarget
* @return string|null
*/
private function getMethodNameWithCorrectCase(string $lowercaseMethodName, string $traitTarget)
{
list ($trait, $method) = explode('::', $traitTarget);
$traitReflection = $this->broker->getClass($trait)->getNativeReflection();
foreach ($traitReflection->getTraitAliases() as $methodAlias => $traitTarget) {
if ($lowercaseMethodName === strtolower($methodAlias)) {
return $methodAlias;
}
$correctName = $this->getMethodNameWithCorrectCase($lowercaseMethodName, $traitTarget);
if ($correctName !== null) {
return $correctName;
}
}
return null;
}
/**
* @return \PHPStan\Reflection\ParameterReflection[]
*/
public function getParameters(): array
{
if ($this->parameters === null) {
$this->parameters = array_map(function (\ReflectionParameter $reflection) {
return new PhpParameterReflection(
$reflection,
isset($this->phpDocParameterTypes[$reflection->getName()]) ? $this->phpDocParameterTypes[$reflection->getName()] : null
);
}, $this->reflection->getParameters());
if (
$this->reflection->getName() === '__construct'
&& $this->declaringClass->getName() === 'ArrayObject'
&& count($this->parameters) === 1
) {
// PHP bug #71077
$this->parameters[] = new DummyParameter(
'flags',
new IntegerType(),
true
);
$this->parameters[] = new DummyParameter(
'iterator_class',
new StringType(),
true
);
}
if (
$this->declaringClass->getName() === 'ReflectionMethod'
&& $this->reflection->getName() === 'invoke'
&& !$this->parameters[1]->isOptional()
) {
// PHP bug #71416
$this->parameters[1] = new DummyParameter(
'parameter',
new MixedType(),
true,
false,
true
);
}
if (
$this->declaringClass->getName() === 'PDO'
&& $this->reflection->getName() === 'query'
&& count($this->parameters) < 4
) {
$this->parameters[] = new DummyParameter(
'statement',
new StringType(),
false
);
$this->parameters[] = new DummyParameter(
'fetchColumn',
new IntegerType(),
true
);
$this->parameters[] = new DummyParameter(
'colno',
new MixedType(),
true
);
$this->parameters[] = new DummyParameter(
'constructorArgs',
new ArrayType(new MixedType(), new MixedType(), false),
true
);
}
if (
$this->declaringClass->getName() === 'DatePeriod'
&& $this->reflection->getName() === '__construct'
&& count($this->parameters) < 4
) {
$this->parameters[] = new DummyParameter(
'options',
new IntegerType(),
true
);
}
if (
$this->declaringClass->getName() === 'Closure'
&& $this->reflection->getName() === '__invoke'
&& count($this->parameters) < 1
) {
$this->parameters[] = new DummyParameter(
'args',
new MixedType(),
true,
false,
true
);
}
if (
$this->declaringClass->getName() === 'ReflectionClass'
&& $this->reflection->getName() === 'newInstance'
&& count($this->parameters) === 1
) {
$this->parameters[0] = new DummyParameter(
'args',
new MixedType(),
true,
false,
true
);
}
if (
$this->declaringClass->getName() === 'DateTimeZone'
&& $this->reflection->getName() === 'getTransitions'
&& count($this->parameters) === 2
) {
$this->parameters[0] = new DummyParameter(
'timestamp_begin',
new IntegerType(),
true
);
$this->parameters[1] = new DummyParameter(
'timestamp_end',
new IntegerType(),
true
);
}
if (
$this->declaringClass->getName() === 'Locale'
&& $this->reflection->getName() === 'getDisplayLanguage'
) {
$this->parameters[1] = new DummyParameter(
'in_locale',
new StringType(),
true
);
}
if (
$this->declaringClass->getName() === 'DOMDocument'
&& $this->reflection->getName() === 'saveHTML'
&& count($this->parameters) === 0
) {
$this->parameters[] = new DummyParameter(
'node',
TypeCombinator::addNull(new ObjectType('DOMNode')),
true
);
}
}
return $this->parameters;
}
public function isVariadic(): bool
{
$isNativelyVariadic = $this->reflection->isVariadic();
if (
!$isNativelyVariadic
&& (
(
$this->declaringClass->getName() === 'ReflectionMethod'
&& $this->reflection->getName() === 'invoke'
)
|| (
$this->declaringClass->getName() === 'Closure'
&& $this->reflection->getName() === '__invoke'
)
|| (
$this->declaringClass->getName() === 'ReflectionClass'
&& $this->reflection->getName() === 'newInstance'
)
)
) {
return true;
}
if (!$isNativelyVariadic && $this->declaringClass->getFileName() !== false) {
$key = sprintf('variadic-method-%s-%s-v0', $this->declaringClass->getName(), $this->reflection->getName());
$cachedResult = $this->cache->load($key);
if ($cachedResult === null) {
$nodes = $this->parser->parseFile($this->declaringClass->getFileName());
$result = $this->callsFuncGetArgs($nodes);
$this->cache->save($key, $result);
return $result;
}
return $cachedResult;
}
return $isNativelyVariadic;
}
/**
* @param mixed $nodes
* @return bool
*/
private function callsFuncGetArgs($nodes): bool
{
foreach ($nodes as $node) {
if (is_array($node)) {
if ($this->callsFuncGetArgs($node)) {
return true;
}
}
if (!($node instanceof \PhpParser\Node)) {
continue;
}
if (
$node instanceof \PhpParser\Node\Stmt\ClassLike
&& isset($node->namespacedName)
&& $this->declaringClass->getName() !== (string) $node->namespacedName
) {
continue;
}
if ($node instanceof ClassMethod) {
if ($node->getStmts() === null) {
continue; // interface
}
$methodName = $node->name;
if ($methodName === $this->reflection->getName()) {
return $this->functionCallStatementFinder->findFunctionCallInStatements(ParametersAcceptor::VARIADIC_FUNCTIONS, $node->getStmts()) !== null;
}
continue;
}
if ($this->callsFuncGetArgs($node)) {
return true;
}
}
return false;
}
public function isPrivate(): bool
{
return $this->reflection->isPrivate();
}
public function isPublic(): bool
{
return $this->reflection->isPublic();
}
public function getReturnType(): Type
{
if ($this->returnType === null) {
if ($this->getName() === '__construct') {
return $this->returnType = new VoidType();
}
$returnType = $this->reflection->getReturnType();
$phpDocReturnType = $this->phpDocReturnType;
if (
$returnType !== null
&& $phpDocReturnType !== null
&& $returnType->allowsNull() !== TypeCombinator::containsNull($phpDocReturnType)
) {
$phpDocReturnType = null;
}
$this->returnType = TypehintHelper::decideTypeFromReflection(
$returnType,
$phpDocReturnType,
$this->declaringClass->getName()
);
}
return $this->returnType;
}
public function getPhpDocReturnType(): Type
{
if ($this->phpDocReturnType !== null) {
return $this->phpDocReturnType;
}
return new MixedType();
}
public function getNativeReturnType(): Type
{
if ($this->nativeReturnType === null) {
$this->nativeReturnType = TypehintHelper::decideTypeFromReflection(
$this->reflection->getReturnType(),
null,
$this->declaringClass->getName()
);
}
return $this->nativeReturnType;
}
}
src/Reflection/Php/PhpPropertyReflection.php 0000666 00000002400 13436751442 0015205 0 ustar 00 declaringClass = $declaringClass;
$this->type = $type;
$this->reflection = $reflection;
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
/**
* @return string|false
*/
public function getDocComment()
{
return $this->reflection->getDocComment();
}
public function isStatic(): bool
{
return $this->reflection->isStatic();
}
public function isPrivate(): bool
{
return $this->reflection->isPrivate();
}
public function isPublic(): bool
{
return $this->reflection->isPublic();
}
public function getType(): Type
{
return $this->type;
}
public function isReadable(): bool
{
return true;
}
public function isWritable(): bool
{
return true;
}
}
src/Reflection/Php/PhpMethodFromParserNodeReflection.php 0000666 00000003307 13436751442 0017417 0 ustar 00 declaringClass = $declaringClass;
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
public function getPrototype(): MethodReflection
{
return $this->declaringClass->getNativeMethod($this->getClassMethod()->name)->getPrototype();
}
private function getClassMethod(): ClassMethod
{
/** @var \PhpParser\Node\Stmt\ClassMethod $functionLike */
$functionLike = $this->getFunctionLike();
return $functionLike;
}
public function isStatic(): bool
{
return $this->getClassMethod()->isStatic();
}
public function isPrivate(): bool
{
return $this->getClassMethod()->isPrivate();
}
public function isPublic(): bool
{
return $this->getClassMethod()->isPublic();
}
public function getReturnType(): Type
{
if ($this->getName() === '__construct') {
return new VoidType();
}
return parent::getReturnType();
}
}
src/Reflection/Php/PhpParameterReflection.php 0000666 00000004072 13436751442 0015310 0 ustar 00 reflection = $reflection;
$this->phpDocType = $phpDocType;
}
public function isOptional(): bool
{
return $this->reflection->isOptional();
}
public function getName(): string
{
return $this->reflection->getName();
}
public function getType(): Type
{
if ($this->type === null) {
$phpDocType = $this->phpDocType;
if ($phpDocType !== null && $this->reflection->isDefaultValueAvailable() && $this->reflection->getDefaultValue() === null) {
$phpDocType = \PHPStan\Type\TypeCombinator::addNull($phpDocType);
}
$this->type = TypehintHelper::decideTypeFromReflection(
$this->reflection->getType(),
$phpDocType,
$this->reflection->getDeclaringClass() !== null ? $this->reflection->getDeclaringClass()->getName() : null,
$this->isVariadic()
);
}
return $this->type;
}
public function isPassedByReference(): bool
{
return $this->reflection->isPassedByReference();
}
public function isVariadic(): bool
{
return $this->reflection->isVariadic();
}
public function getPhpDocType(): Type
{
if ($this->phpDocType !== null) {
return $this->phpDocType;
}
return new MixedType();
}
public function getNativeType(): Type
{
if ($this->nativeType === null) {
$this->nativeType = TypehintHelper::decideTypeFromReflection(
$this->reflection->getType(),
null,
$this->reflection->getDeclaringClass() !== null ? $this->reflection->getDeclaringClass()->getName() : null,
$this->isVariadic()
);
}
return $this->nativeType;
}
}
src/Reflection/Php/UniversalObjectCrateProperty.php 0000666 00000001565 13436751442 0016534 0 ustar 00 declaringClass = $declaringClass;
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
public function isStatic(): bool
{
return false;
}
public function isPrivate(): bool
{
return false;
}
public function isPublic(): bool
{
return true;
}
public function getType(): Type
{
return new MixedType();
}
public function isReadable(): bool
{
return true;
}
public function isWritable(): bool
{
return true;
}
}
src/Reflection/Php/PhpMethodReflectionFactory.php 0000666 00000001172 13436751442 0016136 0 ustar 00 declaringClass = $declaringClass;
$this->reflection = $reflection;
}
public function getName(): string
{
return $this->reflection->getName();
}
/**
* @return mixed
*/
public function getValue()
{
return $this->reflection->getValue();
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
public function isStatic(): bool
{
return true;
}
public function isPrivate(): bool
{
return $this->reflection->isPrivate();
}
public function isPublic(): bool
{
return $this->reflection->isPublic();
}
}
src/Reflection/ParametersAcceptorWithPhpDocs.php 0000666 00000000560 13436751442 0016055 0 ustar 00 broker = $broker;
$this->propertiesClassReflectionExtensions = $propertiesClassReflectionExtensions;
$this->methodsClassReflectionExtensions = $methodsClassReflectionExtensions;
$this->displayName = $displayName;
$this->reflection = $reflection;
$this->anonymous = $anonymous;
}
public function getNativeReflection(): \ReflectionClass
{
return $this->reflection;
}
/**
* @return string|false
*/
public function getFileName()
{
$fileName = $this->reflection->getFileName();
if ($fileName === false) {
return false;
}
if (!file_exists($fileName)) {
return false;
}
return $fileName;
}
/**
* @return false|\PHPStan\Reflection\ClassReflection
*/
public function getParentClass()
{
if ($this->reflection->getParentClass() === false) {
return false;
}
return $this->broker->getClass($this->reflection->getParentClass()->getName());
}
public function getName(): string
{
return $this->reflection->getName();
}
public function getDisplayName(): string
{
return $this->displayName;
}
/**
* @return int[]
*/
public function getClassHierarchyDistances(): array
{
if ($this->classHierarchyDistances === null) {
$distance = 0;
$distances = [
$this->getName() => $distance,
];
$currentClassReflection = $this->getNativeReflection();
while ($currentClassReflection->getParentClass() !== false) {
$distance++;
$parentClassName = $currentClassReflection->getParentClass()->getName();
if (!array_key_exists($parentClassName, $distances)) {
$distances[$parentClassName] = $distance;
}
$currentClassReflection = $currentClassReflection->getParentClass();
}
foreach ($this->getNativeReflection()->getInterfaces() as $interface) {
$distance++;
if (array_key_exists($interface->getName(), $distances)) {
continue;
}
$distances[$interface->getName()] = $distance;
}
$this->classHierarchyDistances = $distances;
}
return $this->classHierarchyDistances;
}
public function hasProperty(string $propertyName): bool
{
foreach ($this->propertiesClassReflectionExtensions as $extension) {
if ($extension->hasProperty($this, $propertyName)) {
return true;
}
}
return false;
}
public function hasMethod(string $methodName): bool
{
foreach ($this->methodsClassReflectionExtensions as $extension) {
if ($extension->hasMethod($this, $methodName)) {
return true;
}
}
return false;
}
public function getMethod(string $methodName, Scope $scope): MethodReflection
{
$key = $methodName;
if ($scope->isInClass()) {
$key = sprintf('%s-%s', $key, $scope->getClassReflection()->getName());
}
if (!isset($this->methods[$key])) {
foreach ($this->methodsClassReflectionExtensions as $extension) {
if ($extension->hasMethod($this, $methodName)) {
$method = $extension->getMethod($this, $methodName);
if ($scope->canCallMethod($method)) {
return $this->methods[$key] = $method;
}
$this->methods[$key] = $method;
}
}
}
if (!isset($this->methods[$key])) {
throw new \PHPStan\Reflection\MissingMethodFromReflectionException($this->getName(), $methodName);
}
return $this->methods[$key];
}
public function hasNativeMethod(string $methodName): bool
{
return $this->getPhpExtension()->hasMethod($this, $methodName);
}
public function getNativeMethod(string $methodName): PhpMethodReflection
{
if (!$this->hasNativeMethod($methodName)) {
throw new \PHPStan\Reflection\MissingMethodFromReflectionException($this->getName(), $methodName);
}
return $this->getPhpExtension()->getNativeMethod($this, $methodName);
}
private function getPhpExtension(): PhpClassReflectionExtension
{
$extension = $this->methodsClassReflectionExtensions[0];
if (!$extension instanceof PhpClassReflectionExtension) {
throw new \PHPStan\ShouldNotHappenException();
}
return $extension;
}
public function getProperty(string $propertyName, Scope $scope): PropertyReflection
{
$key = $propertyName;
if ($scope->isInClass()) {
$key = sprintf('%s-%s', $key, $scope->getClassReflection()->getName());
}
if (!isset($this->properties[$key])) {
foreach ($this->propertiesClassReflectionExtensions as $extension) {
if ($extension->hasProperty($this, $propertyName)) {
$property = $extension->getProperty($this, $propertyName);
if ($scope->canAccessProperty($property)) {
return $this->properties[$key] = $property;
}
$this->properties[$key] = $property;
}
}
}
if (!isset($this->properties[$key])) {
throw new \PHPStan\Reflection\MissingPropertyFromReflectionException($this->getName(), $propertyName);
}
return $this->properties[$key];
}
public function hasNativeProperty(string $propertyName): bool
{
return $this->getPhpExtension()->hasProperty($this, $propertyName);
}
public function getNativeProperty(string $propertyName): PhpPropertyReflection
{
if (!$this->hasNativeProperty($propertyName)) {
throw new \PHPStan\Reflection\MissingPropertyFromReflectionException($this->getName(), $propertyName);
}
return $this->getPhpExtension()->getNativeProperty($this, $propertyName);
}
public function isAbstract(): bool
{
return $this->reflection->isAbstract();
}
public function isInterface(): bool
{
return $this->reflection->isInterface();
}
public function isTrait(): bool
{
return $this->reflection->isTrait();
}
public function isAnonymous(): bool
{
return $this->anonymous;
}
public function isSubclassOf(string $className): bool
{
return $this->reflection->isSubclassOf($className);
}
/**
* @return \PHPStan\Reflection\ClassReflection[]
*/
public function getParents(): array
{
$parents = [];
$parent = $this->getParentClass();
while ($parent !== false) {
$parents[] = $parent;
$parent = $parent->getParentClass();
}
return $parents;
}
/**
* @return \PHPStan\Reflection\ClassReflection[]
*/
public function getInterfaces(): array
{
return array_map(function (\ReflectionClass $interface) {
return $this->broker->getClass($interface->getName());
}, $this->getNativeReflection()->getInterfaces());
}
/**
* @return \PHPStan\Reflection\ClassReflection[]
*/
public function getTraits(): array
{
return array_map(function (\ReflectionClass $trait) {
return $this->broker->getClass($trait->getName());
}, $this->getNativeReflection()->getTraits());
}
/**
* @return string[]
*/
public function getParentClassesNames(): array
{
$parentNames = [];
$currentClassReflection = $this;
while ($currentClassReflection->getParentClass() !== false) {
$parentNames[] = $currentClassReflection->getParentClass()->getName();
$currentClassReflection = $currentClassReflection->getParentClass();
}
return $parentNames;
}
public function hasConstant(string $name): bool
{
return $this->getNativeReflection()->hasConstant($name);
}
public function getConstant(string $name): ClassConstantReflection
{
if (!isset($this->constants[$name])) {
if (PHP_VERSION_ID < 70100) {
$this->constants[$name] = new ObsoleteClassConstantReflection(
$this,
$name,
$this->getNativeReflection()->getConstant($name)
);
} else {
$reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name);
$this->constants[$name] = new ClassConstantWithVisibilityReflection(
$this->broker->getClass($reflectionConstant->getDeclaringClass()->getName()),
$reflectionConstant
);
}
}
return $this->constants[$name];
}
public function hasTraitUse(string $traitName): bool
{
return in_array($traitName, $this->getTraitNames(), true);
}
private function getTraitNames(): array
{
$class = $this->reflection;
$traitNames = $class->getTraitNames();
while ($class->getParentClass() !== false) {
$traitNames = array_values(array_unique(array_merge($traitNames, $class->getParentClass()->getTraitNames())));
$class = $class->getParentClass();
}
return $traitNames;
}
}
src/Reflection/PhpDefect/PhpDefectClassReflectionExtension.php 0000666 00000013135 13436751442 0020560 0 ustar 00 [
'y' => 'int',
'm' => 'int',
'd' => 'int',
'h' => 'int',
'i' => 'int',
's' => 'int',
'invert' => 'int',
'days' => 'mixed',
],
\DOMAttr::class => [ // extends DOMNode
'name' => 'string',
'ownerElement' => \DOMElement::class,
'schemaTypeInfo' => 'bool',
'specified' => 'bool',
'value' => 'string',
],
\DOMCharacterData::class => [ // extends DOMNode
'data' => 'string',
'length' => 'int',
],
\DOMDocument::class => [
'actualEncoding' => 'string',
'config' => \DOMConfiguration::class,
'doctype' => \DOMDocumentType::class,
'documentElement' => \DOMElement::class,
'documentURI' => 'string',
'encoding' => 'string',
'formatOutput' => 'bool',
'implementation' => \DOMImplementation::class,
'preserveWhiteSpace' => 'bool',
'recover' => 'bool',
'resolveExternals' => 'bool',
'standalone' => 'bool',
'strictErrorChecking' => 'bool',
'substituteEntities' => 'bool',
'validateOnParse' => 'bool',
'version' => 'string',
'xmlEncoding' => 'string',
'xmlStandalone' => 'bool',
'xmlVersion' => 'string',
],
\DOMDocumentType::class => [ // extends DOMNode
'publicId' => 'string',
'systemId' => 'string',
'name' => 'string',
'entities' => \DOMNamedNodeMap::class,
'notations' => \DOMNamedNodeMap::class,
'internalSubset' => 'string',
],
\DOMElement::class => [ // extends DOMNode
'schemaTypeInfo' => 'bool',
'tagName' => 'string',
],
\DOMEntity::class => [ // extends DOMNode
'publicId' => 'string',
'systemId' => 'string',
'notationName' => 'string',
'actualEncoding' => 'string',
'encoding' => 'string',
'version' => 'string',
],
\DOMNamedNodeMap::class => [
'length' => 'int',
],
\DOMNode::class => [
'nodeName' => 'string',
'nodeValue' => 'string',
'nodeType' => 'int',
'parentNode' => \DOMNode::class,
'childNodes' => \DOMNodeList::class,
'firstChild' => \DOMNode::class,
'lastChild' => \DOMNode::class,
'previousSibling' => \DOMNode::class,
'nextSibling' => \DOMNode::class,
'attributes' => \DOMNamedNodeMap::class,
'ownerDocument' => \DOMDocument::class,
'namespaceURI' => 'string',
'prefix' => 'string',
'localName' => 'string',
'baseURI' => 'string',
'textContent' => 'string',
],
\DOMNodeList::class => [
'length' => 'int',
],
\DOMNotation::class => [ // extends DOMNode
'publicId' => 'string',
'systemId' => 'string',
],
\DOMProcessingInstruction::class => [ // extends DOMNode
'target' => 'string',
'data' => 'string',
],
\DOMText::class => [ // extends DOMCharacterData
'wholeText' => 'string',
],
\DOMXPath::class => [ // extends DOMCharacterData
'document' => \DOMDocument::class,
],
\XMLReader::class => [
'attributeCount' => 'int',
'baseURI' => 'string',
'depth' => 'int',
'hasAttributes' => 'bool',
'hasValue' => 'bool',
'isDefault' => 'bool',
'isEmptyElement' => 'bool',
'localName' => 'string',
'name' => 'string',
'namespaceURI' => 'string',
'nodeType' => 'int',
'prefix' => 'string',
'value' => 'string',
'xmlLang' => 'string',
],
\ZipArchive::class => [
'status' => 'int',
'statusSys' => 'int',
'numFiles' => 'int',
'filename' => 'string',
'comment' => 'string',
],
\LibXMLError::class => [
'level' => 'int',
'code' => 'int',
'column' => 'int',
'message' => 'string',
'file' => 'string',
'line' => 'int',
],
];
/** @var string[][] */
private static $properties71 = [
\DateInterval::class => [
'f' => 'float',
],
];
/** @var TypeStringResolver */
private $typeStringResolver;
/** @var string[][] */
private $properties = [];
public function __construct(TypeStringResolver $typeStringResolver)
{
$this->typeStringResolver = $typeStringResolver;
$this->properties = self::$defaultProperties;
if (PHP_VERSION_ID >= 70100) { // since PHP 7.1
$this->properties = array_merge_recursive($this->properties, self::$properties71);
}
}
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
$classWithProperties = $this->getClassWithProperties($classReflection, $propertyName);
return $classWithProperties !== null;
}
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
{
/** @var \PHPStan\Reflection\ClassReflection $classWithProperties */
$classWithProperties = $this->getClassWithProperties($classReflection, $propertyName);
$typeString = $this->properties[$classWithProperties->getName()][$propertyName];
return new PhpDefectPropertyReflection(
$classWithProperties,
$this->typeStringResolver->resolve($typeString)
);
}
/**
* @param \PHPStan\Reflection\ClassReflection $classReflection
* @param string $propertyName
* @return \PHPStan\Reflection\ClassReflection|null
*/
private function getClassWithProperties(ClassReflection $classReflection, string $propertyName)
{
if (isset($this->properties[$classReflection->getName()][$propertyName])) {
return $classReflection;
}
foreach ($classReflection->getParents() as $parentClass) {
if (isset($this->properties[$parentClass->getName()][$propertyName])) {
return $parentClass;
}
}
return null;
}
}
src/Reflection/PhpDefect/PhpDefectPropertyReflection.php 0000666 00000001714 13436751442 0017442 0 ustar 00 declaringClass = $declaringClass;
$this->type = $type;
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
public function isStatic(): bool
{
return false;
}
public function isPrivate(): bool
{
return false;
}
public function isPublic(): bool
{
return true;
}
public function getType(): Type
{
return $this->type;
}
public function isReadable(): bool
{
return true;
}
public function isWritable(): bool
{
return true;
}
}
src/Reflection/MissingPropertyFromReflectionException.php 0000666 00000000647 13436751442 0020056 0 ustar 00 reflection = $reflection;
$this->parser = $parser;
$this->functionCallStatementFinder = $functionCallStatementFinder;
$this->cache = $cache;
$this->phpDocParameterTypes = $phpDocParameterTypes;
$this->phpDocReturnType = $phpDocReturnType;
}
public function getNativeReflection(): \ReflectionFunction
{
return $this->reflection;
}
public function getName(): string
{
return $this->reflection->getName();
}
/**
* @return \PHPStan\Reflection\ParameterReflection[]
*/
public function getParameters(): array
{
if ($this->parameters === null) {
$this->parameters = array_map(function (\ReflectionParameter $reflection) {
return new PhpParameterReflection(
$reflection,
isset($this->phpDocParameterTypes[$reflection->getName()]) ? $this->phpDocParameterTypes[$reflection->getName()] : null
);
}, $this->reflection->getParameters());
if (
$this->reflection->getName() === 'array_unique'
&& count($this->parameters) === 1
) {
// PHP bug #70960
$this->parameters[] = new DummyParameter(
'sort_flags',
new IntegerType(),
true
);
}
if (
$this->reflection->getName() === 'fputcsv'
&& count($this->parameters) === 4
) {
$this->parameters[] = new DummyParameter(
'escape_char',
new StringType(),
true
);
}
if (
$this->reflection->getName() === 'unpack'
&& PHP_VERSION_ID >= 70101
) {
$this->parameters[2] = new DummyParameter(
'offset',
new IntegerType(),
true
);
}
if (
$this->reflection->getName() === 'imagepng'
&& count($this->parameters) === 2
) {
$this->parameters[] = new DummyParameter(
'quality',
new IntegerType(),
true
);
$this->parameters[] = new DummyParameter(
'filters',
new IntegerType(),
true
);
}
if (
$this->reflection->getName() === 'session_start'
&& count($this->parameters) === 0
) {
$this->parameters[] = new DummyParameter(
'options',
new ArrayType(new MixedType(), new MixedType()),
true
);
}
if ($this->reflection->getName() === 'locale_get_display_language') {
$this->parameters[1] = new DummyParameter(
'in_locale',
new StringType(),
true
);
}
if (
$this->reflection->getName() === 'imagewebp'
&& count($this->parameters) === 2
) {
$this->parameters[] = new DummyParameter(
'quality',
new IntegerType(),
true
);
}
if (
$this->reflection->getName() === 'setproctitle'
&& count($this->parameters) === 0
) {
$this->parameters[] = new DummyParameter(
'title',
new StringType(),
false
);
}
if (
$this->reflection->getName() === 'get_class'
) {
$this->parameters = [
new DummyParameter(
'object',
new ObjectWithoutClassType(),
true
),
];
}
if (
$this->reflection->getName() === 'mysqli_fetch_all'
&& count($this->parameters) === 1
) {
$this->parameters[] = new DummyParameter(
'resulttype',
new IntegerType(),
true
);
}
if (
$this->reflection->getName() === 'openssl_open'
&& count($this->parameters) === 5
) {
$this->parameters[4] = new DummyParameter(
'method',
new StringType(),
true
);
$this->parameters[5] = new DummyParameter(
'iv',
new StringType(),
true
);
}
if (
$this->reflection->getName() === 'openssl_x509_parse'
) {
$this->parameters[1] = new DummyParameter(
'shortnames',
new TrueOrFalseBooleanType(),
true
);
}
}
return $this->parameters;
}
public function isVariadic(): bool
{
$isNativelyVariadic = $this->reflection->isVariadic();
if (!$isNativelyVariadic && $this->reflection->getFileName() !== false) {
$key = sprintf('variadic-function-%s-v0', $this->reflection->getName());
$cachedResult = $this->cache->load($key);
if ($cachedResult === null) {
$nodes = $this->parser->parseFile($this->reflection->getFileName());
$result = $this->callsFuncGetArgs($nodes);
$this->cache->save($key, $result);
return $result;
}
return $cachedResult;
}
return $isNativelyVariadic;
}
/**
* @param mixed $nodes
* @return bool
*/
private function callsFuncGetArgs($nodes): bool
{
foreach ($nodes as $node) {
if (is_array($node)) {
if ($this->callsFuncGetArgs($node)) {
return true;
}
}
if (!($node instanceof \PhpParser\Node)) {
continue;
}
if ($node instanceof Function_) {
$functionName = (string) $node->namespacedName;
if ($functionName === $this->reflection->getName()) {
return $this->functionCallStatementFinder->findFunctionCallInStatements(self::VARIADIC_FUNCTIONS, $node->getStmts()) !== null;
}
continue;
}
if ($this->callsFuncGetArgs($node)) {
return true;
}
}
return false;
}
public function getReturnType(): Type
{
if ($this->returnType === null) {
if ($this->reflection->getName() === 'count') {
return $this->returnType = new IntegerType();
}
$returnType = $this->reflection->getReturnType();
$phpDocReturnType = $this->phpDocReturnType;
if (
$returnType !== null
&& $phpDocReturnType !== null
&& $returnType->allowsNull() !== TypeCombinator::containsNull($phpDocReturnType)
) {
$phpDocReturnType = null;
}
$this->returnType = TypehintHelper::decideTypeFromReflection(
$returnType,
$phpDocReturnType
);
}
return $this->returnType;
}
public function getPhpDocReturnType(): Type
{
if ($this->phpDocReturnType !== null) {
return $this->phpDocReturnType;
}
return new MixedType();
}
public function getNativeReturnType(): Type
{
if ($this->nativeReturnType === null) {
$this->nativeReturnType = TypehintHelper::decideTypeFromReflection($this->reflection->getReturnType());
}
return $this->nativeReturnType;
}
}
src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php 0000666 00000005211 13436751442 0023717 0 ustar 00 fileTypeMapper = $fileTypeMapper;
}
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
if (!isset($this->properties[$classReflection->getName()])) {
$this->properties[$classReflection->getName()] = $this->createProperties($classReflection, $classReflection);
}
return isset($this->properties[$classReflection->getName()][$propertyName]);
}
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
{
return $this->properties[$classReflection->getName()][$propertyName];
}
/**
* @param \PHPStan\Reflection\ClassReflection $classReflection
* @param \PHPStan\Reflection\ClassReflection $declaringClass
* @return \PHPStan\Reflection\PropertyReflection[]
*/
private function createProperties(
ClassReflection $classReflection,
ClassReflection $declaringClass
): array
{
$properties = [];
foreach ($classReflection->getTraits() as $traitClass) {
$properties += $this->createProperties($traitClass, $classReflection);
}
foreach ($classReflection->getParents() as $parentClass) {
$properties += $this->createProperties($parentClass, $parentClass);
foreach ($parentClass->getTraits() as $traitClass) {
$properties += $this->createProperties($traitClass, $parentClass);
}
}
foreach ($classReflection->getInterfaces() as $interfaceClass) {
$properties += $this->createProperties($interfaceClass, $interfaceClass);
}
$fileName = $classReflection->getFileName();
if ($fileName === false) {
return $properties;
}
$docComment = $classReflection->getNativeReflection()->getDocComment();
if ($docComment === false) {
return $properties;
}
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $classReflection->getName(), $docComment);
foreach ($resolvedPhpDoc->getPropertyTags() as $propertyName => $propertyTag) {
$properties[$propertyName] = new AnnotationPropertyReflection(
$declaringClass,
$propertyTag->getType(),
$propertyTag->isReadable(),
$propertyTag->isWritable()
);
}
return $properties;
}
}
src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php 0000666 00000006325 13436751442 0023175 0 ustar 00 fileTypeMapper = $fileTypeMapper;
}
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
if (!isset($this->methods[$classReflection->getName()])) {
$this->methods[$classReflection->getName()] = $this->createMethods($classReflection, $classReflection);
}
return isset($this->methods[$classReflection->getName()][$methodName]);
}
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
{
return $this->methods[$classReflection->getName()][$methodName];
}
/**
* @param ClassReflection $classReflection
* @param ClassReflection $declaringClass
* @return MethodReflection[]
*/
private function createMethods(
ClassReflection $classReflection,
ClassReflection $declaringClass
): array
{
$methods = [];
foreach ($classReflection->getTraits() as $traitClass) {
$methods += $this->createMethods($traitClass, $classReflection);
}
foreach ($classReflection->getParents() as $parentClass) {
$methods += $this->createMethods($parentClass, $parentClass);
foreach ($parentClass->getTraits() as $traitClass) {
$methods += $this->createMethods($traitClass, $parentClass);
}
}
foreach ($classReflection->getInterfaces() as $interfaceClass) {
$methods += $this->createMethods($interfaceClass, $interfaceClass);
}
$fileName = $classReflection->getFileName();
if ($fileName === false) {
return $methods;
}
$docComment = $classReflection->getNativeReflection()->getDocComment();
if ($docComment === false) {
return $methods;
}
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $classReflection->getName(), $docComment);
foreach ($resolvedPhpDoc->getMethodTags() as $methodName => $methodTag) {
$parameters = [];
foreach ($methodTag->getParameters() as $parameterName => $parameterTag) {
$parameters[] = new AnnotationsMethodParameterReflection(
$parameterName,
$parameterTag->getType(),
$parameterTag->isPassedByReference(),
$parameterTag->isOptional(),
$parameterTag->isVariadic()
);
}
$methods[$methodName] = new AnnotationMethodReflection(
$methodName,
$declaringClass,
$methodTag->getReturnType(),
$parameters,
$methodTag->isStatic(),
$this->detectMethodVariadic($parameters)
);
}
return $methods;
}
/**
* @param AnnotationsMethodParameterReflection[] $parameters
* @return bool
*/
private function detectMethodVariadic(array $parameters): bool
{
if ($parameters === []) {
return false;
}
$possibleVariadicParameterIndex = count($parameters) - 1;
$possibleVariadicParameter = $parameters[$possibleVariadicParameterIndex];
return $possibleVariadicParameter->isVariadic();
}
}
src/Reflection/Annotations/AnnotationMethodReflection.php 0000666 00000003051 13436751442 0017735 0 ustar 00 name = $name;
$this->declaringClass = $declaringClass;
$this->returnType = $returnType;
$this->parameters = $parameters;
$this->isStatic = $isStatic;
$this->isVariadic = $isVariadic;
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
public function getPrototype(): MethodReflection
{
return $this;
}
public function isStatic(): bool
{
return $this->isStatic;
}
public function getParameters(): array
{
return $this->parameters;
}
public function isVariadic(): bool
{
return $this->isVariadic;
}
public function isPrivate(): bool
{
return false;
}
public function isPublic(): bool
{
return true;
}
public function getName(): string
{
return $this->name;
}
public function getReturnType(): Type
{
return $this->returnType;
}
}
src/Reflection/Annotations/AnnotationsMethodParameterReflection.php 0000666 00000002067 13436751442 0021767 0 ustar 00 name = $name;
$this->type = $type;
$this->isPassedByReference = $isPassedByReference;
$this->isOptional = $isOptional;
$this->isVariadic = $isVariadic;
}
public function getName(): string
{
return $this->name;
}
public function isOptional(): bool
{
return $this->isOptional;
}
public function getType(): Type
{
return $this->type;
}
public function isPassedByReference(): bool
{
return $this->isPassedByReference;
}
public function isVariadic(): bool
{
return $this->isVariadic;
}
}
src/Reflection/Annotations/AnnotationPropertyReflection.php 0000666 00000002243 13436751442 0020343 0 ustar 00 declaringClass = $declaringClass;
$this->type = $type;
$this->readable = $readable;
$this->writable = $writable;
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
public function isStatic(): bool
{
return false;
}
public function isPrivate(): bool
{
return false;
}
public function isPublic(): bool
{
return true;
}
public function getType(): Type
{
return $this->type;
}
public function isReadable(): bool
{
return $this->readable;
}
public function isWritable(): bool
{
return $this->writable;
}
}
src/Reflection/ObsoleteClassConstantReflection.php 0000666 00000001772 13436751442 0016451 0 ustar 00 declaringClass = $declaringClass;
$this->name = $name;
$this->value = $value;
}
public function getName(): string
{
return $this->name;
}
/**
* @return mixed
*/
public function getValue()
{
return $this->value;
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
public function isStatic(): bool
{
return true;
}
public function isPrivate(): bool
{
return false;
}
public function isPublic(): bool
{
return true;
}
}
src/Parser/CachedParser.php 0000666 00000002017 13436751442 0011641 0 ustar 00 originalParser = $originalParser;
}
/**
* @param string $file path to a file to parse
* @return \PhpParser\Node[]
*/
public function parseFile(string $file): array
{
if (!isset($this->cachedNodesByFile[$file])) {
$this->cachedNodesByFile[$file] = $this->originalParser->parseFile($file);
}
return $this->cachedNodesByFile[$file];
}
/**
* @param string $sourceCode
* @return \PhpParser\Node[]
*/
public function parseString(string $sourceCode): array
{
if (!isset($this->cachedNodesByString[$sourceCode])) {
$this->cachedNodesByString[$sourceCode] = $this->originalParser->parseString($sourceCode);
}
return $this->cachedNodesByString[$sourceCode];
}
}
src/Parser/Parser.php 0000666 00000000535 13436751442 0010554 0 ustar 00 parser = $parser;
$this->traverser = $traverser;
}
/**
* @param string $file path to a file to parse
* @return \PhpParser\Node[]
*/
public function parseFile(string $file): array
{
return $this->parseString(file_get_contents($file));
}
/**
* @param string $sourceCode
* @return \PhpParser\Node[]
*/
public function parseString(string $sourceCode): array
{
$nodes = $this->parser->parse($sourceCode);
if ($nodes === null) {
throw new \PHPStan\ShouldNotHappenException();
}
return $this->traverser->traverse($nodes);
}
}
src/Parser/FunctionCallStatementFinder.php 0000666 00000001711 13436751442 0014713 0 ustar 00 findFunctionCallInStatements($functionNames, $statement);
if ($result !== null) {
return $result;
}
}
if (!($statement instanceof \PhpParser\Node)) {
continue;
}
if ($statement instanceof FuncCall && $statement->name instanceof Name) {
if (in_array((string) $statement->name, $functionNames, true)) {
return $statement;
}
}
$result = $this->findFunctionCallInStatements($functionNames, $statement);
if ($result !== null) {
return $result;
}
}
return null;
}
}
src/Command/AnalyseCommand.php 0000666 00000020502 13436751442 0012331 0 ustar 00 setName(self::NAME)
->setDescription('Analyses source code')
->setDefinition([
new InputArgument('paths', InputArgument::REQUIRED | InputArgument::IS_ARRAY, 'Paths with source code to run analysis on'),
new InputOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to project configuration file'),
new InputOption(self::OPTION_LEVEL, 'l', InputOption::VALUE_REQUIRED, 'Level of rule options - the higher the stricter'),
new InputOption(ErrorsConsoleStyle::OPTION_NO_PROGRESS, null, InputOption::VALUE_NONE, 'Do not show progress bar, only results'),
new InputOption('debug', null, InputOption::VALUE_NONE, 'Show debug information - which file is analysed, do not catch internal errors'),
new InputOption('autoload-file', 'a', InputOption::VALUE_REQUIRED, 'Project\'s additional autoload file path'),
new InputOption('errorFormat', null, InputOption::VALUE_REQUIRED, 'Format in which to print the result of the analysis', 'table'),
new InputOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for analysis'),
]);
}
public function getAliases(): array
{
return ['analyze'];
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$consoleStyle = new ErrorsConsoleStyle($input, $output);
$memoryLimit = $input->getOption('memory-limit');
if ($memoryLimit !== null) {
if (!preg_match('#^-?\d+[kMG]?$#i', $memoryLimit)) {
$consoleStyle->error(sprintf('Invalid memory limit format "%s".', $memoryLimit));
return 1;
}
if (ini_set('memory_limit', $memoryLimit) === false) {
$consoleStyle->error(sprintf('Memory limit "%s" cannot be set.', $memoryLimit));
return 1;
}
}
$currentWorkingDirectory = getcwd();
$fileHelper = new FileHelper($currentWorkingDirectory);
$autoloadFile = $input->getOption('autoload-file');
if ($autoloadFile !== null && is_file($autoloadFile)) {
$autoloadFile = $fileHelper->normalizePath($autoloadFile);
if (is_file($autoloadFile)) {
require_once $autoloadFile;
}
}
$projectConfigFile = $input->getOption('configuration');
$levelOption = $input->getOption(self::OPTION_LEVEL);
$defaultLevelUsed = false;
if ($projectConfigFile === null && $levelOption === null) {
$levelOption = self::DEFAULT_LEVEL;
$defaultLevelUsed = true;
}
$containerFactory = new ContainerFactory($currentWorkingDirectory);
$additionalConfigFiles = [];
if ($levelOption !== null) {
$levelConfigFile = sprintf('%s/config.level%s.neon', $containerFactory->getConfigDirectory(), $levelOption);
if (!is_file($levelConfigFile)) {
$output->writeln(sprintf('Level config file %s was not found.', $levelConfigFile));
return 1;
}
$additionalConfigFiles[] = $levelConfigFile;
}
if ($projectConfigFile !== null) {
if (!is_file($projectConfigFile)) {
$output->writeln(sprintf('Project config file at path %s does not exist.', $projectConfigFile));
return 1;
}
$additionalConfigFiles[] = $projectConfigFile;
$loader = new Loader();
$projectConfig = $loader->load($projectConfigFile, null);
if (isset($projectConfig['parameters']['tmpDir'])) {
$tmpDir = Helpers::expand($projectConfig['parameters']['tmpDir'], [
'rootDir' => $containerFactory->getRootDirectory(),
'currentWorkingDirectory' => $containerFactory->getCurrentWorkingDirectory(),
]);
}
}
if (!isset($tmpDir)) {
$tmpDir = sys_get_temp_dir() . '/phpstan';
if (!@mkdir($tmpDir, 0777, true) && !is_dir($tmpDir)) {
$consoleStyle->error(sprintf('Cannot create a temp directory %s', $tmpDir));
return 1;
}
}
$container = $containerFactory->create($tmpDir, $additionalConfigFiles);
$memoryLimitFile = $container->parameters['memoryLimitFile'];
if (file_exists($memoryLimitFile)) {
$consoleStyle->note(sprintf(
"PHPStan crashed in the previous run probably because of excessive memory consumption.\nIt consumed around %s of memory.\n\nTo avoid this issue, allow to use more memory with the --memory-limit option.",
file_get_contents($memoryLimitFile)
));
unlink($memoryLimitFile);
}
if (PHP_VERSION_ID >= 70100 && !property_exists(Catch_::class, 'types')) {
$consoleStyle->note(
'You\'re running PHP >= 7.1, but you still have PHP-Parser version 2.x. This will lead to parse errors in case you use PHP 7.1 syntax like nullable parameters, iterable and void typehints, union exception types, or class constant visibility. Update to PHP-Parser 3.x to dismiss this message.'
);
}
$errorFormat = $input->getOption('errorFormat');
$errorFormatterServiceName = sprintf('errorFormatter.%s', $errorFormat);
if (!$container->hasService($errorFormatterServiceName)) {
$consoleStyle->error(sprintf(
'Error formatter "%s" not found. Available error formatters are: %s',
$errorFormat,
implode(', ', array_map(function (string $name) {
return substr($name, strlen('errorFormatter.'));
}, $container->findByType(ErrorFormatter::class)))
));
return 1;
}
/** @var ErrorFormatter $errorFormatter */
$errorFormatter = $container->getService($errorFormatterServiceName);
$this->setUpSignalHandler($consoleStyle, $memoryLimitFile);
if (!isset($container->parameters['customRulesetUsed'])) {
$output->writeln('');
$output->writeln('No rules detected');
$output->writeln('');
$output->writeln('You have the following choices:');
$output->writeln('');
$output->writeln('* while running the analyse option, use the --level option to adjust your rule level - the higher the stricter');
$output->writeln('');
$output->writeln(sprintf('* create your own custom ruleset by selecting which rules you want to check by copying the service definitions from the built-in config level files in %s>.', $fileHelper->normalizePath(__DIR__ . '/../../conf')));
$output->writeln(' * in this case, don\'t forget to define parameter customRulesetUsed> in your config file.');
$output->writeln('');
return 1;
} elseif ($container->parameters['customRulesetUsed']) {
$defaultLevelUsed = false;
}
foreach ($container->parameters['autoload_files'] as $autoloadFile) {
require_once $fileHelper->normalizePath($autoloadFile);
}
if (count($container->parameters['autoload_directories']) > 0) {
$robotLoader = new \Nette\Loaders\RobotLoader();
$robotLoader->acceptFiles = array_map(function (string $extension): string {
return sprintf('*.%s', $extension);
}, $container->parameters['fileExtensions']);
$robotLoader->setTempDirectory($tmpDir);
foreach ($container->parameters['autoload_directories'] as $directory) {
$robotLoader->addDirectory($fileHelper->normalizePath($directory));
}
$robotLoader->register();
}
TypeCombinator::setUnionTypesEnabled($container->parameters['checkUnionTypes']);
/** @var \PHPStan\Command\AnalyseApplication $application */
$application = $container->getByType(AnalyseApplication::class);
return $this->handleReturn(
$application->analyse(
$input->getArgument('paths'),
$consoleStyle,
$errorFormatter,
$defaultLevelUsed,
$input->getOption('debug')
),
$memoryLimitFile
);
}
private function handleReturn(int $code, string $memoryLimitFile): int
{
unlink($memoryLimitFile);
return $code;
}
private function setUpSignalHandler(StyleInterface $consoleStyle, string $memoryLimitFile)
{
if (function_exists('pcntl_signal')) {
pcntl_signal(SIGINT, function () use ($consoleStyle, $memoryLimitFile) {
if (file_exists($memoryLimitFile)) {
unlink($memoryLimitFile);
}
$consoleStyle->newLine();
exit(1);
});
}
}
}
src/Command/ErrorFormatter/TableErrorFormatter.php 0000666 00000004014 13436751442 0016340 0 ustar 00 hasErrors()) {
$style->success('No errors');
if ($analysisResult->isDefaultLevelUsed()) {
$style->note(sprintf(
'PHPStan is performing only the most basic checks. You can pass a higher rule level through the --%s option (the default and current level is %d) to analyse code more thoroughly.',
AnalyseCommand::OPTION_LEVEL,
AnalyseCommand::DEFAULT_LEVEL
));
}
return 0;
}
$currentDirectory = $analysisResult->getCurrentDirectory();
$cropFilename = function (string $filename) use ($currentDirectory): string {
if ($currentDirectory !== '' && strpos($filename, $currentDirectory) === 0) {
return substr($filename, strlen($currentDirectory) + 1);
}
return $filename;
};
/** @var \PHPStan\Analyser\Error[][] $fileErrors */
$fileErrors = [];
foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) {
if (!isset($fileErrors[$fileSpecificError->getFile()])) {
$fileErrors[$fileSpecificError->getFile()] = [];
}
$fileErrors[$fileSpecificError->getFile()][] = $fileSpecificError;
}
foreach ($fileErrors as $file => $errors) {
$rows = [];
foreach ($errors as $error) {
$rows[] = [
(string) $error->getLine(),
$error->getMessage(),
];
}
$style->table(['Line', $cropFilename($file)], $rows);
}
if (count($analysisResult->getNotFileSpecificErrors()) > 0) {
$style->table(['Error'], array_map(function (string $error): array {
return [$error];
}, $analysisResult->getNotFileSpecificErrors()));
}
$style->error(sprintf($analysisResult->getTotalErrorsCount() === 1 ? 'Found %d error' : 'Found %d errors', $analysisResult->getTotalErrorsCount()));
return 1;
}
}
src/Command/ErrorFormatter/CheckstyleErrorFormatter.php 0000666 00000003127 13436751442 0017413 0 ustar 00 hasErrors()) {
$returnCode = 0;
}
$out = '';
/** @var \PHPStan\Analyser\Error $fileSpecificError */
foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) {
$out .= '' . "\n";
$out .= ' ';
$out .= 'escape((string) $fileSpecificError->getLine()) . '"';
$out .= ' column="1"';
$out .= ' severity="error"';
$out .= ' message="' . $this->escape($fileSpecificError->getMessage()) . '"';
$out .= '/>' . "\n";
$out .= '' . "\n";
}
$style->write('' . "\n");
$style->write('' . "\n");
if ($out !== '') {
$style->write($out);
}
$style->write('' . "\n");
return $returnCode;
}
/**
* Escapes values for using in XML
*
* @param string $string
* @return string
*/
protected function escape(string $string): string
{
return htmlspecialchars($string, ENT_XML1 | ENT_COMPAT, 'UTF-8');
}
}
src/Command/ErrorFormatter/RawErrorFormatter.php 0000666 00000001467 13436751442 0016053 0 ustar 00 hasErrors()) {
return 0;
}
foreach ($analysisResult->getNotFileSpecificErrors() as $notFileSpecificError) {
$style->writeln(sprintf('?:?:%s', $notFileSpecificError));
}
foreach ($analysisResult->getFileSpecificErrors() as $fileSpecificError) {
$style->writeln(
sprintf(
'%s:%d:%s',
$fileSpecificError->getFile(),
$fileSpecificError->getLine() !== null ? $fileSpecificError->getLine() : '?',
$fileSpecificError->getMessage()
)
);
}
return 1;
}
}
src/Command/ErrorFormatter/ErrorFormatter.php 0000666 00000000755 13436751442 0015400 0 ustar 00 showProgress = $input->hasOption(self::OPTION_NO_PROGRESS) && !((bool) $input->getOption(self::OPTION_NO_PROGRESS));
$this->output = $output;
}
public function table(array $headers, array $rows)
{
$terminalWidth = (new \Symfony\Component\Console\Terminal())->getWidth();
$maxHeaderWidth = strlen($headers[0]);
foreach ($rows as $row) {
$length = strlen($row[0]);
if ($maxHeaderWidth === 0 || $length > $maxHeaderWidth) {
$maxHeaderWidth = $length;
}
}
$wrap = function ($rows) use ($terminalWidth, $maxHeaderWidth) {
return array_map(function ($row) use ($terminalWidth, $maxHeaderWidth) {
return array_map(function ($s) use ($terminalWidth, $maxHeaderWidth) {
if ($terminalWidth > $maxHeaderWidth + 5) {
return wordwrap(
$s,
$terminalWidth - $maxHeaderWidth - 5,
"\n",
true
);
}
return $s;
}, $row);
}, $rows);
};
parent::table($headers, $wrap($rows));
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
* @param int $max
*/
public function createProgressBar($max = 0): ProgressBar
{
$this->progressBar = parent::createProgressBar($max);
return $this->progressBar;
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
* @param int $max
*/
public function progressStart($max = 0)
{
if (!$this->showProgress) {
return;
}
parent::progressStart($max);
}
/**
* @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
* @param int $step
*/
public function progressAdvance($step = 1)
{
if (!$this->showProgress) {
return;
}
if ($this->output->isDecorated() && $step > 0) {
$stepTime = (time() - $this->progressBar->getStartTime()) / $step;
if ($stepTime > 0 && $stepTime < 1) {
$this->progressBar->setRedrawFrequency(1 / $stepTime);
} else {
$this->progressBar->setRedrawFrequency(1);
}
}
$this->progressBar->setProgress($this->progressBar->getProgress() + $step);
}
public function progressFinish()
{
if (!$this->showProgress) {
return;
}
parent::progressFinish();
}
}
src/Command/AnalysisResult.php 0000666 00000002767 13436751442 0012435 0 ustar 00 fileSpecificErrors = $fileSpecificErrors;
$this->notFileSpecificErrors = $notFileSpecificErrors;
$this->defaultLevelUsed = $defaultLevelUsed;
$this->currentDirectory = $currentDirectory;
}
public function hasErrors(): bool
{
return $this->getTotalErrorsCount() > 0;
}
public function getTotalErrorsCount(): int
{
return count($this->fileSpecificErrors) + count($this->notFileSpecificErrors);
}
/**
* @return \PHPStan\Analyser\Error[]
*/
public function getFileSpecificErrors(): array
{
return $this->fileSpecificErrors;
}
/**
* @return string[]
*/
public function getNotFileSpecificErrors(): array
{
return $this->notFileSpecificErrors;
}
public function isDefaultLevelUsed(): bool
{
return $this->defaultLevelUsed;
}
public function getCurrentDirectory(): string
{
return $this->currentDirectory;
}
}
src/Command/AnalyseApplication.php 0000666 00000007574 13436751442 0013234 0 ustar 00 analyser = $analyser;
$this->memoryLimitFile = $memoryLimitFile;
$this->fileExtensions = $fileExtensions;
$this->fileHelper = $fileHelper;
$this->fileExcluder = $fileExcluder;
}
/**
* @param string[] $paths
* @param \Symfony\Component\Console\Style\OutputStyle $style
* @param \PHPStan\Command\ErrorFormatter\ErrorFormatter $errorFormatter
* @param bool $defaultLevelUsed
* @param bool $debug
* @return int Error code.
*/
public function analyse(
array $paths,
OutputStyle $style,
ErrorFormatter $errorFormatter,
bool $defaultLevelUsed,
bool $debug
): int
{
$errors = [];
$files = [];
$this->updateMemoryLimitFile();
$paths = array_map(function (string $path): string {
return $this->fileHelper->absolutizePath($path);
}, $paths);
$onlyFiles = true;
foreach ($paths as $path) {
if (!file_exists($path)) {
$errors[] = new Error(sprintf('Path %s does not exist', $path), $path, null, false);
} elseif (is_file($path)) {
$files[] = $this->fileHelper->normalizePath($path);
} else {
$finder = new Finder();
$finder->followLinks();
foreach ($finder->files()->name('*.{' . implode(',', $this->fileExtensions) . '}')->in($path) as $fileInfo) {
$files[] = $this->fileHelper->normalizePath($fileInfo->getPathname());
$onlyFiles = false;
}
}
}
$files = array_filter($files, function (string $file): bool {
return !$this->fileExcluder->isExcludedFromAnalysing($file);
});
$this->updateMemoryLimitFile();
if (!$debug) {
$progressStarted = false;
$fileOrder = 0;
$preFileCallback = null;
$postFileCallback = function () use ($style, &$progressStarted, $files, &$fileOrder) {
if (!$progressStarted) {
$style->progressStart(count($files));
$progressStarted = true;
}
$style->progressAdvance();
if ($fileOrder % 100 === 0) {
$this->updateMemoryLimitFile();
}
$fileOrder++;
};
} else {
$preFileCallback = function (string $file) use ($style) {
$style->writeln($file);
};
$postFileCallback = null;
}
$errors = array_merge($errors, $this->analyser->analyse(
$files,
$onlyFiles,
$preFileCallback,
$postFileCallback,
$debug
));
if (isset($progressStarted) && $progressStarted) {
$style->progressFinish();
}
$fileSpecificErrors = [];
$notFileSpecificErrors = [];
foreach ($errors as $error) {
if (is_string($error)) {
$notFileSpecificErrors[] = $error;
} elseif ($error instanceof Error) {
$fileSpecificErrors[] = $error;
} else {
throw new \PHPStan\ShouldNotHappenException();
}
}
return $errorFormatter->formatErrors(
new AnalysisResult(
$fileSpecificErrors,
$notFileSpecificErrors,
$defaultLevelUsed,
$this->fileHelper->normalizePath(dirname($paths[0]))
),
$style
);
}
private function updateMemoryLimitFile()
{
$bytes = memory_get_peak_usage(true);
$megabytes = ceil($bytes / 1024 / 1024);
file_put_contents($this->memoryLimitFile, sprintf('%d MB', $megabytes));
if (function_exists('pcntl_signal_dispatch')) {
pcntl_signal_dispatch();
}
}
}
src/Rules/UnusedFunctionParametersCheck.php 0000666 00000003621 13436751442 0015110 0 ustar 00 getUsedVariables($statements) as $variableName) {
if (isset($unusedParameters[$variableName])) {
unset($unusedParameters[$variableName]);
}
}
$errors = [];
foreach ($unusedParameters as $name => $bool) {
$errors[] = sprintf($unusedParameterMessage, $name);
}
return $errors;
}
/**
* @param \PhpParser\Node[]|\PhpParser\Node $node
* @return string[]
*/
private function getUsedVariables($node): array
{
$variableNames = [];
if ($node instanceof Node) {
if ($node instanceof Node\Expr\Variable && is_string($node->name) && $node->name !== 'this') {
return [$node->name];
}
if ($node instanceof Node\Expr\ClosureUse) {
return [$node->var];
}
if (
$node instanceof Node\Expr\FuncCall
&& $node->name instanceof Node\Name
&& (string) $node->name === 'compact'
) {
foreach ($node->args as $arg) {
if ($arg->value instanceof Node\Scalar\String_) {
$variableNames[] = $arg->value->value;
}
}
}
foreach ($node->getSubNodeNames() as $subNodeName) {
if ($node instanceof Node\Expr\Closure && $subNodeName !== 'uses') {
continue;
}
$subNode = $node->{$subNodeName};
$variableNames = array_merge($variableNames, $this->getUsedVariables($subNode));
}
} elseif (is_array($node)) {
foreach ($node as $subNode) {
$variableNames = array_merge($variableNames, $this->getUsedVariables($subNode));
}
}
return $variableNames;
}
}
src/Rules/Cast/UselessCastRule.php 0000666 00000002114 13436751442 0013131 0 ustar 00 getType($node->expr);
if ($expressionType instanceof UnionType) {
return [];
}
$castType = $scope->getType($node);
if ($castType instanceof FloatType && $node->expr instanceof Node\Expr\BinaryOp\Div) {
return [];
}
if (get_class($expressionType) === get_class($castType)) {
return [
sprintf(
'Casting to %s something that\'s already %s.',
$castType->describe(),
$expressionType->describe()
),
];
}
return [];
}
}
src/Rules/FunctionReturnTypeCheck.php 0000666 00000004753 13436751442 0013751 0 ustar 00 printer = $printer;
$this->ruleLevelHelper = $ruleLevelHelper;
}
/**
* @param \PHPStan\Analyser\Scope $scope
* @param \PHPStan\Type\Type $returnType
* @param \PhpParser\Node\Expr|null $returnValue
* @param string $emptyReturnStatementMessage
* @param string $voidMessage
* @param string $typeMismatchMessage
* @param bool $isGenerator
* @param bool $isAnonymousFunction
* @return string[]
*/
public function checkReturnType(
Scope $scope,
Type $returnType,
Expr $returnValue = null,
string $emptyReturnStatementMessage,
string $voidMessage,
string $typeMismatchMessage,
bool $isGenerator,
bool $isAnonymousFunction = false
): array
{
if ($isGenerator) {
return [];
}
if ($returnValue === null) {
if (
$returnType instanceof VoidType
|| $returnType instanceof MixedType
) {
return [];
}
return [
sprintf(
$emptyReturnStatementMessage,
$returnType->describe()
),
];
}
$returnValueType = $scope->getType($returnValue);
if (
TypeCombinator::removeNull($returnType) instanceof ThisType
&& !$returnValueType instanceof ThisType
) {
if (TypeCombinator::containsNull($returnType) && $returnValueType instanceof \PHPStan\Type\NullType) {
return [];
}
if (
$returnValue instanceof Expr\Variable
&& is_string($returnValue->name)
&& $returnValue->name === 'this'
) {
return [];
}
return [
sprintf(
$typeMismatchMessage,
'$this',
$this->printer->prettyPrintExpr($returnValue)
),
];
}
if ($returnType instanceof VoidType) {
return [
sprintf(
$voidMessage,
$returnValueType->describe()
),
];
}
if (!$this->ruleLevelHelper->accepts($returnType, $returnValueType) && (!$isAnonymousFunction || $returnValueType->isDocumentableNatively())) {
return [
sprintf(
$typeMismatchMessage,
$returnType->describe(),
$returnValueType->describe()
),
];
}
return [];
}
}
src/Rules/Exceptions/CaughtExceptionExistenceRule.php 0000666 00000002513 13436751442 0017067 0 ustar 00 broker = $broker;
}
public function getNodeType(): string
{
return Catch_::class;
}
/**
* @param \PhpParser\Node\Stmt\Catch_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (isset($node->types)) {
$classes = $node->types;
} elseif (isset($node->type)) {
$classes = [$node->type];
} else {
throw new \PHPStan\ShouldNotHappenException();
}
$errors = [];
foreach ($classes as $className) {
$class = (string) $className;
if (!$this->broker->hasClass($class)) {
$errors[] = sprintf('Caught class %s not found.', $class);
continue;
}
$classReflection = $this->broker->getClass($class);
if (!$classReflection->isInterface() && !$classReflection->getNativeReflection()->implementsInterface(\Throwable::class)) {
$errors[] = sprintf('Caught class %s is not an exception.', $classReflection->getDisplayName());
}
}
return $errors;
}
}
src/Rules/Classes/ExistingClassInTraitUseRule.php 0000666 00000001654 13436751442 0016136 0 ustar 00 classCaseSensitivityCheck = $classCaseSensitivityCheck;
}
public function getNodeType(): string
{
return \PhpParser\Node\Stmt\TraitUse::class;
}
/**
* @param \PhpParser\Node\Stmt\TraitUse $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
return $this->classCaseSensitivityCheck->checkClassNames(
array_map(function (Node\Name $traitName): string {
return (string) $traitName;
}, $node->traits)
);
}
}
src/Rules/Classes/InstantiationRule.php 0000666 00000011132 13436751442 0014222 0 ustar 00 broker = $broker;
$this->check = $check;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
}
public function getNodeType(): string
{
return New_::class;
}
/**
* @param \PhpParser\Node\Expr\New_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (!($node->class instanceof \PhpParser\Node\Name)) {
return [];
}
$class = (string) $node->class;
$messages = [];
if ($class === 'static') {
if (!$scope->isInClass()) {
return [
sprintf('Using %s outside of class scope.', $class),
];
}
return [];
} elseif ($class === 'self') {
if (!$scope->isInClass()) {
return [
sprintf('Using %s outside of class scope.', $class),
];
}
$classReflection = $scope->getClassReflection();
} elseif ($class === 'parent') {
if (!$scope->isInClass()) {
return [
sprintf('Using %s outside of class scope.', $class),
];
}
if ($scope->getClassReflection()->getParentClass() === false) {
return [
sprintf(
'%s::%s() calls new parent but %s does not extend any class.',
$scope->getClassReflection()->getDisplayName(),
$scope->getFunctionName(),
$scope->getClassReflection()->getDisplayName()
),
];
}
$classReflection = $scope->getClassReflection()->getParentClass();
} else {
if (!$this->broker->hasClass($class)) {
return [
sprintf('Instantiated class %s not found.', $class),
];
} else {
$messages = $this->classCaseSensitivityCheck->checkClassNames([$class]);
}
$classReflection = $this->broker->getClass($class);
}
if ($classReflection->isInterface()) {
return [
sprintf('Cannot instantiate interface %s.', $classReflection->getDisplayName()),
];
}
if ($classReflection->isAbstract()) {
return [
sprintf('Instantiated class %s is abstract.', $classReflection->getDisplayName()),
];
}
if (!$classReflection->hasNativeMethod('__construct') && !$classReflection->hasNativeMethod($class)) {
if (count($node->args) > 0) {
return array_merge($messages, [
sprintf(
'Class %s does not have a constructor and must be instantiated without any parameters.',
$classReflection->getDisplayName()
),
]);
}
return [];
}
$constructorReflection = $classReflection->hasNativeMethod('__construct') ? $classReflection->getNativeMethod('__construct') : $classReflection->getNativeMethod($class);
if (!$scope->canCallMethod($constructorReflection)) {
$messages[] = sprintf(
'Cannot instantiate class %s via %s constructor %s::%s().',
$classReflection->getDisplayName(),
$constructorReflection->isPrivate() ? 'private' : 'protected',
$constructorReflection->getDeclaringClass()->getDisplayName(),
$constructorReflection->getName()
);
}
return array_merge($messages, $this->check->check(
$constructorReflection,
$scope,
$node,
[
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, %d required.',
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, %d required.',
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, at least %d required.',
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, at least %d required.',
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameter, %d-%d required.',
'Class ' . $classReflection->getDisplayName() . ' constructor invoked with %d parameters, %d-%d required.',
'Parameter #%d %s of class ' . $classReflection->getDisplayName() . ' constructor expects %s, %s given.',
'', // constructor does not have a return type
'Parameter #%d %s of class ' . $classReflection->getDisplayName() . ' constructor is passed by reference, so it expects variables only',
]
));
}
}
src/Rules/Classes/RequireParentConstructCallRule.php 0000666 00000007002 13436751442 0016666 0 ustar 00 getAnalysedContextFile(), '(in context of ') !== false) {
return []; // skip traits
}
if ($node->name !== '__construct') {
return [];
}
$classReflection = $scope->getClassReflection()->getNativeReflection();
if ($classReflection->isInterface() || $classReflection->isAnonymous()) {
return [];
}
if ($this->callsParentConstruct($node)) {
if ($classReflection->getParentClass() === false) {
return [
sprintf(
'%s::__construct() calls parent constructor but does not extend any class.',
$classReflection->getName()
),
];
}
if ($this->getParentConstructorClass($classReflection) === false) {
return [
sprintf(
'%s::__construct() calls parent constructor but parent does not have one.',
$classReflection->getName()
),
];
}
} else {
$parentClass = $this->getParentConstructorClass($classReflection);
if ($parentClass !== false) {
return [
sprintf(
'%s::__construct() does not call parent constructor from %s.',
$classReflection->getName(),
$parentClass->getName()
),
];
}
}
return [];
}
private function callsParentConstruct(Node $parserNode): bool
{
if (!isset($parserNode->stmts)) {
return false;
}
foreach ($parserNode->stmts as $statement) {
$statement = $this->ignoreErrorSuppression($statement);
if ($statement instanceof \PhpParser\Node\Expr\StaticCall) {
if (
$statement->class instanceof Name
&& ((string) $statement->class === 'parent')
&& $statement->name === '__construct'
) {
return true;
}
} else {
if ($this->callsParentConstruct($statement)) {
return true;
}
}
}
return false;
}
/**
* @param \ReflectionClass $classReflection
* @return \ReflectionClass|false
*/
private function getParentConstructorClass(\ReflectionClass $classReflection)
{
while ($classReflection->getParentClass() !== false) {
$constructor = $classReflection->getParentClass()->hasMethod('__construct') ? $classReflection->getParentClass()->getMethod('__construct') : null;
$constructorWithClassName = $classReflection->getParentClass()->hasMethod($classReflection->getParentClass()->getName()) ? $classReflection->getParentClass()->getMethod($classReflection->getParentClass()->getName()) : null;
if (
(
$constructor !== null
&& $constructor->getDeclaringClass()->getName() === $classReflection->getParentClass()->getName()
&& !$constructor->isAbstract()
) || (
$constructorWithClassName !== null
&& $constructorWithClassName->getDeclaringClass()->getName() === $classReflection->getParentClass()->getName()
&& !$constructorWithClassName->isAbstract()
)
) {
return $classReflection->getParentClass();
}
$classReflection = $classReflection->getParentClass();
}
return false;
}
private function ignoreErrorSuppression(Node $statement): Node
{
if ($statement instanceof Node\Expr\ErrorSuppress) {
return $statement->expr;
}
return $statement;
}
}
src/Rules/Classes/ExistingClassInInstanceOfRule.php 0000666 00000003264 13436751442 0016426 0 ustar 00 broker = $broker;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
$this->checkClassCaseSensitivity = $checkClassCaseSensitivity;
}
public function getNodeType(): string
{
return Instanceof_::class;
}
/**
* @param \PhpParser\Node\Expr\Instanceof_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
$class = $node->class;
if (!($class instanceof \PhpParser\Node\Name)) {
return [];
}
$name = (string) $class;
$lowercaseName = strtolower($name);
if (in_array($lowercaseName, [
'self',
'static',
'parent',
], true)) {
if (!$scope->isInClass()) {
return [
sprintf('Using %s outside of class scope.', $lowercaseName),
];
}
return [];
}
if (!$this->broker->hasClass($name)) {
return [
sprintf('Class %s not found.', $name),
];
} elseif ($this->checkClassCaseSensitivity) {
return $this->classCaseSensitivityCheck->checkClassNames([$name]);
}
return [];
}
}
src/Rules/Classes/ExistingClassesInClassImplementsRule.php 0000666 00000001652 13436751442 0020027 0 ustar 00 classCaseSensitivityCheck = $classCaseSensitivityCheck;
}
public function getNodeType(): string
{
return Node\Stmt\Class_::class;
}
/**
* @param \PhpParser\Node\Stmt\Class_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
return $this->classCaseSensitivityCheck->checkClassNames(
array_map(function (Node\Name $traitName): string {
return (string) $traitName;
}, $node->implements)
);
}
}
src/Rules/Classes/ExistingClassInClassExtendsRule.php 0000666 00000001576 13436751442 0017001 0 ustar 00 classCaseSensitivityCheck = $classCaseSensitivityCheck;
}
public function getNodeType(): string
{
return Node\Stmt\Class_::class;
}
/**
* @param \PhpParser\Node\Stmt\Class_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if ($node->extends === null) {
return [];
}
return $this->classCaseSensitivityCheck->checkClassNames([(string) $node->extends]);
}
}
src/Rules/Classes/UnusedConstructorParametersRule.php 0000666 00000002460 13436751442 0017137 0 ustar 00 check = $check;
}
public function getNodeType(): string
{
return ClassMethod::class;
}
/**
* @param \PhpParser\Node\Stmt\ClassMethod $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if ($node->name !== '__construct' || $node->stmts === null) {
return [];
}
if (count($node->params) === 0) {
return [];
}
$message = sprintf('Constructor of class %s has an unused parameter $%%s.', $scope->getClassReflection()->getDisplayName());
if ($scope->getClassReflection()->isAnonymous()) {
$message = 'Constructor of an anonymous class has an unused parameter $%s.';
}
return $this->check->getUnusedParameters(
array_map(function (Param $parameter): string {
return $parameter->name;
}, $node->params),
$node->stmts,
$message
);
}
}
src/Rules/Classes/ExistingClassesInInterfaceExtendsRule.php 0000666 00000001660 13436751442 0020156 0 ustar 00 classCaseSensitivityCheck = $classCaseSensitivityCheck;
}
public function getNodeType(): string
{
return Node\Stmt\Interface_::class;
}
/**
* @param \PhpParser\Node\Stmt\Interface_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
return $this->classCaseSensitivityCheck->checkClassNames(
array_map(function (Node\Name $traitName): string {
return (string) $traitName;
}, $node->extends)
);
}
}
src/Rules/Classes/ImpossibleInstanceOfRule.php 0000666 00000003236 13436751442 0015464 0 ustar 00 checkAlwaysTrueInstanceof = $checkAlwaysTrueInstanceof;
}
public function getNodeType(): string
{
return Node\Expr\Instanceof_::class;
}
/**
* @param \PhpParser\Node\Expr\Instanceof_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if ($node->class instanceof Node\Name) {
$className = $scope->resolveName($node->class);
$type = new ObjectType($className);
} else {
$type = $scope->getType($node->class);
}
$expressionType = $scope->getType($node->expr);
$isExpressionObject = (new ObjectWithoutClassType())->isSuperTypeOf($expressionType);
if (!$isExpressionObject->no() && $type instanceof StringType) {
return [];
}
$isSuperType = $type->isSuperTypeOf($expressionType)
->and($isExpressionObject);
if ($isSuperType->no()) {
return [
sprintf(
'Instanceof between %s and %s will always evaluate to false.',
$expressionType->describe(),
$type->describe()
),
];
} elseif ($isSuperType->yes() && $this->checkAlwaysTrueInstanceof) {
return [
sprintf(
'Instanceof between %s and %s will always evaluate to true.',
$expressionType->describe(),
$type->describe()
),
];
}
return [];
}
}
src/Rules/Classes/ClassConstantRule.php 0000666 00000010052 13436751442 0014155 0 ustar 00 broker = $broker;
$this->ruleLevelHelper = $ruleLevelHelper;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
}
public function getNodeType(): string
{
return ClassConstFetch::class;
}
/**
* @param \PhpParser\Node\Expr\ClassConstFetch $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
$constantName = $node->name;
if (!is_string($constantName)) {
return [];
}
$class = $node->class;
$messages = [];
if ($class instanceof \PhpParser\Node\Name) {
$className = (string) $class;
if ($className === 'self' || $className === 'static') {
if (!$scope->isInClass()) {
return [
sprintf('Using %s outside of class scope.', $className),
];
}
$className = $scope->getClassReflection()->getName();
} elseif ($className === 'parent') {
if (!$scope->isInClass()) {
return [
sprintf('Using %s outside of class scope.', $className),
];
}
$currentClassReflection = $scope->getClassReflection();
if ($currentClassReflection->getParentClass() === false) {
return [
sprintf(
'Access to parent::%s but %s does not extend any class.',
$constantName,
$currentClassReflection->getDisplayName()
),
];
}
$className = $currentClassReflection->getParentClass()->getName();
} else {
if (!$this->broker->hasClass($className)) {
if (strtolower($constantName) === 'class') {
return [
sprintf('Class %s not found.', $className),
];
}
return [
sprintf('Access to constant %s on an unknown class %s.', $constantName, $className),
];
} else {
$messages = $this->classCaseSensitivityCheck->checkClassNames([$className]);
}
$className = $this->broker->getClass($className)->getName();
}
$classType = new ObjectType($className);
} else {
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$class,
sprintf('Access to constant %s on an unknown class %%s.', $constantName)
);
$classType = $classTypeResult->getType();
if ($classType instanceof ErrorType) {
return $classTypeResult->getUnknownClassErrors();
}
}
if ($classType instanceof StringType) {
return $messages;
}
$typeForDescribe = $classType;
$classType = TypeCombinator::remove($classType, new StringType());
if (!$classType->canAccessConstants()) {
return array_merge($messages, [
sprintf('Cannot access constant %s on %s.', $constantName, $typeForDescribe->describe()),
]);
}
if (strtolower($constantName) === 'class') {
return $messages;
}
if (!$classType->hasConstant($constantName)) {
return array_merge($messages, [
sprintf(
'Access to undefined constant %s::%s.',
$typeForDescribe->describe(),
$constantName
),
]);
}
$constantReflection = $classType->getConstant($constantName);
if (!$scope->canAccessConstant($constantReflection)) {
return array_merge($messages, [
sprintf(
'Access to %s constant %s of class %s.',
$constantReflection->isPrivate() ? 'private' : 'protected',
$constantName,
$constantReflection->getDeclaringClass()->getDisplayName()
),
]);
}
return $messages;
}
}
src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php 0000666 00000003342 13436751442 0020327 0 ustar 00 typeSpecifier = $typeSpecifier;
$this->checkAlwaysTrueCheckTypeFunctionCall = $checkAlwaysTrueCheckTypeFunctionCall;
}
public function getNodeType(): string
{
return \PhpParser\Node\Expr\FuncCall::class;
}
/**
* @param \PhpParser\Node\Expr\FuncCall $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[] errors
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Node\Name) {
return [];
}
$sureTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $node)->getSureTypes();
if (count($sureTypes) !== 1) {
return [];
}
$sureType = reset($sureTypes);
$argumentType = $scope->getType($sureType[0]);
/** @var \PHPStan\Type\Type $resultType */
$resultType = $sureType[1];
$isSuperType = $resultType->isSuperTypeOf($argumentType);
$functionName = (string) $node->name;
if ($functionName === 'is_a') {
return [];
}
if ($isSuperType->no()) {
return [sprintf(
'Call to function %s() will always evaluate to false.',
$functionName
)];
} elseif ($isSuperType->yes() && $this->checkAlwaysTrueCheckTypeFunctionCall) {
return [sprintf(
'Call to function %s() will always evaluate to true.',
$functionName
)];
}
return [];
}
}
src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php 0000666 00000002720 13436751442 0020562 0 ustar 00 getType($node->left);
$rightType = $scope->getType($node->right);
if (
(
$node->left instanceof Node\Expr\PropertyFetch
|| $node->left instanceof Node\Expr\StaticPropertyFetch
)
&& $rightType instanceof NullType
) {
return [];
}
if (
(
$node->right instanceof Node\Expr\PropertyFetch
|| $node->right instanceof Node\Expr\StaticPropertyFetch
)
&& $leftType instanceof NullType
) {
return [];
}
if ($leftType->isSuperTypeOf($rightType)->no()) {
return [
sprintf(
'Strict comparison using %s between %s and %s will always evaluate to %s.',
$node instanceof Node\Expr\BinaryOp\Identical ? '===' : '!==',
$leftType->describe(),
$rightType->describe(),
$node instanceof Node\Expr\BinaryOp\Identical ? 'false' : 'true'
),
];
}
return [];
}
}
src/Rules/ClassCaseSensitivityCheck.php 0000666 00000002322 13436751442 0014224 0 ustar 00 broker = $broker;
}
/**
* @param string[] $classNames
* @return string[]
*/
public function checkClassNames(array $classNames): array
{
$messages = [];
foreach ($classNames as $className) {
if (!$this->broker->hasClass($className)) {
continue;
}
$classReflection = $this->broker->getClass($className);
$realClassName = $classReflection->getName();
if (strtolower($realClassName) !== strtolower($className)) {
continue; // skip class alias
}
if ($realClassName === $className) {
continue;
}
$messages[] = sprintf(
'%s %s referenced with incorrect case: %s.',
$this->getTypeName($classReflection),
$realClassName,
$className
);
}
return $messages;
}
private function getTypeName(ClassReflection $classReflection): string
{
if ($classReflection->isInterface()) {
return 'Interface';
} elseif ($classReflection->isTrait()) {
return 'Trait';
}
return 'Class';
}
}
src/Rules/RuleLevelHelper.php 0000666 00000005167 13436751442 0012223 0 ustar 00 broker = $broker;
$this->checkNullables = $checkNullables;
$this->checkThisOnly = $checkThisOnly;
$this->checkUnionTypes = $checkUnionTypes;
}
public function isThis(Expr $expression): bool
{
if (!($expression instanceof Expr\Variable)) {
return false;
}
if (!is_string($expression->name)) {
return false;
}
return $expression->name === 'this';
}
public function accepts(Type $acceptingType, Type $acceptedType): bool
{
if (
!$this->checkNullables
&& !$acceptingType instanceof NullType
&& !$acceptedType instanceof NullType
) {
$acceptedType = TypeCombinator::removeNull($acceptedType);
}
return $acceptingType->accepts($acceptedType);
}
public function findTypeToCheck(
Scope $scope,
Expr $var,
string $unknownClassErrorPattern
): FoundTypeResult
{
if ($this->checkThisOnly && !$this->isThis($var)) {
return new FoundTypeResult(new ErrorType(), [], []);
}
$type = $scope->getType($var);
if (!$type instanceof NullType) {
$type = \PHPStan\Type\TypeCombinator::removeNull($type);
}
if ($type instanceof MixedType || $type instanceof NeverType) {
return new FoundTypeResult(new ErrorType(), [], []);
}
if ($type instanceof StaticType) {
$type = $type->resolveStatic($type->getBaseClass());
}
if ($type instanceof ArrayType) {
return new FoundTypeResult($type, [], []);
}
$errors = [];
$referencedClasses = $type->getReferencedClasses();
foreach ($referencedClasses as $referencedClass) {
if (!$this->broker->hasClass($referencedClass)) {
$errors[] = sprintf($unknownClassErrorPattern, $referencedClass);
}
}
if (count($errors) > 0) {
return new FoundTypeResult(new ErrorType(), [], $errors);
}
if (!$this->checkUnionTypes && $type instanceof UnionType) {
return new FoundTypeResult(new ErrorType(), [], []);
}
return new FoundTypeResult($type, $referencedClasses, []);
}
}
src/Rules/Rule.php 0000666 00000000633 13436751442 0010064 0 ustar 00 ruleLevelHelper = $ruleLevelHelper;
$this->checkArgumentTypes = $checkArgumentTypes;
$this->checkArgumentsPassedByReference = $checkArgumentsPassedByReference;
}
/**
* @param \PHPStan\Reflection\ParametersAcceptor $function
* @param \PHPStan\Analyser\Scope $scope
* @param \PhpParser\Node\Expr\FuncCall|\PhpParser\Node\Expr\MethodCall|\PhpParser\Node\Expr\StaticCall|\PhpParser\Node\Expr\New_ $funcCall
* @param string[] $messages Eight message templates
* @return string[]
*/
public function check(ParametersAcceptor $function, Scope $scope, $funcCall, array $messages): array
{
if (
$function instanceof FunctionReflection
&& in_array($function->getName(), [
'implode',
'strtok',
], true)
) {
$functionParametersMinCount = 1;
$functionParametersMaxCount = 2;
} elseif (
$function instanceof MethodReflection
&& $function->getDeclaringClass()->getName() === 'DatePeriod'
&& $function->getName() === '__construct'
) {
$functionParametersMinCount = 1;
$functionParametersMaxCount = 4;
} elseif (
$function instanceof MethodReflection
&& $function->getDeclaringClass()->getName() === 'mysqli'
&& $function->getName() === 'query'
) {
$functionParametersMinCount = 1;
$functionParametersMaxCount = 2;
} else {
$functionParametersMinCount = 0;
$functionParametersMaxCount = 0;
foreach ($function->getParameters() as $parameter) {
if (!$parameter->isOptional()) {
$functionParametersMinCount++;
}
$functionParametersMaxCount++;
}
if ($function->isVariadic()) {
$functionParametersMaxCount = -1;
}
}
$errors = [];
$invokedParametersCount = count($funcCall->args);
foreach ($funcCall->args as $arg) {
if ($arg->unpack) {
$invokedParametersCount = max($functionParametersMinCount, $functionParametersMaxCount);
break;
}
}
if ($invokedParametersCount < $functionParametersMinCount || $invokedParametersCount > $functionParametersMaxCount) {
if ($functionParametersMinCount === $functionParametersMaxCount) {
$errors[] = sprintf(
$invokedParametersCount === 1 ? $messages[0] : $messages[1],
$invokedParametersCount,
$functionParametersMinCount
);
} elseif ($functionParametersMaxCount === -1 && $invokedParametersCount < $functionParametersMinCount) {
$errors[] = sprintf(
$invokedParametersCount === 1 ? $messages[2] : $messages[3],
$invokedParametersCount,
$functionParametersMinCount
);
} elseif ($functionParametersMaxCount !== -1) {
$errors[] = sprintf(
$invokedParametersCount === 1 ? $messages[4] : $messages[5],
$invokedParametersCount,
$functionParametersMinCount,
$functionParametersMaxCount
);
}
}
if (
$function->getReturnType() instanceof VoidType
&& !$scope->isInFirstLevelStatement()
&& !$funcCall instanceof \PhpParser\Node\Expr\New_
) {
$errors[] = $messages[7];
}
if (!$this->checkArgumentTypes && !$this->checkArgumentsPassedByReference) {
return $errors;
}
$parameters = $function->getParameters();
foreach ($funcCall->args as $i => $argument) {
if (!isset($parameters[$i])) {
if (!$function->isVariadic() || count($parameters) === 0) {
break;
}
$parameter = $parameters[count($parameters) - 1];
$parameterType = $parameter->getType();
if ($parameterType instanceof ArrayType) {
if (!$argument->unpack) {
$parameterType = $parameterType->getItemType();
}
} else {
break;
}
} else {
$parameter = $parameters[$i];
$parameterType = $parameter->getType();
if ($parameter->isVariadic()) {
if ($parameterType instanceof ArrayType && !$argument->unpack) {
$parameterType = $parameterType->getItemType();
}
} elseif ($argument->unpack) {
continue;
}
}
$argumentValueType = $scope->getType($argument->value);
$secondAccepts = null;
if ($parameterType->isIterable()->yes() && $parameter->isVariadic()) {
$secondAccepts = $this->ruleLevelHelper->accepts(
new IterableIterableType(
new MixedType(),
$parameterType->getIterableValueType()
),
$argumentValueType
);
}
if (
$this->checkArgumentTypes
&& !$this->ruleLevelHelper->accepts($parameterType, $argumentValueType)
&& ($secondAccepts === null || !$secondAccepts)
&& (
!($parameterType instanceof StringType)
|| !$argumentValueType->hasMethod('__toString')
)
) {
$errors[] = sprintf(
$messages[6],
$i + 1,
sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()),
$parameterType->describe(),
$argumentValueType->describe()
);
}
if (
$this->checkArgumentsPassedByReference
&& $parameter->isPassedByReference()
&& !$argument->value instanceof \PhpParser\Node\Expr\Variable
&& !$argument->value instanceof \PhpParser\Node\Expr\ArrayDimFetch
&& !$argument->value instanceof \PhpParser\Node\Expr\PropertyFetch
&& !$argument->value instanceof \PhpParser\Node\Expr\StaticPropertyFetch
) {
$errors[] = sprintf(
$messages[8],
$i + 1,
sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName())
);
}
}
return $errors;
}
}
src/Rules/Methods/ReturnTypeRule.php 0000666 00000003516 13436751442 0013534 0 ustar 00 returnTypeCheck = $returnTypeCheck;
}
public function getNodeType(): string
{
return Return_::class;
}
/**
* @param \PhpParser\Node\Stmt\Return_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if ($scope->getFunction() === null) {
return [];
}
if ($scope->isInAnonymousFunction()) {
return [];
}
$method = $scope->getFunction();
if (!($method instanceof MethodReflection)) {
return [];
}
$reflection = null;
if ($method->getDeclaringClass()->getNativeReflection()->hasMethod($method->getName())) {
$reflection = $method->getDeclaringClass()->getNativeReflection()->getMethod($method->getName());
}
return $this->returnTypeCheck->checkReturnType(
$scope,
$method->getReturnType(),
$node->expr,
sprintf(
'Method %s::%s() should return %%s but empty return statement found.',
$method->getDeclaringClass()->getDisplayName(),
$method->getName()
),
sprintf(
'Method %s::%s() with return type void returns %%s but should not return anything.',
$method->getDeclaringClass()->getDisplayName(),
$method->getName()
),
sprintf(
'Method %s::%s() should return %%s but returns %%s.',
$method->getDeclaringClass()->getDisplayName(),
$method->getName()
),
$reflection !== null && $reflection->isGenerator(),
false
);
}
}
src/Rules/Methods/CallMethodsOnPossiblyNullRule.php 0000666 00000002457 13436751442 0016472 0 ustar 00 ruleLevelHelper = $ruleLevelHelper;
$this->checkThisOnly = $checkThisOnly;
}
public function getNodeType(): string
{
return MethodCall::class;
}
/**
* @param \PhpParser\Node\Expr\MethodCall $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (!is_string($node->name)) {
return [];
}
if ($this->checkThisOnly && !$this->ruleLevelHelper->isThis($node->var)) {
return [];
}
$type = $scope->getType($node->var);
if (!$type instanceof UnionType) {
return [];
}
if (\PHPStan\Type\TypeCombinator::containsNull($type)) {
return [
sprintf(
'Calling method %s() on possibly null value of type %s.',
$node->name,
$type->describe()
),
];
}
return [];
}
}
src/Rules/Methods/CallStaticMethodsRule.php 0000666 00000014422 13436751442 0014760 0 ustar 00 broker = $broker;
$this->check = $check;
$this->ruleLevelHelper = $ruleLevelHelper;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
}
public function getNodeType(): string
{
return StaticCall::class;
}
/**
* @param \PhpParser\Node\Expr\StaticCall $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
$methodName = $node->name;
if (!is_string($methodName)) {
return [];
}
$class = $node->class;
$errors = [];
if ($class instanceof Name) {
$className = (string) $class;
if ($className === 'self' || $className === 'static') {
if (!$scope->isInClass()) {
return [
sprintf(
'Calling %s::%s() outside of class scope.',
$class,
$methodName
),
];
}
$className = $scope->getClassReflection()->getName();
} elseif ($className === 'parent') {
if (!$scope->isInClass()) {
return [
sprintf(
'Calling %s::%s() outside of class scope.',
$className,
$methodName
),
];
}
$currentClassReflection = $scope->getClassReflection();
if ($currentClassReflection->getParentClass() === false) {
return [
sprintf(
'%s::%s() calls parent::%s() but %s does not extend any class.',
$scope->getClassReflection()->getDisplayName(),
$scope->getFunctionName(),
$methodName,
$scope->getClassReflection()->getDisplayName()
),
];
}
if ($scope->getFunctionName() === null) {
throw new \PHPStan\ShouldNotHappenException();
}
$className = $currentClassReflection->getParentClass()->getName();
} else {
if (!$this->broker->hasClass($className)) {
return [
sprintf('Call to static method %s() on an unknown class %s.', $methodName, $className),
];
} else {
$errors = $this->classCaseSensitivityCheck->checkClassNames([$className]);
}
$className = $this->broker->getClass($className)->getName();
}
$classType = new ObjectType($className);
} else {
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$class,
sprintf('Call to static method %s() on an unknown class %%s.', $methodName)
);
$classType = $classTypeResult->getType();
if ($classType instanceof ErrorType) {
return $classTypeResult->getUnknownClassErrors();
}
}
if ($classType instanceof StringType) {
return [];
}
$typeForDescribe = $classType;
$classType = TypeCombinator::remove($classType, new StringType());
if (!$classType->canCallMethods()) {
return array_merge($errors, [
sprintf('Cannot call static method %s() on %s.', $methodName, $typeForDescribe->describe()),
]);
}
if (!$classType->hasMethod($methodName)) {
return array_merge($errors, [
sprintf(
'Call to an undefined static method %s::%s().',
$typeForDescribe->describe(),
$methodName
),
]);
}
$method = $classType->getMethod($methodName, $scope);
if (!$method->isStatic()) {
$function = $scope->getFunction();
if (
!$function instanceof MethodReflection
|| $function->isStatic()
|| !$scope->isInClass()
|| (
$classType instanceof TypeWithClassName
&& $scope->getClassReflection()->getName() !== $classType->getClassName()
&& !$scope->getClassReflection()->isSubclassOf($classType->getClassName())
)
) {
return array_merge($errors, [
sprintf(
'Static call to instance method %s::%s().',
$method->getDeclaringClass()->getDisplayName(),
$method->getName()
),
]);
}
}
if (!$scope->canCallMethod($method)) {
$errors = array_merge($errors, [
sprintf(
'Call to %s %s %s() of class %s.',
$method->isPrivate() ? 'private' : 'protected',
$method->isStatic() ? 'static method' : 'method',
$method->getName(),
$method->getDeclaringClass()->getDisplayName()
),
]);
}
$lowercasedMethodName = sprintf(
'%s %s',
$method->isStatic() ? 'static method' : 'method',
$method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()'
);
$displayMethodName = sprintf(
'%s %s',
$method->isStatic() ? 'Static method' : 'Method',
$method->getDeclaringClass()->getDisplayName() . '::' . $method->getName() . '()'
);
$errors = array_merge($errors, $this->check->check(
$method,
$scope,
$node,
[
$displayMethodName . ' invoked with %d parameter, %d required.',
$displayMethodName . ' invoked with %d parameters, %d required.',
$displayMethodName . ' invoked with %d parameter, at least %d required.',
$displayMethodName . ' invoked with %d parameters, at least %d required.',
$displayMethodName . ' invoked with %d parameter, %d-%d required.',
$displayMethodName . ' invoked with %d parameters, %d-%d required.',
'Parameter #%d %s of ' . $lowercasedMethodName . ' expects %s, %s given.',
'Result of ' . $lowercasedMethodName . ' (void) is used.',
'Parameter #%d %s of ' . $lowercasedMethodName . ' is passed by reference, so it expects variables only.',
]
));
if ($method->getName() !== $methodName) {
$errors[] = sprintf('Call to %s with incorrect case: %s', $lowercasedMethodName, $methodName);
}
return $errors;
}
}
src/Rules/Methods/ExistingClassesInTypehintsRule.php 0000666 00000002057 13436751442 0016721 0 ustar 00 check = $check;
}
public function getNodeType(): string
{
return ClassMethod::class;
}
/**
* @param \PhpParser\Node\Stmt\ClassMethod $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
return $this->check->checkFunction(
$node,
$scope,
sprintf(
'Parameter $%%s of method %s::%s() has invalid typehint type %%s.',
$scope->getClassReflection()->getDisplayName(),
$node->name
),
sprintf(
'Return typehint of method %s::%s() has invalid type %%s.',
$scope->getClassReflection()->getDisplayName(),
$node->name
)
);
}
}
src/Rules/Methods/CallMethodsRule.php 0000666 00000007642 13436751442 0013616 0 ustar 00 broker = $broker;
$this->check = $check;
$this->ruleLevelHelper = $ruleLevelHelper;
}
public function getNodeType(): string
{
return MethodCall::class;
}
/**
* @param \PhpParser\Node\Expr\MethodCall $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (!is_string($node->name)) {
return [];
}
$name = $node->name;
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$node->var,
sprintf('Call to method %s() on an unknown class %%s.', $name)
);
$type = $typeResult->getType();
if ($type instanceof ErrorType) {
return $typeResult->getUnknownClassErrors();
}
if (!$type->canCallMethods()) {
return [
sprintf('Cannot call method %s() on %s.', $name, $type->describe()),
];
}
if (!$type->hasMethod($name)) {
if (count($typeResult->getReferencedClasses()) === 1) {
$referencedClass = $typeResult->getReferencedClasses()[0];
$methodClassReflection = $this->broker->getClass($referencedClass);
$parentClassReflection = $methodClassReflection->getParentClass();
while ($parentClassReflection !== false) {
if ($parentClassReflection->hasMethod($name)) {
return [
sprintf(
'Call to private method %s() of parent class %s.',
$parentClassReflection->getMethod($name, $scope)->getName(),
$parentClassReflection->getDisplayName()
),
];
}
$parentClassReflection = $parentClassReflection->getParentClass();
}
}
return [
sprintf(
'Call to an undefined method %s::%s().',
$type->describe(),
$name
),
];
}
$methodReflection = $type->getMethod($name, $scope);
$messagesMethodName = $methodReflection->getDeclaringClass()->getDisplayName() . '::' . $methodReflection->getName() . '()';
$errors = [];
if (!$scope->canCallMethod($methodReflection)) {
$errors[] = sprintf(
'Call to %s method %s() of class %s.',
$methodReflection->isPrivate() ? 'private' : 'protected',
$methodReflection->getName(),
$methodReflection->getDeclaringClass()->getDisplayName()
);
}
$errors = array_merge($errors, $this->check->check(
$methodReflection,
$scope,
$node,
[
'Method ' . $messagesMethodName . ' invoked with %d parameter, %d required.',
'Method ' . $messagesMethodName . ' invoked with %d parameters, %d required.',
'Method ' . $messagesMethodName . ' invoked with %d parameter, at least %d required.',
'Method ' . $messagesMethodName . ' invoked with %d parameters, at least %d required.',
'Method ' . $messagesMethodName . ' invoked with %d parameter, %d-%d required.',
'Method ' . $messagesMethodName . ' invoked with %d parameters, %d-%d required.',
'Parameter #%d %s of method ' . $messagesMethodName . ' expects %s, %s given.',
'Result of method ' . $messagesMethodName . ' (void) is used.',
'Parameter #%d %s of method ' . $messagesMethodName . ' is passed by reference, so it expects variables only.',
]
));
if (strtolower($methodReflection->getName()) === strtolower($name) && $methodReflection->getName() !== $name) {
$errors[] = sprintf('Call to method %s with incorrect case: %s', $messagesMethodName, $name);
}
return $errors;
}
}
src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php 0000666 00000004556 13436751442 0017012 0 ustar 00 broker = $broker;
$this->printer = $printer;
}
public function getNodeType(): string
{
return \PhpParser\Node\Expr\Array_::class;
}
/**
* @param \PhpParser\Node\Expr\Array_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(\PhpParser\Node $node, Scope $scope): array
{
$values = [];
$duplicateKeys = [];
foreach ($node->items as $item) {
if ($item === null) {
continue;
}
if ($item->key === null) {
continue;
}
$key = $item->key;
if (
!$key instanceof \PhpParser\Node\Scalar\String_
&& !$key instanceof \PhpParser\Node\Scalar\LNumber
&& !$key instanceof \PhpParser\Node\Scalar\DNumber
&& !$key instanceof \PhpParser\Node\Expr\ConstFetch
) {
continue;
}
if ($key instanceof \PhpParser\Node\Expr\ConstFetch) {
$printedValue = (string) $key->name;
$constName = strtolower($printedValue);
if ($constName === 'true') {
$value = true;
} elseif ($constName === 'false') {
$value = false;
} elseif ($constName === 'null') {
$value = null;
} elseif ($this->broker->hasConstant($key->name, $scope)) {
$value = constant($this->broker->resolveConstantName($key->name, $scope));
} else {
continue;
}
} else {
$printedValue = $this->printer->prettyPrintExpr($key);
$value = eval(sprintf('return %s;', $printedValue));
}
$previousCount = count($values);
$values[$value] = $printedValue;
if ($previousCount === count($values)) {
if (!isset($duplicateKeys[$value])) {
$duplicateKeys[$value] = [$values[$value]];
}
$duplicateKeys[$value][] = $printedValue;
}
}
$messages = [];
foreach ($duplicateKeys as $key => $values) {
$messages[] = sprintf(
'Array has %d %s with value %s (%s).',
count($values),
count($values) === 1 ? 'duplicate key' : 'duplicate keys',
var_export($key, true),
implode(', ', $values)
);
}
return $messages;
}
}
src/Rules/Arrays/AppendedArrayItemTypeRule.php 0000666 00000002470 13436751442 0015447 0 ustar 00 ruleLevelHelper = $ruleLevelHelper;
}
public function getNodeType(): string
{
return Assign::class;
}
/**
* @param \PhpParser\Node\Expr\Assign $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(\PhpParser\Node $node, Scope $scope): array
{
if (!($node->var instanceof ArrayDimFetch)) {
return [];
}
$assignedToType = $scope->getType($node->var->var);
if (!($assignedToType instanceof ArrayType)) {
return [];
}
if ($assignedToType->isItemTypeInferredFromLiteralArray()) {
return [];
}
$assignedValueType = $scope->getType($node->expr);
if (!$this->ruleLevelHelper->accepts($assignedToType->getItemType(), $assignedValueType)) {
return [
sprintf(
'Array (%s) does not accept %s.',
$assignedToType->describe(),
$assignedValueType->describe()
),
];
}
return [];
}
}
src/Rules/Arrays/IterableInForeachRule.php 0000666 00000002133 13436751442 0014551 0 ustar 00 checkUnionTypes = $checkUnionTypes;
}
public function getNodeType(): string
{
return \PhpParser\Node\Stmt\Foreach_::class;
}
/**
* @param \PhpParser\Node\Stmt\Foreach_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(\PhpParser\Node $node, Scope $scope): array
{
$iteratedExpressionType = $scope->getType($node->expr);
if (!$this->checkUnionTypes && $iteratedExpressionType instanceof UnionType) {
return [];
}
if (!$iteratedExpressionType instanceof MixedType && !$iteratedExpressionType->isIterable()->yes()) {
return [
sprintf(
'Argument of an invalid type %s supplied for foreach, only iterables are supported.',
$iteratedExpressionType->describe()
),
];
}
return [];
}
}
src/Rules/Arrays/InvalidKeyInArrayDimFetchRule.php 0000666 00000001547 13436751442 0016204 0 ustar 00 dim === null) {
return [];
}
$varType = $scope->getType($node->var);
if (!$varType instanceof ArrayType) {
return [];
}
$dimensionType = $scope->getType($node->dim);
if (!AllowedArrayKeysTypes::getType()->accepts($dimensionType)) {
return [
sprintf('Invalid array key type %s.', $dimensionType->describe()),
];
}
return [];
}
}
src/Rules/Arrays/InvalidKeyInArrayItemRule.php 0000666 00000001332 13436751442 0015407 0 ustar 00 key === null) {
return [];
}
$dimensionType = $scope->getType($node->key);
if (!AllowedArrayKeysTypes::getType()->accepts($dimensionType)) {
return [
sprintf('Invalid array key type %s.', $dimensionType->describe()),
];
}
return [];
}
}
src/Rules/Arrays/AllowedArrayKeysTypes.php 0000666 00000000764 13436751442 0014672 0 ustar 00 broker = $broker;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
$this->checkClassCaseSensitivity = $checkClassCaseSensitivity;
$this->checkThisOnly = $checkThisOnly;
}
/**
* @param \PhpParser\Node\FunctionLike $function
* @param \PHPStan\Analyser\Scope $scope
* @param string $parameterMessage
* @param string $returnMessage
* @return string[]
*/
public function checkFunction(
FunctionLike $function,
Scope $scope,
string $parameterMessage,
string $returnMessage
): array
{
if ($function instanceof ClassMethod) {
return $this->checkParametersAcceptor(
$scope->getClassReflection()->getNativeMethod($function->name),
$parameterMessage,
$returnMessage
);
}
if ($function instanceof Function_) {
$functionName = $function->name;
if (isset($function->namespacedName)) {
$functionName = (string) $function->namespacedName;
}
$functionNameName = new Name($functionName);
if (!$this->broker->hasFunction($functionNameName)) {
return [];
}
return $this->checkParametersAcceptor(
$this->broker->getFunction($functionNameName),
$parameterMessage,
$returnMessage
);
}
$errors = [];
foreach ($function->getParams() as $param) {
$class = $param->type instanceof NullableType
? (string) $param->type->type
: (string) $param->type;
if ($class === '' || in_array($class, self::VALID_TYPEHINTS, true)) {
continue;
}
if (!$this->broker->hasClass($class)) {
$errors[] = sprintf($parameterMessage, $param->name, $class);
} elseif ($this->checkClassCaseSensitivity) {
$errors = array_merge(
$errors,
$this->classCaseSensitivityCheck->checkClassNames([$class])
);
}
}
$returnType = $function->getReturnType() instanceof NullableType
? (string) $function->getReturnType()->type
: (string) $function->getReturnType();
if (
$returnType !== ''
&& !in_array($returnType, self::VALID_TYPEHINTS, true)
) {
if (!$this->broker->hasClass($returnType)) {
$errors[] = sprintf($returnMessage, $returnType);
} elseif ($this->checkClassCaseSensitivity) {
$errors = array_merge(
$errors,
$this->classCaseSensitivityCheck->checkClassNames([$returnType])
);
}
}
return $errors;
}
private function checkParametersAcceptor(
ParametersAcceptorWithPhpDocs $parametersAcceptor,
string $parameterMessage,
string $returnMessage
): array
{
$errors = [];
foreach ($parametersAcceptor->getParameters() as $parameter) {
if ($this->checkThisOnly) {
$referencedClasses = $parameter->getType()->getReferencedClasses();
} else {
$referencedClasses = array_merge(
$parameter->getNativeType()->getReferencedClasses(),
$parameter->getPhpDocType()->getReferencedClasses()
);
}
foreach ($referencedClasses as $class) {
if (!$this->broker->hasClass($class)) {
$errors[] = sprintf($parameterMessage, $parameter->getName(), $class);
}
}
if ($this->checkClassCaseSensitivity) {
$errors = array_merge(
$errors,
$this->classCaseSensitivityCheck->checkClassNames($referencedClasses)
);
}
if ($parameter->getType() instanceof NonexistentParentClassType) {
$errors[] = sprintf($parameterMessage, $parameter->getName(), $parameter->getType()->describe());
}
}
if ($this->checkThisOnly) {
$returnTypeReferencedClasses = $parametersAcceptor->getReturnType()->getReferencedClasses();
} else {
$returnTypeReferencedClasses = array_merge(
$parametersAcceptor->getNativeReturnType()->getReferencedClasses(),
$parametersAcceptor->getPhpDocReturnType()->getReferencedClasses()
);
}
foreach ($returnTypeReferencedClasses as $class) {
if (!$this->broker->hasClass($class)) {
$errors[] = sprintf($returnMessage, $class);
}
}
if ($this->checkClassCaseSensitivity) {
$errors = array_merge(
$errors,
$this->classCaseSensitivityCheck->checkClassNames($returnTypeReferencedClasses)
);
}
if ($parametersAcceptor->getReturnType() instanceof NonexistentParentClassType) {
$errors[] = sprintf($returnMessage, $parametersAcceptor->getReturnType()->describe());
}
return $errors;
}
}
src/Rules/Properties/AccessPropertiesOnPossiblyNullRule.php 0000666 00000002503 13436751442 0020272 0 ustar 00 ruleLevelHelper = $ruleLevelHelper;
$this->checkThisOnly = $checkThisOnly;
}
public function getNodeType(): string
{
return PropertyFetch::class;
}
/**
* @param \PhpParser\Node\Expr\PropertyFetch $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (!is_string($node->name)) {
return [];
}
if ($this->checkThisOnly && !$this->ruleLevelHelper->isThis($node->var)) {
return [];
}
$type = $scope->getType($node->var);
if (!$type instanceof UnionType) {
return [];
}
if (\PHPStan\Type\TypeCombinator::containsNull($type)) {
return [
sprintf(
'Accessing property $%s on possibly null value of type %s.',
$node->name,
$type->describe()
),
];
}
return [];
}
}
src/Rules/Properties/PropertyReflectionFinder.php 0000666 00000003235 13436751442 0016301 0 ustar 00 name)) {
return null;
}
$propertyHolderType = $scope->getType($propertyFetch->var);
return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name, $scope);
} elseif ($propertyFetch instanceof \PhpParser\Node\Expr\StaticPropertyFetch) {
if (!is_string($propertyFetch->name)) {
return null;
}
if ($propertyFetch->class instanceof \PhpParser\Node\Name) {
$propertyHolderType = new ObjectType($scope->resolveName($propertyFetch->class));
} else {
$propertyHolderType = $scope->getType($propertyFetch->class);
}
return $this->findPropertyReflection($propertyHolderType, $propertyFetch->name, $scope);
}
return null;
}
/**
* @param \PHPStan\Type\Type $propertyHolderType
* @param string $propertyName
* @param Scope $scope
* @return \PHPStan\Reflection\PropertyReflection|null
*/
private function findPropertyReflection(Type $propertyHolderType, string $propertyName, Scope $scope)
{
if (!$propertyHolderType->hasProperty($propertyName)) {
return null;
}
return $propertyHolderType->getProperty($propertyName, $scope);
}
}
src/Rules/Properties/ExistingClassesInPropertiesRule.php 0000666 00000003416 13436751442 0017617 0 ustar 00 broker = $broker;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
$this->checkClassCaseSensitivity = $checkClassCaseSensitivity;
}
public function getNodeType(): string
{
return PropertyProperty::class;
}
/**
* @param \PhpParser\Node\Stmt\PropertyProperty $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
$propertyReflection = $scope->getClassReflection()->getNativeProperty($node->name);
$propertyType = $propertyReflection->getType();
$errors = [];
foreach ($propertyType->getReferencedClasses() as $referencedClass) {
if ($this->broker->hasClass($referencedClass)) {
continue;
}
$errors[] = sprintf(
'Property %s::$%s has unknown class %s as its type.',
$propertyReflection->getDeclaringClass()->getDisplayName(),
$node->name,
$referencedClass
);
}
if ($this->checkClassCaseSensitivity) {
$errors = array_merge(
$errors,
$this->classCaseSensitivityCheck->checkClassNames($propertyType->getReferencedClasses())
);
}
return $errors;
}
}
src/Rules/Properties/ReadingWriteOnlyPropertiesRule.php 0000666 00000004003 13436751442 0017437 0 ustar 00 propertyDescriptor = $propertyDescriptor;
$this->propertyReflectionFinder = $propertyReflectionFinder;
$this->ruleLevelHelper = $ruleLevelHelper;
$this->checkThisOnly = $checkThisOnly;
}
public function getNodeType(): string
{
return \PhpParser\Node::class;
}
/**
* @param \PhpParser\Node $node
* @param Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (
!($node instanceof Node\Expr\PropertyFetch)
&& !($node instanceof Node\Expr\StaticPropertyFetch)
) {
return [];
}
if (
$node instanceof Node\Expr\PropertyFetch
&& $this->checkThisOnly
&& !$this->ruleLevelHelper->isThis($node->var)
) {
return [];
}
if ($scope->isInExpressionAssign($node)) {
return [];
}
$propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node, $scope);
if ($propertyReflection === null) {
return [];
}
if (!$scope->canAccessProperty($propertyReflection)) {
return [];
}
if (!$propertyReflection->isReadable()) {
$propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $node);
if ($propertyDescription === null) {
return [];
}
return [
sprintf(
'%s is not readable.',
$propertyDescription
),
];
}
return [];
}
}
src/Rules/Properties/AccessStaticPropertiesRule.php 0000666 00000011027 13436751442 0016566 0 ustar 00 broker = $broker;
$this->ruleLevelHelper = $ruleLevelHelper;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
}
public function getNodeType(): string
{
return StaticPropertyFetch::class;
}
/**
* @param \PhpParser\Node\Expr\StaticPropertyFetch $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (!is_string($node->name)) {
return [];
}
$name = $node->name;
$messages = [];
if ($node->class instanceof Name) {
$class = (string) $node->class;
if ($class === 'self' || $class === 'static') {
if (!$scope->isInClass()) {
return [
sprintf(
'Accessing %s::$%s outside of class scope.',
$class,
$name
),
];
}
$className = $scope->getClassReflection()->getName();
} elseif ($class === 'parent') {
if (!$scope->isInClass()) {
return [
sprintf(
'Accessing %s::$%s outside of class scope.',
$class,
$name
),
];
}
if ($scope->getClassReflection()->getParentClass() === false) {
return [
sprintf(
'%s::%s() accesses parent::$%s but %s does not extend any class.',
$scope->getClassReflection()->getDisplayName(),
$scope->getFunctionName(),
$name,
$scope->getClassReflection()->getDisplayName()
),
];
}
if ($scope->getFunctionName() === null) {
throw new \PHPStan\ShouldNotHappenException();
}
$currentMethodReflection = $scope->getClassReflection()->getNativeMethod($scope->getFunctionName());
if (!$currentMethodReflection->isStatic()) {
// calling parent::method() from instance method
return [];
}
$className = $scope->getClassReflection()->getParentClass()->getName();
} else {
if (!$this->broker->hasClass($class)) {
return [
sprintf(
'Access to static property $%s on an unknown class %s.',
$name,
$class
),
];
} else {
$messages = $this->classCaseSensitivityCheck->checkClassNames([$class]);
}
$className = $this->broker->getClass($class)->getName();
}
$classType = new ObjectType($className);
} else {
$classTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$node->class,
sprintf('Access to static property $%s on an unknown class %%s.', $name)
);
$classType = $classTypeResult->getType();
if ($classType instanceof ErrorType) {
return $classTypeResult->getUnknownClassErrors();
}
}
if ($classType instanceof StringType) {
return [];
}
$typeForDescribe = $classType;
$classType = TypeCombinator::remove($classType, new StringType());
if (!$classType->canAccessProperties()) {
return array_merge($messages, [
sprintf('Cannot access static property $%s on %s.', $name, $typeForDescribe->describe()),
]);
}
if (!$classType->hasProperty($name)) {
if ($scope->isSpecified($node)) {
return $messages;
}
return array_merge($messages, [
sprintf(
'Access to an undefined static property %s::$%s.',
$typeForDescribe->describe(),
$name
),
]);
}
$property = $classType->getProperty($name, $scope);
if (!$property->isStatic()) {
return array_merge($messages, [
sprintf(
'Static access to instance property %s::$%s.',
$property->getDeclaringClass()->getDisplayName(),
$name
),
]);
}
if (!$scope->canAccessProperty($property)) {
return array_merge($messages, [
sprintf(
'Access to %s property $%s of class %s.',
$property->isPrivate() ? 'private' : 'protected',
$name,
$property->getDeclaringClass()->getDisplayName()
),
]);
}
return $messages;
}
}
src/Rules/Properties/WritingToReadOnlyPropertiesRule.php 0000666 00000004405 13436751442 0017603 0 ustar 00 ruleLevelHelper = $ruleLevelHelper;
$this->propertyDescriptor = $propertyDescriptor;
$this->propertyReflectionFinder = $propertyReflectionFinder;
$this->checkThisOnly = $checkThisOnly;
}
public function getNodeType(): string
{
return \PhpParser\Node::class;
}
/**
* @param \PhpParser\Node $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (
!$node instanceof Node\Expr\Assign
&& !$node instanceof Node\Expr\AssignOp
) {
return [];
}
if (
!($node->var instanceof Node\Expr\PropertyFetch)
&& !($node->var instanceof Node\Expr\StaticPropertyFetch)
) {
return [];
}
if (
$node->var instanceof Node\Expr\PropertyFetch
&& $this->checkThisOnly
&& !$this->ruleLevelHelper->isThis($node->var->var)
) {
return [];
}
/** @var \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch */
$propertyFetch = $node->var;
$propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $scope);
if ($propertyReflection === null) {
return [];
}
if (!$scope->canAccessProperty($propertyReflection)) {
return [];
}
if (!$propertyReflection->isWritable()) {
$propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $propertyFetch);
if ($propertyDescription === null) {
return [];
}
return [
sprintf(
'%s is not writable.',
$propertyDescription
),
];
}
return [];
}
}
src/Rules/Properties/TypesAssignedToPropertiesRule.php 0000666 00000004351 13436751442 0017304 0 ustar 00 ruleLevelHelper = $ruleLevelHelper;
$this->propertyDescriptor = $propertyDescriptor;
$this->propertyReflectionFinder = $propertyReflectionFinder;
}
public function getNodeType(): string
{
return \PhpParser\Node::class;
}
/**
* @param \PhpParser\Node $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (
!$node instanceof Node\Expr\Assign
&& !$node instanceof Node\Expr\AssignOp
) {
return [];
}
if (
!($node->var instanceof Node\Expr\PropertyFetch)
&& !($node->var instanceof Node\Expr\StaticPropertyFetch)
) {
return [];
}
/** @var \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch */
$propertyFetch = $node->var;
$propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($propertyFetch, $scope);
if ($propertyReflection === null) {
return [];
}
$propertyType = $propertyReflection->getType();
if ($node instanceof Node\Expr\Assign) {
$assignedValueType = $scope->getType($node->expr);
} else {
$assignedValueType = $scope->getType($node);
}
if (!$this->ruleLevelHelper->accepts($propertyType, $assignedValueType)) {
$propertyDescription = $this->propertyDescriptor->describeProperty($propertyReflection, $propertyFetch);
if ($propertyDescription === null) {
return [];
}
return [
sprintf(
'%s (%s) does not accept %s.',
$propertyDescription,
$propertyType->describe(),
$assignedValueType->describe()
),
];
}
return [];
}
}
src/Rules/Properties/PropertyDescriptor.php 0000666 00000001475 13436751442 0015201 0 ustar 00 getDeclaringClass()->getDisplayName(), $propertyFetch->name);
} elseif ($propertyFetch instanceof \PhpParser\Node\Expr\StaticPropertyFetch) {
return sprintf('Static property %s::$%s', $property->getDeclaringClass()->getDisplayName(), $propertyFetch->name);
}
return null;
}
}
src/Rules/Properties/DefaultValueTypesAssignedToPropertiesRule.php 0000666 00000003102 13436751442 0021577 0 ustar 00 ruleLevelHelper = $ruleLevelHelper;
}
public function getNodeType(): string
{
return Property::class;
}
/**
* @param \PhpParser\Node\Stmt\Property $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
$classReflection = $scope->getClassReflection();
$errors = [];
foreach ($node->props as $property) {
if ($property->default === null) {
continue;
}
if ($property->default instanceof Node\Expr\ConstFetch && (string) $property->default->name === 'null') {
continue;
}
$propertyReflection = $classReflection->getNativeProperty($property->name);
$propertyType = $propertyReflection->getType();
$defaultValueType = $scope->getType($property->default);
if ($this->ruleLevelHelper->accepts($propertyType, $defaultValueType)) {
continue;
}
$errors[] = sprintf(
'%s %s::$%s (%s) does not accept default value of type %s.',
$node->isStatic() ? 'Static property' : 'Property',
$classReflection->getDisplayName(),
$property->name,
$propertyType->describe(),
$defaultValueType->describe()
);
}
return $errors;
}
}
src/Rules/Properties/AccessPropertiesRule.php 0000666 00000004755 13436751442 0015430 0 ustar 00 broker = $broker;
$this->ruleLevelHelper = $ruleLevelHelper;
}
public function getNodeType(): string
{
return PropertyFetch::class;
}
/**
* @param \PhpParser\Node\Expr\PropertyFetch $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(\PhpParser\Node $node, Scope $scope): array
{
if (!is_string($node->name)) {
return [];
}
$name = $node->name;
$typeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$node->var,
sprintf('Access to property $%s on an unknown class %%s.', $name)
);
$type = $typeResult->getType();
if ($type instanceof ErrorType) {
return $typeResult->getUnknownClassErrors();
}
if (!$type->canAccessProperties()) {
return [
sprintf('Cannot access property $%s on %s.', $name, $type->describe()),
];
}
if (!$type->hasProperty($name)) {
if ($scope->isSpecified($node)) {
return [];
}
if (count($typeResult->getReferencedClasses()) === 1) {
$referencedClass = $typeResult->getReferencedClasses()[0];
$propertyClassReflection = $this->broker->getClass($referencedClass);
$parentClassReflection = $propertyClassReflection->getParentClass();
while ($parentClassReflection !== false) {
if ($parentClassReflection->hasProperty($name)) {
return [
sprintf(
'Access to private property $%s of parent class %s.',
$name,
$parentClassReflection->getDisplayName()
),
];
}
$parentClassReflection = $parentClassReflection->getParentClass();
}
}
return [
sprintf(
'Access to an undefined property %s::$%s.',
$type->describe(),
$name
),
];
}
$propertyReflection = $type->getProperty($name, $scope);
if (!$scope->canAccessProperty($propertyReflection)) {
return [
sprintf(
'Access to %s property %s::$%s.',
$propertyReflection->isPrivate() ? 'private' : 'protected',
$type->describe(),
$name
),
];
}
return [];
}
}
src/Rules/Functions/CallToNonExistentFunctionRule.php 0000666 00000002407 13436751442 0017041 0 ustar 00 broker = $broker;
}
public function getNodeType(): string
{
return FuncCall::class;
}
/**
* @param \PhpParser\Node\Expr\FuncCall $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (!($node->name instanceof \PhpParser\Node\Name)) {
return [];
}
if (strpos((string) $node->name, 'apache_') === 0 || strpos((string) $node->name, 'fastcgi_') === 0) {
return [];
}
if (!$this->broker->hasFunction($node->name, $scope)) {
return [sprintf('Function %s not found.', (string) $node->name)];
}
$function = $this->broker->getFunction($node->name, $scope);
$name = (string) $node->name;
if ($function->getName() !== $this->broker->resolveFunctionName($node->name, $scope)) {
return [sprintf('Call to function %s() with incorrect case: %s', $function->getName(), $name)];
}
return [];
}
}
src/Rules/Functions/ReturnTypeRule.php 0000666 00000003424 13436751442 0014077 0 ustar 00 returnTypeCheck = $returnTypeCheck;
}
public function getNodeType(): string
{
return Return_::class;
}
/**
* @param \PhpParser\Node\Stmt\Return_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if ($scope->getFunction() === null) {
return [];
}
if ($scope->isInAnonymousFunction()) {
return [];
}
$function = $scope->getFunction();
if (
!($function instanceof PhpFunctionFromParserNodeReflection)
|| $function instanceof PhpMethodFromParserNodeReflection
) {
return [];
}
$reflection = null;
if (function_exists($function->getName())) {
$reflection = new \ReflectionFunction($function->getName());
}
return $this->returnTypeCheck->checkReturnType(
$scope,
$function->getReturnType(),
$node->expr,
sprintf(
'Function %s() should return %%s but empty return statement found.',
$function->getName()
),
sprintf(
'Function %s() with return type void returns %%s but should not return anything.',
$function->getName()
),
sprintf(
'Function %s() should return %%s but returns %%s.',
$function->getName()
),
$reflection !== null && $reflection->isGenerator(),
false
);
}
}
src/Rules/Functions/CallToFunctionParametersRule.php 0000666 00000003636 13436751442 0016673 0 ustar 00 broker = $broker;
$this->check = $check;
}
public function getNodeType(): string
{
return FuncCall::class;
}
/**
* @param \PhpParser\Node\Expr\FuncCall $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (!($node->name instanceof \PhpParser\Node\Name)) {
return [];
}
if (!$this->broker->hasFunction($node->name, $scope)) {
return [];
}
$function = $this->broker->getFunction($node->name, $scope);
return $this->check->check(
$function,
$scope,
$node,
[
'Function ' . $function->getName() . ' invoked with %d parameter, %d required.',
'Function ' . $function->getName() . ' invoked with %d parameters, %d required.',
'Function ' . $function->getName() . ' invoked with %d parameter, at least %d required.',
'Function ' . $function->getName() . ' invoked with %d parameters, at least %d required.',
'Function ' . $function->getName() . ' invoked with %d parameter, %d-%d required.',
'Function ' . $function->getName() . ' invoked with %d parameters, %d-%d required.',
'Parameter #%d %s of function ' . $function->getName() . ' expects %s, %s given.',
'Result of function ' . $function->getName() . ' (void) is used.',
'Parameter #%d %s of function ' . $function->getName() . ' is passed by reference, so it expects variables only.',
]
);
}
}
src/Rules/Functions/ClosureReturnTypeRule.php 0000666 00000002511 13436751442 0015430 0 ustar 00 returnTypeCheck = $returnTypeCheck;
}
public function getNodeType(): string
{
return Return_::class;
}
/**
* @param \PhpParser\Node\Stmt\Return_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$scope->isInAnonymousFunction()) {
return [];
}
/** @var \PHPStan\Type\Type $returnType */
$returnType = $scope->getAnonymousFunctionReturnType();
$generatorType = new ObjectType(\Generator::class);
return $this->returnTypeCheck->checkReturnType(
$scope,
$returnType,
$node->expr,
'Anonymous function should return %s but empty return statement found.',
'Anonymous function with return type void returns %s but should not return anything.',
'Anonymous function should return %s but returns %s.',
$generatorType->isSuperTypeOf($returnType)->yes(),
true
);
}
}
src/Rules/Functions/PrintfParametersRule.php 0000666 00000005472 13436751442 0015251 0 ustar 00 name instanceof \PhpParser\Node\Name)) {
return [];
}
$functionsArgumentPositions = [
'printf' => 0,
'sprintf' => 0,
'sscanf' => 1,
'fscanf' => 1,
];
$minimumNumberOfArguments = [
'printf' => 1,
'sprintf' => 1,
'sscanf' => 3,
'fscanf' => 3,
];
$name = strtolower((string) $node->name);
if (!isset($functionsArgumentPositions[$name])) {
return [];
}
$formatArgumentPosition = $functionsArgumentPositions[$name];
$args = $node->args;
$argsCount = count($args);
if ($argsCount < $minimumNumberOfArguments[$name]) {
return []; // caught by CallToFunctionParametersRule
}
$formatArg = $args[$formatArgumentPosition]->value;
if (!($formatArg instanceof String_)) {
return []; // inspect only literal string format
}
foreach ($node->args as $arg) {
if ($arg->unpack) {
return [];
}
}
$format = $formatArg->value;
$placeHoldersCount = $this->getPlaceholdersCount($name, $format);
$argsCount -= $formatArgumentPosition;
if ($argsCount !== $placeHoldersCount + 1) {
return [
sprintf(
sprintf(
'%s, %s.',
$placeHoldersCount === 1 ? 'Call to %s contains %d placeholder' : 'Call to %s contains %d placeholders',
$argsCount - 1 === 1 ? '%d value given' : '%d values given'
),
$name,
$placeHoldersCount,
$argsCount - 1
),
];
}
return [];
}
private function getPlaceholdersCount(string $functionName, string $format): int
{
$specifiers = in_array($functionName, ['sprintf', 'printf'], true) ? '[bcdeEfFgGosuxX]' : '(?:[cdDeEfinosuxX]|\[[^\]]+\])';
$pattern = '~(?%*)%(?:(?\d+)\$)?[-+]?(?:[ 0]|(?:\'[^%]))?-?\d*(?:\.\d*)?' . $specifiers . '~';
if (!preg_match_all($pattern, $format, $matches, PREG_SET_ORDER)) {
return 0;
}
$placeholders = array_filter($matches, function (array $match): bool {
return strlen($match['before']) % 2 === 0;
});
if (count($placeholders) === 0) {
return 0;
}
$maxPositionedNumber = 0;
$maxOrdinaryNumber = 0;
foreach ($placeholders as $placeholder) {
if (isset($placeholder['position']) && $placeholder['position'] !== '') {
$maxPositionedNumber = max((int) $placeholder['position'], $maxPositionedNumber);
} else {
$maxOrdinaryNumber++;
}
}
return max($maxPositionedNumber, $maxOrdinaryNumber);
}
}
src/Rules/Functions/CallToCountOnlyWithArrayOrCountableRule.php 0000666 00000002673 13436751442 0021005 0 ustar 00 ruleLevelHelper = $ruleLevelHelper;
}
public function getNodeType(): string
{
return Node\Expr\FuncCall::class;
}
/**
* @param \PhpParser\Node\Expr\FuncCall $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$node->name instanceof Node\Name) {
return [];
}
$functionName = strtolower((string) $node->name);
if ($functionName !== 'count') {
return [];
}
if (!isset($node->args[0])) {
return [];
}
$argumentType = $scope->getType($node->args[0]->value);
$requiredType = new UnionType([
new ArrayType(new MixedType(), new MixedType()),
new ObjectType(\Countable::class),
]);
if (!$this->ruleLevelHelper->accepts($requiredType, $argumentType)) {
return [
sprintf(
'Call to function count() with argument type %s will always result in number 1.',
$argumentType->describe()
),
];
}
return [];
}
}
src/Rules/Functions/InnerFunctionRule.php 0000666 00000001421 13436751442 0014532 0 ustar 00 getFunction() === null) {
return [];
}
return [
'Inner named functions are not supported by PHPStan. Consider refactoring to an anonymous function, class method, or a top-level-defined function. See issue #165 (https://github.com/phpstan/phpstan/issues/165) for more details.',
];
}
}
src/Rules/Functions/ExistingClassesInTypehintsRule.php 0000666 00000001723 13436751442 0017265 0 ustar 00 check = $check;
}
public function getNodeType(): string
{
return Function_::class;
}
/**
* @param \PhpParser\Node\Stmt\Function_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
return $this->check->checkFunction(
$node,
$scope,
sprintf(
'Parameter $%%s of function %s() has invalid typehint type %%s.',
$node->namespacedName
),
sprintf(
'Return typehint of function %s() has invalid type %%s.',
$node->namespacedName
)
);
}
}
src/Rules/Functions/UnusedClosureUsesRule.php 0000666 00000001660 13436751442 0015416 0 ustar 00 check = $check;
}
public function getNodeType(): string
{
return Node\Expr\Closure::class;
}
/**
* @param \PhpParser\Node\Expr\Closure $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (count($node->uses) === 0) {
return [];
}
return $this->check->getUnusedParameters(
array_map(function (Node\Expr\ClosureUse $use): string {
return $use->var;
}, $node->uses),
$node->stmts,
'Anonymous function has an unused use $%s.'
);
}
}
src/Rules/Functions/NonExistentDefinedFunctionRule.php 0000666 00000002070 13436751442 0017215 0 ustar 00 broker = $broker;
}
public function getNodeType(): string
{
return Function_::class;
}
/**
* @param \PhpParser\Node\Stmt\Function_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
$functionName = $node->name;
if (isset($node->namespacedName)) {
$functionName = (string) $node->namespacedName;
}
$functionNameName = new Name($functionName);
if ($this->broker->hasFunction($functionNameName)) {
return [];
}
return [
sprintf(
'Function %s not found while trying to analyse it - autoloading is probably not configured properly.',
$functionName
),
];
}
}
src/Rules/Functions/ExistingClassesInClosureTypehintsRule.php 0000666 00000001601 13436751442 0020615 0 ustar 00 check = $check;
}
public function getNodeType(): string
{
return Closure::class;
}
/**
* @param \PhpParser\Node\Expr\Closure $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
return $this->check->checkFunction(
$node,
$scope,
'Parameter $%s of anonymous function has invalid typehint type %s.',
'Return typehint of anonymous function has invalid type %s.'
);
}
}
src/Rules/Constants/ConstantRule.php 0000666 00000001423 13436751442 0013550 0 ustar 00 broker = $broker;
}
public function getNodeType(): string
{
return Node\Expr\ConstFetch::class;
}
/**
* @param \PhpParser\Node\Expr\ConstFetch $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (!$this->broker->hasConstant($node->name, $scope)) {
return [
sprintf(
'Constant %s not found.',
(string) $node->name
),
];
}
return [];
}
}
src/Rules/RegistryFactory.php 0000666 00000001130 13436751442 0012306 0 ustar 00 container = $container;
}
public function create(): Registry
{
$tagToService = function (array $tags) {
return array_map(function (string $serviceName) {
return $this->container->getService($serviceName);
}, array_keys($tags));
};
return new Registry(
$tagToService($this->container->findByTag(self::RULE_TAG))
);
}
}
src/Rules/FoundTypeResult.php 0000666 00000001637 13436751442 0012276 0 ustar 00 type = $type;
$this->referencedClasses = $referencedClasses;
$this->unknownClassErrors = $unknownClassErrors;
}
public function getType(): Type
{
return $this->type;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return $this->referencedClasses;
}
/**
* @return string[]
*/
public function getUnknownClassErrors(): array
{
return $this->unknownClassErrors;
}
}
src/Rules/Registry.php 0000666 00000001610 13436751442 0010761 0 ustar 00 rules[$rule->getNodeType()][] = $rule;
}
}
/**
* @param string $nodeType
* @return \PHPStan\Rules\Rule[]
*/
public function getRules(string $nodeType): array
{
if (!isset($this->cache[$nodeType])) {
$nodeTypes = [$nodeType] + class_parents($nodeType) + class_implements($nodeType);
$rules = [];
foreach ($nodeTypes as $nodeType) {
foreach ($this->rules[$nodeType] ?? [] as $rule) {
$rules[get_class($rule)] = $rule;
}
}
$this->cache[$nodeType] = array_values($rules);
}
return $this->cache[$nodeType];
}
}
src/Rules/Variables/DefinedVariableRule.php 0000666 00000003541 13436751442 0014722 0 ustar 00 cliArgumentsVariablesRegistered = $cliArgumentsVariablesRegistered;
$this->checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables;
}
public function getNodeType(): string
{
return Variable::class;
}
public static function isGlobalVariable(string $variableName): bool
{
return in_array($variableName, [
'GLOBALS',
'_SERVER',
'_GET',
'_POST',
'_FILES',
'_COOKIE',
'_SESSION',
'_REQUEST',
'_ENV',
], true);
}
/**
* @param \PhpParser\Node\Expr\Variable $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (!is_string($node->name)) {
return [];
}
if (self::isGlobalVariable($node->name)) {
return [];
}
if ($this->cliArgumentsVariablesRegistered && in_array($node->name, [
'argc',
'argv',
], true)) {
$isInMain = !$scope->isInClass() && !$scope->isInAnonymousFunction() && $scope->getFunction() === null;
if ($isInMain) {
return [];
}
}
if ($scope->isInExpressionAssign($node)) {
return [];
}
if ($scope->hasVariableType($node->name)->no()) {
return [
sprintf('Undefined variable: $%s', $node->name),
];
} elseif (
$this->checkMaybeUndefinedVariables
&& !$scope->hasVariableType($node->name)->yes()
) {
return [
sprintf('Variable $%s might not be defined.', $node->name),
];
}
return [];
}
}
src/Rules/Variables/ThisVariableRule.php 0000666 00000002100 13436751442 0014261 0 ustar 00 name) || $node->name !== 'this') {
return [];
}
if ($scope->isInClosureBind()) {
return [];
}
if (!$scope->isInClass()) {
return [
'Using $this outside a class.',
];
}
$function = $scope->getFunction();
if (!$function instanceof MethodReflection) {
throw new \PHPStan\ShouldNotHappenException();
}
if ($function->isStatic()) {
return [
sprintf(
'Using $this in static method %s::%s().',
$scope->getClassReflection()->getDisplayName(),
$function->getName()
),
];
}
return [];
}
}
src/Rules/Variables/DefinedVariableInAnonymousFunctionUseRule.php 0000666 00000002076 13436751442 0021307 0 ustar 00 checkMaybeUndefinedVariables = $checkMaybeUndefinedVariables;
}
public function getNodeType(): string
{
return ClosureUse::class;
}
/**
* @param \PhpParser\Node\Expr\ClosureUse $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if ($node->byRef) {
return [];
}
if ($scope->hasVariableType($node->var)->no()) {
return [
sprintf('Undefined variable: $%s', $node->var),
];
} elseif (
$this->checkMaybeUndefinedVariables
&& !$scope->hasVariableType($node->var)->yes()
) {
return [
sprintf('Variable $%s might not be defined.', $node->var),
];
}
return [];
}
}
src/Rules/Variables/VariableCloningRule.php 0000666 00000002174 13436751442 0014756 0 ustar 00 checkNullables = $checkNullables;
}
public function getNodeType(): string
{
return Clone_::class;
}
/**
* @param \PhpParser\Node\Expr\Clone_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
$type = $scope->getType($node->expr);
if (!$this->checkNullables && !$type instanceof NullType) {
$type = \PHPStan\Type\TypeCombinator::removeNull($type);
}
if ($type->isClonable()) {
return [];
}
if ($node->expr instanceof Variable) {
return [
sprintf(
'Cannot clone non-object variable $%s of type %s.',
$node->expr->name,
$type->describe()
),
];
}
return [
sprintf('Cannot clone %s.', $type->describe()),
];
}
}
src/Rules/Variables/VariableCertaintyInIssetRule.php 0000666 00000002747 13436751442 0016634 0 ustar 00 vars as $var) {
$isSubNode = false;
while (
$var instanceof Node\Expr\ArrayDimFetch
|| $var instanceof Node\Expr\PropertyFetch
) {
$var = $var->var;
$isSubNode = true;
}
while (
$var instanceof Node\Expr\StaticPropertyFetch
&& $var->class instanceof Node\Expr
) {
$var = $var->class;
$isSubNode = true;
}
if (!$var instanceof Node\Expr\Variable || !is_string($var->name)) {
continue;
}
if (DefinedVariableRule::isGlobalVariable($var->name)) {
continue;
}
$certainty = $scope->hasVariableType($var->name);
if ($certainty->no()) {
$messages[] = sprintf('Variable $%s in isset() is never defined.', $var->name);
} elseif ($certainty->yes() && !$isSubNode) {
$variableType = $scope->getVariableType($var->name);
if (!$variableType->accepts(new NullType())) {
$messages[] = sprintf('Variable $%s in isset() always exists and is not nullable.', $var->name);
}
}
}
return $messages;
}
}
src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php 0000666 00000007112 13436751442 0015527 0 ustar 00 fileTypeMapper = $fileTypeMapper;
}
public function getNodeType(): string
{
return \PhpParser\Node\FunctionLike::class;
}
/**
* @param \PhpParser\Node\FunctionLike $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
$docComment = $node->getDocComment();
if ($docComment === null) {
return [];
}
$resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
$scope->getFile(),
$scope->isInClass() ? $scope->getClassReflection()->getName() : null,
$docComment->getText()
);
$nativeParameterTypes = $this->getNativeParameterTypes($node, $scope);
$nativeReturnType = $this->getNativeReturnType($node, $scope);
$errors = [];
foreach ($resolvedPhpDoc->getParamTags() as $parameterName => $phpDocParamTag) {
$phpDocParamType = $phpDocParamTag->getType();
if (!isset($nativeParameterTypes[$parameterName])) {
$errors[] = sprintf(
'PHPDoc tag @param references unknown parameter $%s',
$parameterName
);
} else {
$nativeParamType = $nativeParameterTypes[$parameterName];
$isParamSuperType = $nativeParamType->isSuperTypeOf($phpDocParamType);
if (
$phpDocParamTag->isVariadic()
&& $nativeParamType instanceof ArrayType
&& $nativeParamType->getItemType() instanceof ArrayType
) {
continue;
}
if ($isParamSuperType->no()) {
$errors[] = sprintf(
'PHPDoc tag @param for parameter $%s with type %s is incompatible with native type %s',
$parameterName,
$phpDocParamType->describe(),
$nativeParamType->describe()
);
} elseif ($isParamSuperType->maybe()) {
$errors[] = sprintf(
'PHPDoc tag @param for parameter $%s with type %s is not subtype of native type %s',
$parameterName,
$phpDocParamType->describe(),
$nativeParamType->describe()
);
}
}
}
if ($resolvedPhpDoc->getReturnTag() !== null) {
$phpDocReturnType = $resolvedPhpDoc->getReturnTag()->getType();
$isReturnSuperType = $nativeReturnType->isSuperTypeOf($phpDocReturnType);
if ($isReturnSuperType->no()) {
$errors[] = sprintf(
'PHPDoc tag @return with type %s is incompatible with native type %s',
$phpDocReturnType->describe(),
$nativeReturnType->describe()
);
} elseif ($isReturnSuperType->maybe()) {
$errors[] = sprintf(
'PHPDoc tag @return with type %s is not subtype of native type %s',
$phpDocReturnType->describe(),
$nativeReturnType->describe()
);
}
}
return $errors;
}
/**
* @param Node\FunctionLike $node
* @param Scope $scope
* @return Type[]
*/
private function getNativeParameterTypes(\PhpParser\Node\FunctionLike $node, Scope $scope): array
{
$nativeParameterTypes = [];
foreach ($node->getParams() as $parameter) {
$isNullable = $scope->isParameterValueNullable($parameter);
$nativeParameterTypes[$parameter->name] = $scope->getFunctionType(
$parameter->type,
$isNullable,
$parameter->variadic
);
}
return $nativeParameterTypes;
}
private function getNativeReturnType(\PhpParser\Node\FunctionLike $node, Scope $scope): Type
{
return $scope->getFunctionType($node->getReturnType(), false, false);
}
}
src/Rules/PhpDoc/InvalidPhpDocTagValueRule.php 0000666 00000003344 13436751442 0015301 0 ustar 00 phpDocLexer = $phpDocLexer;
$this->phpDocParser = $phpDocParser;
}
public function getNodeType(): string
{
return \PhpParser\Node::class;
}
/**
* @param \PhpParser\Node $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (
!$node instanceof Node\Stmt\ClassLike
&& !$node instanceof Node\FunctionLike
&& !$node instanceof Node\Stmt\Foreach_
&& !$node instanceof Node\Stmt\Property
&& !$node instanceof Node\Expr\Assign
&& !$node instanceof Node\Expr\AssignRef
) {
return [];
}
$docComment = $node->getDocComment();
if ($docComment === null) {
return [];
}
$phpDocString = $docComment->getText();
$tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString));
$phpDocNode = $this->phpDocParser->parse($tokens);
$errors = [];
foreach ($phpDocNode->getTags() as $phpDocTag) {
if ($phpDocTag->value instanceof InvalidTagValueNode) {
$errors[] = sprintf(
'PHPDoc tag %s has invalid value (%s): %s',
$phpDocTag->name,
$phpDocTag->value->value,
$phpDocTag->value->exception->getMessage()
);
}
}
return $errors;
}
}
src/Rules/Namespaces/ExistingNamesInGroupUseRule.php 0000666 00000005354 13436751442 0016630 0 ustar 00 broker = $broker;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
}
public function getNodeType(): string
{
return \PhpParser\Node\Stmt\GroupUse::class;
}
/**
* @param \PhpParser\Node\Stmt\GroupUse $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
$messages = [];
foreach ($node->uses as $use) {
$message = null;
/** @var Node\Name $name */
$name = Node\Name::concat($node->prefix, $use->name);
if (
$node->type === Use_::TYPE_CONSTANT
|| $use->type === Use_::TYPE_CONSTANT
) {
$message = $this->checkConstant($name);
} elseif (
$node->type === Use_::TYPE_FUNCTION
|| $use->type === Use_::TYPE_FUNCTION
) {
$message = $this->checkFunction($name);
} elseif ($use->type === Use_::TYPE_NORMAL) {
$message = $this->checkClass($name);
} else {
throw new \PHPStan\ShouldNotHappenException();
}
if ($message !== null) {
$messages[] = $message;
}
}
return $messages;
}
/**
* @param \PhpParser\Node\Name $name
* @return string|null
*/
private function checkConstant(Node\Name $name)
{
if (!$this->broker->hasConstant($name)) {
return sprintf('Used constant %s not found.', (string) $name);
}
return null;
}
/**
* @param \PhpParser\Node\Name $name
* @return string|null
*/
private function checkFunction(Node\Name $name)
{
if (!$this->broker->hasFunction($name)) {
return sprintf('Used function %s not found.', (string) $name);
}
$functionReflection = $this->broker->getFunction($name);
$realName = $functionReflection->getName();
$usedName = (string) $name;
if ($realName !== $usedName) {
return sprintf(
'Function %s used with incorrect case: %s.',
$realName,
$usedName
);
}
return null;
}
/**
* @param \PhpParser\Node\Name $name
* @return string|null
*/
private function checkClass(Node\Name $name)
{
$messages = $this->classCaseSensitivityCheck->checkClassNames([(string) $name]);
if (count($messages) === 0) {
return null;
} elseif (count($messages) === 1) {
return $messages[0];
}
throw new \PHPStan\ShouldNotHappenException();
}
}
src/Rules/Namespaces/ExistingNamesInUseRule.php 0000666 00000005253 13436751442 0015611 0 ustar 00 broker = $broker;
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
}
public function getNodeType(): string
{
return \PhpParser\Node\Stmt\Use_::class;
}
/**
* @param \PhpParser\Node\Stmt\Use_ $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
if ($node->type === Node\Stmt\Use_::TYPE_UNKNOWN) {
throw new \PHPStan\ShouldNotHappenException();
}
foreach ($node->uses as $use) {
if ($use->type !== Node\Stmt\Use_::TYPE_UNKNOWN) {
throw new \PHPStan\ShouldNotHappenException();
}
}
if ($node->type === Node\Stmt\Use_::TYPE_CONSTANT) {
return $this->checkConstants($node->uses);
}
if ($node->type === Node\Stmt\Use_::TYPE_FUNCTION) {
return $this->checkFunctions($node->uses);
}
return $this->checkClasses($node->uses);
}
/**
* @param \PhpParser\Node\Stmt\UseUse[] $uses
* @return string[]
*/
private function checkConstants(array $uses): array
{
$messages = [];
foreach ($uses as $use) {
if (!$this->broker->hasConstant($use->name)) {
$messages[] = sprintf('Used constant %s not found.', (string) $use->name);
}
}
return $messages;
}
/**
* @param \PhpParser\Node\Stmt\UseUse[] $uses
* @return string[]
*/
private function checkFunctions(array $uses): array
{
$messages = [];
foreach ($uses as $use) {
if (!$this->broker->hasFunction($use->name)) {
$messages[] = sprintf('Used function %s not found.', (string) $use->name);
} else {
$functionReflection = $this->broker->getFunction($use->name);
$realName = $functionReflection->getName();
$usedName = (string) $use->name;
if ($realName !== $usedName) {
$messages[] = sprintf(
'Function %s used with incorrect case: %s.',
$realName,
$usedName
);
}
}
}
return $messages;
}
/**
* @param \PhpParser\Node\Stmt\UseUse[] $uses
* @return string[]
*/
private function checkClasses(array $uses): array
{
return $this->classCaseSensitivityCheck->checkClassNames(
array_map(function (\PhpParser\Node\Stmt\UseUse $use): string {
return (string) $use->name;
}, $uses)
);
}
}
src/PhpDoc/TypeNodeResolver.php 0000666 00000017610 13436751442 0012514 0 ustar 00 resolveIdentifierTypeNode($typeNode, $nameScope);
} elseif ($typeNode instanceof ThisTypeNode) {
return $this->resolveThisTypeNode($typeNode, $nameScope);
} elseif ($typeNode instanceof NullableTypeNode) {
return $this->resolveNullableTypeNode($typeNode, $nameScope);
} elseif ($typeNode instanceof UnionTypeNode) {
return $this->resolveUnionTypeNode($typeNode, $nameScope);
} elseif ($typeNode instanceof IntersectionTypeNode) {
return $this->resolveIntersectionTypeNode($typeNode, $nameScope);
} elseif ($typeNode instanceof ArrayTypeNode) {
return $this->resolveArrayTypeNode($typeNode, $nameScope);
} elseif ($typeNode instanceof GenericTypeNode) {
return $this->resolveGenericTypeNode($typeNode, $nameScope);
}
return new ErrorType();
}
private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameScope $nameScope): Type
{
switch (strtolower($typeNode->name)) {
case 'int':
case 'integer':
return new IntegerType();
case 'string':
return new StringType();
case 'bool':
case 'boolean':
return new TrueOrFalseBooleanType();
case 'true':
return new TrueBooleanType();
case 'false':
return new FalseBooleanType();
case 'null':
return new NullType();
case 'float':
case 'double':
return new FloatType();
case 'array':
return new ArrayType(new MixedType(), new MixedType());
case 'scalar':
return new UnionType([
new IntegerType(),
new FloatType(),
new StringType(),
new TrueOrFalseBooleanType(),
]);
case 'number':
return new UnionType([
new IntegerType(),
new FloatType(),
]);
case 'iterable':
return new IterableIterableType(new MixedType(), new MixedType());
case 'callable':
return new CallableType();
case 'resource':
return new ResourceType();
case 'mixed':
return new MixedType(true);
case 'void':
return new VoidType();
case 'object':
return new ObjectWithoutClassType();
}
if ($nameScope->getClassName() !== null) {
switch (strtolower($typeNode->name)) {
case 'self':
return new ObjectType($nameScope->getClassName());
case 'static':
return new StaticType($nameScope->getClassName());
case 'parent':
$broker = Broker::getInstance();
if ($broker->hasClass($nameScope->getClassName())) {
$classReflection = $broker->getClass($nameScope->getClassName());
if ($classReflection->getParentClass() !== false) {
return new ObjectType($classReflection->getParentClass()->getName());
}
}
return new NonexistentParentClassType();
}
}
return new ObjectType($nameScope->resolveStringName($typeNode->name));
}
private function resolveThisTypeNode(ThisTypeNode $typeNode, NameScope $nameScope): Type
{
if ($nameScope->getClassName() !== null) {
return new ThisType($nameScope->getClassName());
}
return new ErrorType();
}
private function resolveNullableTypeNode(NullableTypeNode $typeNode, NameScope $nameScope): Type
{
return TypeCombinator::addNull($this->resolve($typeNode->type, $nameScope));
}
private function resolveUnionTypeNode(UnionTypeNode $typeNode, NameScope $nameScope): Type
{
$iterableTypeNodes = [];
$otherTypeNodes = [];
foreach ($typeNode->types as $innerTypeNode) {
if ($innerTypeNode instanceof ArrayTypeNode) {
$iterableTypeNodes[] = $innerTypeNode->type;
} else {
$otherTypeNodes[] = $innerTypeNode;
}
}
$hasPhpunitMock = false;
$otherTypeTypes = $this->resolveMultiple($otherTypeNodes, $nameScope);
foreach ($otherTypeTypes as $otherType) {
if (
$otherType instanceof TypeWithClassName
&& in_array($otherType->getClassName(), [
'PHPUnit_Framework_MockObject_MockObject',
\PHPUnit\Framework\MockObject\MockObject::class,
], true)
) {
$hasPhpunitMock = true;
break;
}
}
if (count($otherTypeTypes) === 2 && $hasPhpunitMock) {
return TypeCombinator::intersect(...$otherTypeTypes);
}
if (count($iterableTypeNodes) > 0) {
$arrayTypeTypes = $this->resolveMultiple($iterableTypeNodes, $nameScope);
$arrayTypeType = TypeCombinator::union(...$arrayTypeTypes);
$addArray = true;
foreach ($otherTypeTypes as &$type) {
if ($type->isIterable()->yes() && $type->getIterableValueType()->isSuperTypeOf($arrayTypeType)->yes()) {
if ($type instanceof ObjectType) {
$type = new IntersectionType([$type, new IterableIterableType(new MixedType(), $arrayTypeType)]);
} elseif ($type instanceof ArrayType) {
$type = new ArrayType(new MixedType(), $arrayTypeType);
} elseif ($type instanceof IterableIterableType) {
$type = new IterableIterableType(new MixedType(), $arrayTypeType);
} else {
continue;
}
$addArray = false;
}
}
if ($addArray) {
$otherTypeTypes[] = new ArrayType(new MixedType(), $arrayTypeType);
}
}
return TypeCombinator::union(...$otherTypeTypes);
}
private function resolveIntersectionTypeNode(IntersectionTypeNode $typeNode, NameScope $nameScope): Type
{
$types = $this->resolveMultiple($typeNode->types, $nameScope);
return TypeCombinator::intersect(...$types);
}
private function resolveArrayTypeNode(ArrayTypeNode $typeNode, NameScope $nameScope): Type
{
$itemType = $this->resolve($typeNode->type, $nameScope);
return new ArrayType(new MixedType(), $itemType);
}
private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $nameScope): Type
{
$mainType = strtolower($typeNode->type->name);
$genericTypes = $this->resolveMultiple($typeNode->genericTypes, $nameScope);
if ($mainType === 'array') {
if (count($genericTypes) === 1) { // array
return new ArrayType(new MixedType(), $genericTypes[0]);
} elseif (count($genericTypes) === 2) { // array
return new ArrayType($genericTypes[0], $genericTypes[1]);
}
} elseif ($mainType === 'iterable') {
if (count($genericTypes) === 1) { // iterable
return new IterableIterableType(new MixedType(), $genericTypes[0]);
} elseif (count($genericTypes) === 2) { // iterable
return new IterableIterableType($genericTypes[0], $genericTypes[1]);
}
}
return new ErrorType();
}
/**
* @param TypeNode[] $typeNodes
* @param NameScope $nameScope
* @return Type[]
*/
private function resolveMultiple(array $typeNodes, NameScope $nameScope): array
{
$types = [];
foreach ($typeNodes as $typeNode) {
$types[] = $this->resolve($typeNode, $nameScope);
}
return $types;
}
}
src/PhpDoc/PhpDocBlock.php 0000666 00000006651 13436751442 0011376 0 ustar 00 docComment = $docComment;
$this->file = $file;
$this->class = $class;
}
public function getDocComment(): string
{
return $this->docComment;
}
public function getFile(): string
{
return $this->file;
}
public function getClass(): string
{
return $this->class;
}
public static function resolvePhpDocBlockForProperty(
Broker $broker,
string $docComment,
string $class,
string $propertyName,
string $file
): self
{
return self::resolvePhpDocBlock(
$broker,
$docComment,
$class,
$propertyName,
$file,
'hasNativeProperty',
'getNativeProperty',
__FUNCTION__
);
}
public static function resolvePhpDocBlockForMethod(
Broker $broker,
string $docComment,
string $class,
string $methodName,
string $file
): self
{
return self::resolvePhpDocBlock(
$broker,
$docComment,
$class,
$methodName,
$file,
'hasNativeMethod',
'getNativeMethod',
__FUNCTION__
);
}
private static function resolvePhpDocBlock(
Broker $broker,
string $docComment,
string $class,
string $name,
string $file,
string $hasMethodName,
string $getMethodName,
string $resolveMethodName
): self
{
if (
preg_match('#\{@inheritdoc\}#i', $docComment) > 0
&& $broker->hasClass($class)
) {
$classReflection = $broker->getClass($class);
if ($classReflection->getParentClass() !== false) {
$parentClassReflection = $classReflection->getParentClass();
$phpDocBlockFromClass = self::resolvePhpDocBlockFromClass(
$broker,
$parentClassReflection,
$name,
$hasMethodName,
$getMethodName,
$resolveMethodName
);
if ($phpDocBlockFromClass !== null) {
return $phpDocBlockFromClass;
}
}
foreach ($classReflection->getInterfaces() as $interface) {
$phpDocBlockFromClass = self::resolvePhpDocBlockFromClass(
$broker,
$interface,
$name,
$hasMethodName,
$getMethodName,
$resolveMethodName
);
if ($phpDocBlockFromClass !== null) {
return $phpDocBlockFromClass;
}
}
}
return new self($docComment, $file, $class);
}
/**
* @param \PHPStan\Broker\Broker $broker
* @param \PHPStan\Reflection\ClassReflection $classReflection
* @param string $name
* @param string $hasMethodName
* @param string $getMethodName
* @param string $resolveMethodName
* @return self|null
*/
private static function resolvePhpDocBlockFromClass(
Broker $broker,
ClassReflection $classReflection,
string $name,
string $hasMethodName,
string $getMethodName,
string $resolveMethodName
)
{
if ($classReflection->getFileName() !== false && $classReflection->$hasMethodName($name)) {
/** @var \PHPStan\Reflection\Php\PhpMethodReflection|\PHPStan\Reflection\Php\PhpPropertyReflection $parentReflection */
$parentReflection = $classReflection->$getMethodName($name);
if ($parentReflection->getDocComment() !== false) {
return self::$resolveMethodName(
$broker,
$parentReflection->getDocComment(),
$classReflection->getName(),
$name,
$classReflection->getFileName()
);
}
}
return null;
}
}
src/PhpDoc/PhpDocNodeResolver.php 0000666 00000013020 13436751442 0012737 0 ustar 00 typeNodeResolver = $typeNodeResolver;
}
public function resolve(PhpDocNode $phpDocNode, NameScope $nameScope): ResolvedPhpDocBlock
{
return new ResolvedPhpDocBlock(
$this->resolveVarTags($phpDocNode, $nameScope),
$this->resolveMethodTags($phpDocNode, $nameScope),
$this->resolvePropertyTags($phpDocNode, $nameScope),
$this->resolveParamTags($phpDocNode, $nameScope),
$this->resolveReturnTag($phpDocNode, $nameScope)
);
}
/**
* @param PhpDocNode $phpDocNode
* @param NameScope $nameScope
* @return \PHPStan\PhpDoc\Tag\VarTag[]
*/
private function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
{
$resolved = [];
foreach ($phpDocNode->getVarTagValues() as $tagValue) {
if ($tagValue->variableName !== '') {
$variableName = substr($tagValue->variableName, 1);
$type = !isset($resolved[$variableName])
? $this->typeNodeResolver->resolve($tagValue->type, $nameScope)
: new MixedType();
$resolved[$variableName] = new VarTag($type);
} else {
$resolved[] = new VarTag($this->typeNodeResolver->resolve($tagValue->type, $nameScope));
}
}
return $resolved;
}
/**
* @param PhpDocNode $phpDocNode
* @param NameScope $nameScope
* @return \PHPStan\PhpDoc\Tag\PropertyTag[]
*/
private function resolvePropertyTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
{
$resolved = [];
foreach ($phpDocNode->getPropertyTagValues() as $tagValue) {
$propertyName = substr($tagValue->propertyName, 1);
$propertyType = !isset($resolved[$propertyName])
? $this->typeNodeResolver->resolve($tagValue->type, $nameScope)
: new MixedType();
$resolved[$propertyName] = new PropertyTag(
$propertyType,
true,
true
);
}
foreach ($phpDocNode->getPropertyReadTagValues() as $tagValue) {
$propertyName = substr($tagValue->propertyName, 1);
$propertyType = !isset($resolved[$propertyName])
? $this->typeNodeResolver->resolve($tagValue->type, $nameScope)
: new MixedType();
$resolved[$propertyName] = new PropertyTag(
$propertyType,
true,
false
);
}
foreach ($phpDocNode->getPropertyWriteTagValues() as $tagValue) {
$propertyName = substr($tagValue->propertyName, 1);
$propertyType = !isset($resolved[$propertyName])
? $this->typeNodeResolver->resolve($tagValue->type, $nameScope)
: new MixedType();
$resolved[$propertyName] = new PropertyTag(
$propertyType,
false,
true
);
}
return $resolved;
}
/**
* @param PhpDocNode $phpDocNode
* @param NameScope $nameScope
* @return \PHPStan\PhpDoc\Tag\MethodTag[]
*/
private function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
{
$resolved = [];
foreach ($phpDocNode->getMethodTagValues() as $tagValue) {
$parameters = [];
foreach ($tagValue->parameters as $parameterNode) {
$parameterName = substr($parameterNode->parameterName, 1);
$type = $parameterNode->type !== null ? $this->typeNodeResolver->resolve($parameterNode->type, $nameScope) : new MixedType();
if ($parameterNode->defaultValue instanceof ConstExprNullNode) {
$type = TypeCombinator::addNull($type);
}
$parameters[$parameterName] = new MethodTagParameter(
$type,
$parameterNode->isReference,
$parameterNode->defaultValue !== null,
$parameterNode->isVariadic
);
}
$resolved[$tagValue->methodName] = new MethodTag(
$tagValue->returnType !== null ? $this->typeNodeResolver->resolve($tagValue->returnType, $nameScope) : new MixedType(),
$tagValue->isStatic,
$parameters
);
}
return $resolved;
}
/**
* @param PhpDocNode $phpDocNode
* @param NameScope $nameScope
* @return \PHPStan\PhpDoc\Tag\ParamTag[]
*/
private function resolveParamTags(PhpDocNode $phpDocNode, NameScope $nameScope): array
{
$resolved = [];
foreach ($phpDocNode->getParamTagValues() as $tagValue) {
$parameterName = substr($tagValue->parameterName, 1);
$parameterType = !isset($resolved[$parameterName])
? $this->typeNodeResolver->resolve($tagValue->type, $nameScope)
: new MixedType();
if ($tagValue->isVariadic) {
if (!$parameterType instanceof ArrayType) {
$parameterType = new ArrayType(new IntegerType(), $parameterType);
} elseif ($parameterType->getIterableKeyType() instanceof MixedType) {
$parameterType = new ArrayType(new IntegerType(), $parameterType->getItemType());
}
}
$resolved[$parameterName] = new ParamTag(
$parameterType,
$tagValue->isVariadic
);
}
return $resolved;
}
/**
* @param PhpDocNode $phpDocNode
* @param NameScope $nameScope
* @return \PHPStan\PhpDoc\Tag\ReturnTag|null
*/
private function resolveReturnTag(PhpDocNode $phpDocNode, NameScope $nameScope)
{
foreach ($phpDocNode->getReturnTagValues() as $tagValue) {
return new ReturnTag($this->typeNodeResolver->resolve($tagValue->type, $nameScope));
}
return null;
}
}
src/PhpDoc/TypeStringResolver.php 0000666 00000001751 13436751442 0013074 0 ustar 00 typeLexer = $typeLexer;
$this->typeParser = $typeParser;
$this->typeNodeResolver = $typeNodeResolver;
}
public function resolve(string $typeString, NameScope $nameScope = null): Type
{
$tokens = new TokenIterator($this->typeLexer->tokenize($typeString));
$typeNode = $this->typeParser->parse($tokens);
$tokens->consumeTokenType(Lexer::TOKEN_END);
return $this->typeNodeResolver->resolve($typeNode, $nameScope ?? new NameScope(null, []));
}
}
src/PhpDoc/Tag/MethodTagParameter.php 0000666 00000002063 13436751442 0013467 0 ustar 00 type = $type;
$this->isPassedByReference = $isPassedByReference;
$this->isOptional = $isOptional;
$this->isVariadic = $isVariadic;
}
public function getType(): Type
{
return $this->type;
}
public function isPassedByReference(): bool
{
return $this->isPassedByReference;
}
public function isOptional(): bool
{
return $this->isOptional;
}
public function isVariadic(): bool
{
return $this->isVariadic;
}
public static function __set_state(array $properties): self
{
return new self(
$properties['type'],
$properties['isPassedByReference'],
$properties['isOptional'],
$properties['isVariadic']
);
}
}
src/PhpDoc/Tag/PropertyTag.php 0000666 00000001420 13436751442 0012226 0 ustar 00 type = $type;
$this->readable = $readable;
$this->writable = $writable;
}
public function getType(): Type
{
return $this->type;
}
public function isReadable(): bool
{
return $this->readable;
}
public function isWritable(): bool
{
return $this->writable;
}
public static function __set_state(array $properties): self
{
return new self(
$properties['type'],
$properties['readable'],
$properties['writable']
);
}
}
src/PhpDoc/Tag/ParamTag.php 0000666 00000001131 13436751442 0011441 0 ustar 00 type = $type;
$this->isVariadic = $isVariadic;
}
public function getType(): Type
{
return $this->type;
}
public function isVariadic(): bool
{
return $this->isVariadic;
}
public static function __set_state(array $properties): self
{
return new self(
$properties['type'],
$properties['isVariadic']
);
}
}
src/PhpDoc/Tag/MethodTag.php 0000666 00000002070 13436751442 0011624 0 ustar 00 returnType = $returnType;
$this->isStatic = $isStatic;
$this->parameters = $parameters;
}
public function getReturnType(): Type
{
return $this->returnType;
}
public function isStatic(): bool
{
return $this->isStatic;
}
/**
* @return \PHPStan\PhpDoc\Tag\MethodTagParameter[]
*/
public function getParameters(): array
{
return $this->parameters;
}
public static function __set_state(array $properties): self
{
return new self(
$properties['returnType'],
$properties['isStatic'],
$properties['parameters']
);
}
}
src/PhpDoc/Tag/ReturnTag.php 0000666 00000000627 13436751442 0011671 0 ustar 00 type = $type;
}
public function getType(): Type
{
return $this->type;
}
public static function __set_state(array $properties): self
{
return new self(
$properties['type']
);
}
}
src/PhpDoc/Tag/VarTag.php 0000666 00000000624 13436751442 0011137 0 ustar 00 type = $type;
}
public function getType(): Type
{
return $this->type;
}
public static function __set_state(array $properties): self
{
return new self(
$properties['type']
);
}
}
src/PhpDoc/PhpDocStringResolver.php 0000666 00000001761 13436751442 0013331 0 ustar 00 phpDocNodeResolver = $phpDocNodeResolver;
$this->phpDocLexer = $phpDocLexer;
$this->phpDocParser = $phpDocParser;
}
public function resolve(string $phpDocString, NameScope $nameScope): ResolvedPhpDocBlock
{
$tokens = new TokenIterator($this->phpDocLexer->tokenize($phpDocString));
$phpDocNode = $this->phpDocParser->parse($tokens);
$tokens->consumeTokenType(Lexer::TOKEN_END);
return $this->phpDocNodeResolver->resolve($phpDocNode, $nameScope);
}
}
src/PhpDoc/ResolvedPhpDocBlock.php 0000666 00000003662 13436751442 0013101 0 ustar 00 varTags = $varTags;
$this->methodTags = $methodTags;
$this->propertyTags = $propertyTags;
$this->paramTags = $paramTags;
$this->returnTag = $returnTag;
}
/**
* @return \PHPStan\PhpDoc\Tag\VarTag[]
*/
public function getVarTags(): array
{
return $this->varTags;
}
/**
* @return \PHPStan\PhpDoc\Tag\MethodTag[]
*/
public function getMethodTags(): array
{
return $this->methodTags;
}
/**
* @return \PHPStan\PhpDoc\Tag\PropertyTag[]
*/
public function getPropertyTags(): array
{
return $this->propertyTags;
}
/**
* @return \PHPStan\PhpDoc\Tag\ParamTag[]
*/
public function getParamTags(): array
{
return $this->paramTags;
}
/**
* @return \PHPStan\PhpDoc\Tag\ReturnTag|null
*/
public function getReturnTag()
{
return $this->returnTag;
}
public static function __set_state(array $properties): self
{
return new self(
$properties['varTags'],
$properties['methodTags'],
$properties['propertyTags'],
$properties['paramTags'],
$properties['returnTag']
);
}
}
src/DependencyInjection/ContainerFactory.php 0000666 00000003431 13436751442 0015255 0 ustar 00 currentWorkingDirectory = $currentWorkingDirectory;
$fileHelper = new FileHelper($currentWorkingDirectory);
$this->rootDirectory = $fileHelper->normalizePath(__DIR__ . '/../..');
$this->configDirectory = $this->rootDirectory . '/conf';
}
public function create(
string $tempDirectory,
array $additionalConfigFiles
): \Nette\DI\Container
{
$configurator = new Configurator();
$configurator->defaultExtensions = [
'php' => PhpExtension::class,
'extensions' => \Nette\DI\Extensions\ExtensionsExtension::class,
];
$configurator->setDebugMode(true);
$configurator->setTempDirectory($tempDirectory);
$configurator->addParameters([
'rootDir' => $this->rootDirectory,
'currentWorkingDirectory' => $this->currentWorkingDirectory,
'cliArgumentsVariablesRegistered' => ini_get('register_argc_argv') === '1',
'tmpDir' => $tempDirectory,
]);
$configurator->addConfig($this->configDirectory . '/config.neon');
foreach ($additionalConfigFiles as $additionalConfigFile) {
$configurator->addConfig($additionalConfigFile);
}
return $configurator->createContainer();
}
public function getCurrentWorkingDirectory(): string
{
return $this->currentWorkingDirectory;
}
public function getRootDirectory(): string
{
return $this->rootDirectory;
}
public function getConfigDirectory(): string
{
return $this->configDirectory;
}
}
build.xml 0000666 00000006020 13436751442 0006400 0 ustar 00
CODE_OF_CONDUCT.md 0000666 00000006227 13436751442 0007367 0 ustar 00 # Contributor Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project maintainer at . All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
tmp/.gitignore 0000666 00000000014 13436751442 0007344 0 ustar 00 *
!/log
!.*
composer.json 0000666 00000001764 13436751442 0007313 0 ustar 00 {
"name": "phpstan/phpstan",
"description": "PHPStan - PHP Static Analysis Tool",
"license": ["MIT"],
"bin": [
"bin/phpstan"
],
"extra": {
"branch-alias": {
"dev-master": "0.9-dev"
}
},
"require": {
"php": "~7.0",
"nette/bootstrap": "^2.4 || ^3.0",
"nette/di": "^2.4.7 || ^3.0",
"nette/robot-loader": "^3.0.1",
"nette/utils": "^2.4.5 || ^3.0",
"nikic/php-parser": "^3.1",
"symfony/console": "~3.2 || ~4.0",
"symfony/finder": "~3.2 || ~4.0",
"jean85/pretty-package-versions": "^1.0.3",
"phpstan/phpdoc-parser": "^0.1"
},
"require-dev": {
"consistence/coding-standard": "2.2.1",
"jakub-onderka/php-parallel-lint": "^0.9.2",
"phing/phing": "^2.16.0",
"phpstan/phpstan-php-parser": "^0.9",
"phpstan/phpstan-phpunit": "^0.9",
"phpstan/phpstan-strict-rules": "^0.9",
"phpunit/phpunit": "^6.5.2",
"slevomat/coding-standard": "4.0.0"
},
"autoload": {
"psr-4": {"PHPStan\\": ["src/", "build/PHPStan"]}
},
"autoload-dev": {
"classmap": ["tests/PHPStan"]
}
}
.travis.yml 0000666 00000000747 13436751442 0006702 0 ustar 00 language: php
php:
- 7.0
- 7.1
- 7.2
- master
env:
- dependencies=lowest
- dependencies=highest
matrix:
allow_failures:
- php: master
env: dependencies=lowest
- php: master
env: dependencies=highest
before_script:
- composer self-update
- if [ "$dependencies" = "lowest" ]; then composer update --prefer-lowest --no-interaction; fi;
- if [ "$dependencies" = "highest" ]; then composer update --no-interaction; fi;
script:
- vendor/bin/phing
bin/phpstan 0000666 00000001644 13436751442 0006736 0 ustar 00 #!/usr/bin/env php
getPrettyVersion();
} catch (\OutOfBoundsException $e) {
}
$application = new \Symfony\Component\Console\Application(
'PHPStan - PHP Static Analysis Tool',
$version
);
$application->setCatchExceptions(false);
$application->add(new AnalyseCommand());
$application->run();
.editorconfig 0000666 00000000543 13436751442 0007240 0 ustar 00 root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
[*.{php,phpt}]
indent_style = tab
indent_size = 4
[*.xml]
indent_style = tab
indent_size = 4
[*.neon]
indent_style = tab
indent_size = 4
[*.{yaml,yml}]
indent_style = space
indent_size = 2
[composer.json]
indent_style = tab
indent_size = 4
appveyor.yml 0000666 00000007263 13436751442 0007161 0 ustar 00 build: false
clone_depth: 50
platform:
- x64
environment:
matrix:
- dependencies: lowest
php_version: 7.0
- dependencies: highest
php_version: 7.0
- dependencies: lowest
php_version: 7.1
- dependencies: highest
php_version: 7.1
project_directory: c:\projects\phpstan
composer_directory: c:\tools\composer
composer_executable: c:\tools\composer\composer.phar
php_archive_directory: c:\tools\php-archive
php_directory: c:\tools\php
clone_folder: c:\projects\phpstan
cache:
- c:\tools\composer
- c:\tools\php-archive
- '%LOCALAPPDATA%\Composer'
init:
- ps: $Env:PATH = $Env:php_directory + ';' + $Env:composer_directory + ';' + $Env:PATH
- ps: $Env:ANSICON = '121x90 (121x90)'
install:
# Download requested PHP version
- ps: If ((Test-Path $Env:php_archive_directory) -eq $False) { New-Item -Path $Env:php_archive_directory -ItemType 'directory' }
- ps: $requested_php_version = %{If ($Env:dependencies -eq 'lowest') { $Env:php_version + '.0' } Else { (((choco search php --exact --all-versions -r | Select-String -pattern $Env:php_version) -replace '[php|]', '') | %{ New-Object System.Version $_ } | Sort-Object | Select-Object -Last 1).ToString() }}
- ps: $php_version_url = %{If ($Env:dependencies -eq 'lowest') { 'http://windows.php.net/downloads/releases/archives/php-' + $requested_php_version + '-nts-Win32-VC14-' + $Env:platform + '.zip' } Else { 'http://windows.php.net/downloads/releases/php-' + $requested_php_version + '-nts-Win32-VC14-' + $Env:platform + '.zip' }}
- ps: $php_version_file = $Env:php_archive_directory + '\php-' + $requested_php_version + '.zip'
- ps: If ((Test-Path $php_version_file) -eq $False) { appveyor-retry appveyor DownloadFile $php_version_url -FileName $php_version_file }
# Prepare PHP
- ps: New-Item -Path $Env:php_directory -ItemType 'directory'
- ps: cd $Env:php_directory
- ps: 7z x $php_version_file
- ps: Copy-Item php.ini-production -Destination php.ini
- ps: Add-Content -Path php.ini -Value 'memory_limit=1G'
- ps: Add-Content -Path php.ini -Value 'date.timezone="UTC"'
- ps: Add-Content -Path php.ini -Value 'extension_dir=ext'
- ps: Add-Content -Path php.ini -Value 'extension=php_curl.dll'
- ps: Add-Content -Path php.ini -Value 'extension=php_intl.dll'
- ps: Add-Content -Path php.ini -Value 'extension=php_mbstring.dll'
- ps: Add-Content -Path php.ini -Value 'extension=php_openssl.dll'
- ps: Add-Content -Path php.ini -Value 'extension=php_gd2.dll'
- ps: Add-Content -Path php.ini -Value 'extension=php_mysqli.dll'
- ps: php --version
# Prepare composer
- ps: If ((Test-Path $Env:composer_directory) -eq $False) { New-Item -Path $Env:composer_directory -ItemType 'directory' }
- ps: If ((Test-Path $Env:composer_executable) -eq $False) { appveyor-retry appveyor DownloadFile https://getcomposer.org/composer.phar -FileName $Env:composer_executable }
- ps: Set-Content -Path ($Env:composer_directory + '\composer.bat') -Value ('@php ' + $Env:composer_executable + ' %*')
- composer self-update
# Install dependencies
- ps: cd $Env:project_directory
- IF %dependencies%==lowest composer update --prefer-lowest --no-interaction --no-progress
- IF %dependencies%==highest composer update --no-interaction --no-progress
# Apply patch to jakub-onderka/php-parallel-lint
- ps: appveyor-retry appveyor DownloadFile https://github.com/slevomat/PHP-Parallel-Lint/commit/50e939edf81619be100186f9c6192d97decad173.diff -FileName php-parallel-lint.patch
- git apply --directory vendor/jakub-onderka/php-parallel-lint -p1 --unsafe-paths --verbose php-parallel-lint.patch
- ps: Remove-Item -Path php-parallel-lint.patch
test_script:
- ps: cd $Env:project_directory
- vendor\bin\phing
.gitignore 0000666 00000000136 13436751442 0006551 0 ustar 00 /build/phpstan-generated.neon
/composer.lock
/conf/config.local.yml
/vendor
/.idea
/tests/tmp
conf/config.level0.neon 0000666 00000007001 13436751442 0011020 0 ustar 00 parameters:
customRulesetUsed: false
services:
-
class: PHPStan\Rules\Arrays\DuplicateKeysInLiteralArraysRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\ClassConstantRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\ExistingClassInInstanceOfRule
tags:
- phpstan.rules.rule
arguments:
checkClassCaseSensitivity: %checkClassCaseSensitivity%
-
class: PHPStan\Rules\Classes\ExistingClassesInClassImplementsRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\ExistingClassesInInterfaceExtendsRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\ExistingClassInClassExtendsRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\ExistingClassInTraitUseRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\InstantiationRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\RequireParentConstructCallRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\UnusedConstructorParametersRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Exceptions\CaughtExceptionExistenceRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Functions\CallToFunctionParametersRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Functions\CallToNonExistentFunctionRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Functions\ExistingClassesInClosureTypehintsRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Functions\ExistingClassesInTypehintsRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Functions\InnerFunctionRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Functions\NonExistentDefinedFunctionRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Functions\PrintfParametersRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Functions\UnusedClosureUsesRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Methods\CallMethodsRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Methods\CallStaticMethodsRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Methods\ExistingClassesInTypehintsRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Namespaces\ExistingNamesInGroupUseRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Namespaces\ExistingNamesInUseRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Properties\AccessPropertiesRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Properties\AccessStaticPropertiesRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Properties\ExistingClassesInPropertiesRule
tags:
- phpstan.rules.rule
arguments:
checkClassCaseSensitivity: %checkClassCaseSensitivity%
-
class: PHPStan\Rules\Properties\WritingToReadOnlyPropertiesRule
arguments:
checkThisOnly: %checkThisOnly%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Properties\ReadingWriteOnlyPropertiesRule
arguments:
checkThisOnly: %checkThisOnly%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Variables\DefinedVariableRule
arguments:
cliArgumentsVariablesRegistered: %cliArgumentsVariablesRegistered%
checkMaybeUndefinedVariables: %checkMaybeUndefinedVariables%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Variables\DefinedVariableInAnonymousFunctionUseRule
arguments:
checkMaybeUndefinedVariables: %checkMaybeUndefinedVariables%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Variables\ThisVariableRule
tags:
- phpstan.rules.rule
conf/config.level7.neon 0000666 00000000565 13436751442 0011037 0 ustar 00 includes:
- config.level6.neon
parameters:
checkNullables: true
services:
-
class: PHPStan\Rules\Properties\AccessPropertiesOnPossiblyNullRule
arguments:
checkThisOnly: %checkThisOnly%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Methods\CallMethodsOnPossiblyNullRule
arguments:
checkThisOnly: %checkThisOnly%
tags:
- phpstan.rules.rule
conf/config.level5.neon 0000666 00000000355 13436751442 0011032 0 ustar 00 includes:
- config.level4.neon
parameters:
checkFunctionArgumentTypes: true
checkArgumentsPassedByReference: true
services:
-
class: PHPStan\Rules\Functions\CallToCountOnlyWithArrayOrCountableRule
tags:
- phpstan.rules.rule
conf/config.level6.neon 0000666 00000000104 13436751442 0011023 0 ustar 00 includes:
- config.level5.neon
parameters:
checkUnionTypes: true
conf/config.level3.neon 0000666 00000002054 13436751442 0011026 0 ustar 00 includes:
- config.level2.neon
services:
-
class: PHPStan\Rules\Arrays\AppendedArrayItemTypeRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Arrays\InvalidKeyInArrayDimFetchRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Arrays\InvalidKeyInArrayItemRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Arrays\IterableInForeachRule
arguments:
checkUnionTypes: %checkUnionTypes%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Functions\ClosureReturnTypeRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Functions\ReturnTypeRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Methods\ReturnTypeRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Properties\DefaultValueTypesAssignedToPropertiesRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Properties\TypesAssignedToPropertiesRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Variables\VariableCloningRule
arguments:
checkNullables: %checkNullables%
tags:
- phpstan.rules.rule
conf/config.neon 0000666 00000012657 13436751442 0007647 0 ustar 00 parameters:
bootstrap: null
excludes_analyse: []
autoload_directories: []
autoload_files: []
fileExtensions:
- php
checkAlwaysTrueCheckTypeFunctionCall: false
checkAlwaysTrueInstanceof: false
checkClassCaseSensitivity: false
checkFunctionArgumentTypes: false
checkArgumentsPassedByReference: false
checkMaybeUndefinedVariables: false
checkNullables: false
checkThisOnly: true
checkUnionTypes: false
polluteScopeWithLoopInitialAssignments: true
polluteCatchScopeWithTryAssignments: false
ignoreErrors: []
internalErrorsCountLimit: 50
reportUnmatchedIgnoredErrors: true
universalObjectCratesClasses:
- stdClass
- SimpleXMLElement
earlyTerminatingMethodCalls: []
memoryLimitFile: %tmpDir%/.memory_limit
services:
-
class: PhpParser\BuilderFactory
-
class: PhpParser\Lexer
-
class: PhpParser\NodeTraverser
setup:
- addVisitor(@PhpParser\NodeVisitor\NameResolver)
-
class: PhpParser\NodeVisitor\NameResolver
-
class: PhpParser\Parser\Php7
-
class: PhpParser\PrettyPrinter\Standard
-
class: PHPStan\PhpDocParser\Lexer\Lexer
-
class: PHPStan\PhpDocParser\Parser\TypeParser
-
class: PHPStan\PhpDocParser\Parser\ConstExprParser
-
class: PHPStan\PhpDocParser\Parser\PhpDocParser
-
class: PHPStan\PhpDoc\PhpDocNodeResolver
-
class: PHPStan\PhpDoc\PhpDocStringResolver
-
class: PHPStan\PhpDoc\TypeNodeResolver
-
class: PHPStan\PhpDoc\TypeStringResolver
-
class: PHPStan\Analyser\Analyser
arguments:
ignoreErrors: %ignoreErrors%
reportUnmatchedIgnoredErrors: %reportUnmatchedIgnoredErrors%
bootstrapFile: %bootstrap%
internalErrorsCountLimit: %internalErrorsCountLimit%
-
class: PHPStan\Analyser\NodeScopeResolver
arguments:
polluteScopeWithLoopInitialAssignments: %polluteScopeWithLoopInitialAssignments%
polluteCatchScopeWithTryAssignments: %polluteCatchScopeWithTryAssignments%
earlyTerminatingMethodCalls: %earlyTerminatingMethodCalls%
-
class: PHPStan\Analyser\TypeSpecifier
-
class: PHPStan\Cache\Cache
arguments:
storage: @cacheStorage
-
class: PHPStan\Command\AnalyseApplication
arguments:
memoryLimitFile: %memoryLimitFile%
fileExtensions: %fileExtensions%
-
class: PHPStan\File\FileHelper
arguments:
workingDirectory: %currentWorkingDirectory%
-
class: PHPStan\File\FileExcluder
arguments:
analyseExcludes: %excludes_analyse%
-
class: PHPStan\Parser\CachedParser
arguments:
originalParser: @directParser
-
class: PHPStan\Parser\FunctionCallStatementFinder
-
implement: PHPStan\Reflection\FunctionReflectionFactory
-
class: PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension
-
class: PHPStan\Reflection\Annotations\AnnotationsPropertiesClassReflectionExtension
-
class: PHPStan\Reflection\Php\PhpClassReflectionExtension
-
class: PHPStan\Reflection\PhpDefect\PhpDefectClassReflectionExtension
-
implement: PHPStan\Reflection\Php\PhpMethodReflectionFactory
-
class: PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension
tags:
- phpstan.broker.propertiesClassReflectionExtension
arguments:
classes: %universalObjectCratesClasses%
-
class: PHPStan\Rules\ClassCaseSensitivityCheck
-
class: PHPStan\Rules\FunctionCallParametersCheck
arguments:
checkArgumentTypes: %checkFunctionArgumentTypes%
checkArgumentsPassedByReference: %checkArgumentsPassedByReference%
-
class: PHPStan\Rules\FunctionDefinitionCheck
arguments:
checkClassCaseSensitivity: %checkClassCaseSensitivity%
checkThisOnly: %checkThisOnly%
-
class: PHPStan\Rules\FunctionReturnTypeCheck
-
class: PHPStan\Rules\Properties\PropertyDescriptor
-
class: PHPStan\Rules\Properties\PropertyReflectionFinder
-
class: PHPStan\Rules\RegistryFactory
-
class: PHPStan\Rules\RuleLevelHelper
arguments:
checkNullables: %checkNullables%
checkThisOnly: %checkThisOnly%
checkUnionTypes: %checkUnionTypes%
-
class: PHPStan\Rules\UnusedFunctionParametersCheck
-
class: PHPStan\Type\FileTypeMapper
-
class: PHPStan\Type\Php\AllArgumentBasedFunctionReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: PHPStan\Type\Php\ArgumentBasedArrayFunctionReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: PHPStan\Type\Php\ArrayFilterFunctionReturnTypeReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: PHPStan\Type\Php\CallbackBasedArrayFunctionReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: PHPStan\Type\Php\CallbackBasedFunctionReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
broker:
class: PHPStan\Broker\Broker
factory: @brokerFactory::create
brokerFactory:
class: PHPStan\Broker\BrokerFactory
cacheStorage:
class: PHPStan\Cache\FileCacheStorage
arguments:
directory: %tmpDir%/cache/PHPStan
autowired: no
directParser:
class: PHPStan\Parser\DirectParser
autowired: no
registry:
class: PHPStan\Rules\Registry
factory: @PHPStan\Rules\RegistryFactory::create
errorFormatter.raw:
class: PHPStan\Command\ErrorFormatter\RawErrorFormatter
errorFormatter.table:
class: PHPStan\Command\ErrorFormatter\TableErrorFormatter
errorFormatter.checkstyle:
class: PHPStan\Command\ErrorFormatter\CheckstyleErrorFormatter
conf/config.level4.neon 0000666 00000001117 13436751442 0011026 0 ustar 00 includes:
- config.level3.neon
services:
-
class: PHPStan\Rules\Cast\UselessCastRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\ImpossibleInstanceOfRule
arguments:
checkAlwaysTrueInstanceof: %checkAlwaysTrueInstanceof%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Comparison\ImpossibleCheckTypeFunctionCallRule
arguments:
checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Comparison\StrictComparisonOfDifferentTypesRule
tags:
- phpstan.rules.rule
conf/config.level2.neon 0000666 00000000447 13436751442 0011031 0 ustar 00 includes:
- config.level1.neon
parameters:
checkClassCaseSensitivity: true
checkThisOnly: false
services:
-
class: PHPStan\Rules\PhpDoc\InvalidPhpDocTagValueRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule
tags:
- phpstan.rules.rule
conf/config.levelmax.neon 0000666 00000000040 13436751442 0011442 0 ustar 00 includes:
- config.level7.neon
conf/config.level1.neon 0000666 00000000417 13436751442 0011025 0 ustar 00 includes:
- config.level0.neon
parameters:
checkMaybeUndefinedVariables: true
services:
-
class: PHPStan\Rules\Constants\ConstantRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Variables\VariableCertaintyInIssetRule
tags:
- phpstan.rules.rule
.gitattributes 0000666 00000000052 13436751442 0007451 0 ustar 00 /build export-ignore
/tests export-ignore