README.md 0000666 00000042645 13436751643 0006056 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 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)**
## 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!
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 class code.
* 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 installed already created framework-specific extensions:
* [Nette Framework](https://github.com/phpstan/phpstan-nette)
* [Dibi - Database Abstraction Library](https://github.com/phpstan/phpstan-dibi)
Other framework-specific extension will be coming soon!
## Prerequisities
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`.
You can also use [PHPStan via Docker](https://github.com/tommy-muehle/docker-phpstan).
## First run
To let PHPStan analyse your codebase, you have use the `analyse` commmand 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 continous integration script.
## Rule levels
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 5 levels
(0 is the loosest and 4 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.
There's also **experimental** level 5 that currently enables:
* Union types (Foo|Bar will be a specified type with checks performed on it instead of mixed)
* Checking function and method argument types when calling them
## 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 nonexistent 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:
```yaml
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).
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.
```yaml
parameters:
excludes_analyse:
- %rootDir%/../../../tests/*/data/*
```
### 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:
```yaml
parameters:
universalObjectCratesClasses:
- Dibi\Row
- Ratchet\ConnectionInterface
```
### Add non-obviously assigned variables to scope
If you use the initial assignment variable after for-loop or while-loop, set `polluteScopeWithLoopInitialAssignments` boolean parameter to `true`.
```php
for ($i = 0; $i < count($list); $i++) {
// ...
}
echo $i;
```
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 either write
an else branch with throwing an exception:
```php
if (somethingIsTrue()) {
$foo = true;
} elseif (orSomethingElseIsTrue()) {
$foo = false;
} else {
throw new ShouldNotHappenException();
}
doFoo($foo);
```
Or you can set `defineVariablesWithoutDefaultBranch` boolean parameter to `true` and leave the code like this:
```php
if (somethingIsTrue()) {
$foo = true;
} elseif (orSomethingElseIsTrue()) {
$foo = false;
}
doFoo($foo);
```
I recommend leaving `polluteScopeWithLoopInitialAssignments`, `polluteCatchScopeWithTryAssignments` and
`defineVariablesWithoutDefaultBranch` 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:
```yaml
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:
```yaml
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.
### 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:
```yaml
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:
```yaml
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.
## 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.
### 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:
```yaml
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:
```yaml
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 static 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 static 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;
$type = $scope->getType($arg);
if ($type->getClass() !== null) {
return $type;
}
return $methodReflection->getReturnType();
}
```
And finally, register the extension to PHPStan in the project's config file:
```yaml
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.
## 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.
* If you install PHPStan globally on your system, you can experience errors resulting from
using different versions of dependencies than PHPStan uses. For example if PHPStan's
version of Symfony Console has a method with different arguments than your version
of Symfony Console and you use this method in the analysed code, PHPStan can mark that as error.
This will be solved in the future by prefixing the namespaces of PHPStan's dependencies.
## 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.
BACKERS.md 0000666 00000001021 13436751643 0006152 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).
As a reward, you will gain access to PHPStan's Slack where you can share
your experience and know-how with other backers.
Check out all the tiers - higher ones include additional goodies like placing
the logo of your company in PHPStan's README.
# $5+
* Adam Žurek
* Haralan Dobrev
* Jobs Dev
* Tomáš Votruba
* Pavel Vondrášek
# $1+
* Bronek Bialek
src/FileHelper.php 0000666 00000002350 13436751643 0010103 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 $path): string
{
$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 === '..') {
array_pop($normalizedPathParts);
} else {
$normalizedPathParts[] = $pathPart;
}
}
return $pathRoot . implode(DIRECTORY_SEPARATOR, $normalizedPathParts);
}
}
src/Type/ResourceType.php 0000666 00000000546 13436751643 0011443 0 ustar 00 nullable ? '|null' : '');
}
public function canAccessProperties(): bool
{
return false;
}
public function canCallMethods(): bool
{
return false;
}
}
src/Type/TypehintHelper.php 0000666 00000013673 13436751643 0011763 0 ustar 00 getParentClass() !== false) {
return new ObjectType($classReflection->getParentClass()->getName(), false);
}
}
return new NonexistentParentClassType(false);
}
} elseif ($typehintString === 'parent') {
return new NonexistentParentClassType(false);
}
$lowercasedTypehintString = strtolower($typehintString);
switch ($lowercasedTypehintString) {
case 'int':
case 'integer':
return new IntegerType($isNullable);
case 'bool':
case 'boolean':
return new TrueOrFalseBooleanType($isNullable);
case 'true':
return new TrueBooleanType($isNullable);
case 'false':
return new FalseBooleanType($isNullable);
case 'string':
return new StringType($isNullable);
case 'float':
return new FloatType($isNullable);
case 'array':
return new ArrayType(new MixedType(), $isNullable);
case 'iterable':
return new IterableIterableType(new MixedType(), $isNullable);
case 'callable':
return new CallableType($isNullable);
case 'null':
return new NullType();
case 'resource':
return new ResourceType($isNullable);
case 'object':
case 'mixed':
return new MixedType();
case 'void':
return new VoidType();
default:
$className = $typehintString;
if ($nameScope !== null) {
$className = $nameScope->resolveStringName($className);
}
return new ObjectType($className, $isNullable);
}
}
public static function decideTypeFromReflection(
\ReflectionType $reflectionType = null,
Type $phpDocType = null,
string $selfClass = null,
bool $isVariadic = false
): Type
{
if ($reflectionType === null) {
return $phpDocType !== null ? $phpDocType : new MixedType();
}
$reflectionTypeString = (string) $reflectionType;
if ($isVariadic) {
$reflectionTypeString .= '[]';
}
$type = self::getTypeObjectFromTypehint(
$reflectionTypeString,
$reflectionType->allowsNull(),
$selfClass
);
return self::decideType($type, $phpDocType);
}
public static function decideType(
Type $type,
Type $phpDocType = null
): Type
{
if ($phpDocType !== null) {
if ($type instanceof IterableType && $phpDocType instanceof ArrayType) {
if ($type instanceof IterableIterableType) {
$phpDocType = new IterableIterableType(
$phpDocType->getItemType(),
$type->isNullable() || $phpDocType->isNullable()
);
} elseif ($type instanceof ArrayType) {
$type = new ArrayType(
$phpDocType->getItemType(),
$type->isNullable() || $phpDocType->isNullable()
);
}
} elseif ($phpDocType instanceof UnionType) {
if ($phpDocType->accepts($type)) {
return $phpDocType;
}
}
if ($type->accepts($phpDocType)) {
return $phpDocType;
}
}
return $type;
}
/**
* @param \PHPStan\Type\Type[] $typeMap
* @param string $docComment
* @return \PHPStan\Type\Type|null
*/
public static function getReturnTypeFromPhpDoc(array $typeMap, string $docComment)
{
$returnTypeString = self::getReturnTypeStringFromMethod($docComment);
if ($returnTypeString !== null && isset($typeMap[$returnTypeString])) {
return $typeMap[$returnTypeString];
}
return null;
}
/**
* @param string $docComment
* @return string|null
*/
private static function getReturnTypeStringFromMethod(string $docComment)
{
$count = preg_match_all('#@return\s+' . FileTypeMapper::TYPE_PATTERN . '#', $docComment, $matches);
if ($count !== 1) {
return null;
}
return $matches[1][0];
}
/**
* @param \PHPStan\Type\Type[] $typeMap
* @param string[] $parameterNames
* @param string $docComment
* @return \PHPStan\Type\Type[]
*/
public static function getParameterTypesFromPhpDoc(
array $typeMap,
array $parameterNames,
string $docComment
): array
{
preg_match_all('#@param\s+' . FileTypeMapper::TYPE_PATTERN . '\s+\$([a-zA-Z0-9_]+)#', $docComment, $matches, PREG_SET_ORDER);
$phpDocParameterTypeStrings = [];
foreach ($matches as $match) {
$typeString = $match[1];
$parameterName = $match[2];
if (!isset($phpDocParameterTypeStrings[$parameterName])) {
$phpDocParameterTypeStrings[$parameterName] = [];
}
$phpDocParameterTypeStrings[$parameterName][] = $typeString;
}
$phpDocParameterTypes = [];
foreach ($parameterNames as $parameterName) {
$typeString = self::getParameterAnnotationTypeString($phpDocParameterTypeStrings, $parameterName);
if ($typeString !== null && isset($typeMap[$typeString])) {
$phpDocParameterTypes[$parameterName] = $typeMap[$typeString];
}
}
return $phpDocParameterTypes;
}
/**
* @param mixed[] $phpDocParameterTypeStrings
* @param string $parameterName
* @return string|null
*/
private static function getParameterAnnotationTypeString(array $phpDocParameterTypeStrings, string $parameterName)
{
if (!isset($phpDocParameterTypeStrings[$parameterName])) {
return null;
}
$typeStrings = $phpDocParameterTypeStrings[$parameterName];
if (count($typeStrings) > 1) {
return null;
}
return $typeStrings[0];
}
}
src/Type/NonexistentParentClassType.php 0000666 00000000562 13436751643 0014330 0 ustar 00 nullable ? '|null' : '');
}
public function canAccessProperties(): bool
{
return false;
}
public function canCallMethods(): bool
{
return false;
}
}
src/Type/ClassTypeHelperTrait.php 0000666 00000000570 13436751643 0013062 0 ustar 00 nullable = $nullable;
}
public function describe(): string
{
return 'false' . ($this->nullable ? '|null' : '');
}
public function canAccessProperties(): bool
{
return false;
}
public function canCallMethods(): bool
{
return false;
}
/**
* @return string|null
*/
public function getClass()
{
return null;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return [];
}
public function isNullable(): bool
{
return $this->nullable;
}
public function combineWith(Type $otherType): Type
{
if ($otherType instanceof self) {
return new self($this->isNullable() || $otherType->isNullable());
}
if ($otherType instanceof BooleanType) {
return new TrueOrFalseBooleanType($this->isNullable() || $otherType->isNullable());
}
if ($otherType instanceof NullType) {
return $this->makeNullable();
}
return new MixedType();
}
public function makeNullable(): Type
{
return new self(true);
}
public function accepts(Type $type): bool
{
if ($type instanceof self) {
return true;
}
if ($this->isNullable() && $type instanceof NullType) {
return true;
}
return $type instanceof MixedType;
}
public function isDocumentableNatively(): bool
{
return true;
}
}
src/Type/StringType.php 0000666 00000000542 13436751643 0011116 0 ustar 00 nullable ? '|null' : '');
}
public function canAccessProperties(): bool
{
return false;
}
public function canCallMethods(): bool
{
return false;
}
}
src/Type/CommentHelper.php 0000666 00000000760 13436751643 0011552 0 ustar 00 getDocComment();
$phpDocText = null;
if ($phpDoc !== null) {
return $phpDoc->getText();
}
$comments = $node->getAttribute('comments');
if ($comments === null) {
return null;
}
return $comments[count($comments) - 1]->getText();
}
}
src/Type/JustNullableTypeTrait.php 0000666 00000002377 13436751643 0013270 0 ustar 00 nullable = $nullable;
}
/**
* @return string|null
*/
public function getClass()
{
return null;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return [];
}
public function isNullable(): bool
{
return $this->nullable;
}
public function combineWith(Type $otherType): Type
{
if ($otherType instanceof $this) {
$thisClass = get_class($this);
return new $thisClass($this->isNullable() || $otherType->isNullable());
}
if ($otherType instanceof NullType) {
return $this->makeNullable();
}
return new MixedType();
}
public function makeNullable(): Type
{
$thisClass = get_class($this);
return new $thisClass(true);
}
public function accepts(Type $type): bool
{
if ($type instanceof $this) {
return true;
}
if ($this->isNullable() && $type instanceof NullType) {
return true;
}
if ($type instanceof UnionType && UnionTypeHelper::acceptsAll($this, $type)) {
return true;
}
return $type instanceof MixedType;
}
public function isDocumentableNatively(): bool
{
return true;
}
}
src/Type/IterableType.php 0000666 00000000327 13436751643 0011400 0 ustar 00 itemType = $itemType;
$this->nullable = $nullable;
$this->itemTypeInferredFromLiteralArray = $itemTypeInferredFromLiteralArray;
$this->possiblyCallable = $possiblyCallable;
}
/**
* @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($itemType, false);
}
return new self($itemType, $nullable);
}
public function isItemTypeInferredFromLiteralArray(): bool
{
return $this->itemTypeInferredFromLiteralArray;
}
public function isPossiblyCallable(): bool
{
return $this->possiblyCallable;
}
public function combineWith(Type $otherType): Type
{
if ($otherType instanceof IterableType) {
$isItemInferredFromLiteralArray = $this->isItemTypeInferredFromLiteralArray();
$isPossiblyCallable = $this->isPossiblyCallable();
if ($otherType instanceof self) {
$isItemInferredFromLiteralArray = $isItemInferredFromLiteralArray || $otherType->isItemTypeInferredFromLiteralArray();
$isPossiblyCallable = $isPossiblyCallable || $otherType->isPossiblyCallable();
}
return new self(
$this->getItemType()->combineWith($otherType->getItemType()),
$this->isNullable() || $otherType->isNullable(),
$isItemInferredFromLiteralArray,
$isPossiblyCallable
);
}
if ($otherType instanceof NullType) {
return $this->makeNullable();
}
return new MixedType();
}
public function makeNullable(): Type
{
return new self($this->getItemType(), true, $this->isItemTypeInferredFromLiteralArray(), $this->isPossiblyCallable());
}
public function accepts(Type $type): bool
{
if ($type instanceof self) {
return $this->getItemType()->accepts($type->getItemType());
}
if ($type instanceof MixedType) {
return true;
}
if ($this->isNullable() && $type instanceof NullType) {
return true;
}
if ($type instanceof UnionType && UnionTypeHelper::acceptsAll($this, $type)) {
return true;
}
return false;
}
public function describe(): string
{
return sprintf('%s[]', $this->getItemType()->describe()) . ($this->nullable ? '|null' : '');
}
public function isDocumentableNatively(): bool
{
return true;
}
public function resolveStatic(string $className): Type
{
if ($this->getItemType() instanceof StaticResolvableType) {
return new self(
$this->getItemType()->resolveStatic($className),
$this->isNullable(),
$this->isItemTypeInferredFromLiteralArray(),
$this->isPossiblyCallable()
);
}
return $this;
}
public function changeBaseClass(string $className): StaticResolvableType
{
if ($this->getItemType() instanceof StaticResolvableType) {
return new self(
$this->getItemType()->changeBaseClass($className),
$this->isNullable(),
$this->isItemTypeInferredFromLiteralArray(),
$this->isPossiblyCallable()
);
}
return $this;
}
}
src/Type/TrueBooleanType.php 0000666 00000002701 13436751643 0012066 0 ustar 00 nullable = $nullable;
}
public function describe(): string
{
return 'true' . ($this->nullable ? '|null' : '');
}
public function canAccessProperties(): bool
{
return false;
}
public function canCallMethods(): bool
{
return false;
}
/**
* @return string|null
*/
public function getClass()
{
return null;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return [];
}
public function isNullable(): bool
{
return $this->nullable;
}
public function combineWith(Type $otherType): Type
{
if ($otherType instanceof self) {
return new self($this->isNullable() || $otherType->isNullable());
}
if ($otherType instanceof BooleanType) {
return new TrueOrFalseBooleanType($this->isNullable() || $otherType->isNullable());
}
if ($otherType instanceof NullType) {
return $this->makeNullable();
}
return new MixedType();
}
public function makeNullable(): Type
{
return new self(true);
}
public function accepts(Type $type): bool
{
if ($type instanceof self) {
return true;
}
if ($this->isNullable() && $type instanceof NullType) {
return true;
}
return $type instanceof MixedType;
}
public function isDocumentableNatively(): bool
{
return true;
}
}
src/Type/IterableTypeTrait.php 0000666 00000001503 13436751643 0012401 0 ustar 00 getItemType();
$depth++;
}
return new NestedArrayItemType($itemType, $depth);
}
public function getItemType(): Type
{
return $this->itemType;
}
/**
* @return string|null
*/
public function getClass()
{
return null;
}
public function isNullable(): bool
{
return $this->nullable;
}
public function canAccessProperties(): bool
{
return false;
}
public function canCallMethods(): bool
{
return false;
}
}
src/Type/IterableIterableType.php 0000666 00000004304 13436751643 0013047 0 ustar 00 itemType = $itemType;
$this->nullable = $nullable;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return $this->getItemType()->getReferencedClasses();
}
public function combineWith(Type $otherType): Type
{
if ($otherType instanceof IterableType) {
return new self(
$this->getItemType()->combineWith($otherType->getItemType()),
$this->isNullable() || $otherType->isNullable()
);
}
if ($otherType instanceof NullType) {
return $this->makeNullable();
}
return new MixedType();
}
public function makeNullable(): Type
{
return new self($this->getItemType(), true);
}
public function accepts(Type $type): bool
{
if ($type instanceof IterableType) {
return $this->getItemType()->accepts($type->getItemType());
}
if ($type->getClass() !== null && $this->exists($type->getClass())) {
$classReflection = new \ReflectionClass($type->getClass());
return $classReflection->implementsInterface(\Traversable::class);
}
if ($type instanceof MixedType) {
return true;
}
if ($this->isNullable() && $type instanceof NullType) {
return true;
}
if ($type instanceof UnionType && UnionTypeHelper::acceptsAll($this, $type)) {
return true;
}
return false;
}
public function describe(): string
{
return sprintf('iterable(%s[])', $this->getItemType()->describe()) . ($this->nullable ? '|null' : '');
}
public function isDocumentableNatively(): bool
{
return true;
}
public function resolveStatic(string $className): Type
{
if ($this->getItemType() instanceof StaticResolvableType) {
return new self(
$this->getItemType()->resolveStatic($className),
$this->isNullable()
);
}
return $this;
}
public function changeBaseClass(string $className): StaticResolvableType
{
if ($this->getItemType() instanceof StaticResolvableType) {
return new self(
$this->getItemType()->changeBaseClass($className),
$this->isNullable()
);
}
return $this;
}
}
src/Type/CallableType.php 0000666 00000003315 13436751643 0011350 0 ustar 00 nullable = $nullable;
}
/**
* @return string|null
*/
public function getClass()
{
return null;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return [];
}
public function isNullable(): bool
{
return $this->nullable;
}
public function combineWith(Type $otherType): Type
{
if ($otherType instanceof self) {
return new self($this->isNullable() || $otherType->isNullable());
}
if ($otherType instanceof ArrayType && $otherType->isPossiblyCallable()) {
return $this;
}
if ($otherType instanceof NullType) {
return $this->makeNullable();
}
return new MixedType();
}
public function makeNullable(): Type
{
return new self(true);
}
public function accepts(Type $type): bool
{
if ($type instanceof self) {
return true;
}
if ($this->isNullable() && $type instanceof NullType) {
return true;
}
if ($type instanceof ArrayType && $type->isPossiblyCallable()) {
return true;
}
if ($type instanceof StringType) {
return true;
}
if ($type->getClass() === 'Closure') {
return true;
}
if ($type instanceof UnionType && UnionTypeHelper::acceptsAll($this, $type)) {
return true;
}
return $type instanceof MixedType;
}
public function describe(): string
{
return 'callable' . ($this->nullable ? '|null' : '');
}
public function canAccessProperties(): bool
{
return false;
}
public function canCallMethods(): bool
{
return true;
}
public function isDocumentableNatively(): bool
{
return true;
}
}
src/Type/StaticType.php 0000666 00000003204 13436751643 0011075 0 ustar 00 baseClass = $baseClass;
$this->nullable = $nullable;
}
/**
* @return string|null
*/
public function getClass()
{
return $this->baseClass;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return [$this->getClass()];
}
public function getBaseClass(): string
{
return $this->baseClass;
}
public function isNullable(): bool
{
return $this->nullable;
}
public function combineWith(Type $otherType): Type
{
return new self($this->baseClass, $this->isNullable() || $otherType->isNullable());
}
public function makeNullable(): Type
{
return new self($this->baseClass, true);
}
public function accepts(Type $type): bool
{
return (new ObjectType($this->baseClass, $this->isNullable()))->accepts($type);
}
public function describe(): string
{
return sprintf('static(%s)', $this->baseClass) . ($this->nullable ? '|null' : '');
}
public function canAccessProperties(): bool
{
return true;
}
public function canCallMethods(): bool
{
return true;
}
public function isDocumentableNatively(): bool
{
return true;
}
public function resolveStatic(string $className): Type
{
return new ObjectType($className, $this->isNullable());
}
public function changeBaseClass(string $className): StaticResolvableType
{
$thisClass = get_class($this);
return new $thisClass($className, $this->isNullable());
}
}
src/Type/UnionType.php 0000666 00000000301 13436751643 0010731 0 ustar 00 describe();
}, $types));
if ($isNullable) {
$description .= '|null';
}
return $description;
}
/**
* @param \PHPStan\Type\Type[] $types
* @return bool
*/
public static function canAccessProperties(array $types): bool
{
foreach ($types as $type) {
if ($type->canAccessProperties()) {
return true;
}
}
return false;
}
/**
* @param \PHPStan\Type\Type[] $types
* @return bool
*/
public static function canCallMethods(array $types): bool
{
foreach ($types as $type) {
if ($type->canCallMethods()) {
return true;
}
}
return false;
}
/**
* @param string $className
* @param \PHPStan\Type\Type[] $types
* @return \PHPStan\Type\Type[]
*/
public static function resolveStatic(string $className, array $types): array
{
foreach ($types as $i => $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|null
*/
public static function getClass(array $types)
{
$uniqueTypeClass = null;
foreach ($types as $type) {
if ($type->getClass() !== null) {
if ($uniqueTypeClass !== null) {
return null;
}
$uniqueTypeClass = $type->getClass();
}
}
return $uniqueTypeClass;
}
/**
* @param \PHPStan\Type\Type[] $types
* @return string[]
*/
public static function getReferencedClasses(array $types): array
{
$classes = [];
foreach ($types as $type) {
$classes = array_merge($classes, $type->getReferencedClasses());
}
return $classes;
}
/**
* @param \PHPStan\Type\UnionType $unionType
* @param \PHPStan\Type\Type $type
* @return bool|null
*/
public static function accepts(UnionType $unionType, Type $type)
{
if ($type instanceof UnionType) {
foreach ($type->getTypes() as $otherOtherType) {
$matchesAtLeastOne = false;
foreach ($unionType->getTypes() as $otherType) {
if ($otherType->accepts($otherOtherType)) {
$matchesAtLeastOne = true;
break;
}
}
if (!$matchesAtLeastOne) {
return false;
}
}
return true;
}
return null;
}
public static function acceptsAll(Type $type, UnionType $unionType): bool
{
foreach ($unionType->getTypes() as $otherType) {
if (!$type->accepts($otherType)) {
return false;
}
}
return true;
}
}
src/Type/DynamicStaticMethodReturnTypeExtension.php 0000666 00000000734 13436751643 0016645 0 ustar 00 class = $class;
$this->nullable = $nullable;
}
public function getClass(): string
{
return $this->class;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return [$this->getClass()];
}
public function isNullable(): bool
{
return $this->nullable;
}
public function combineWith(Type $otherType): Type
{
if ($otherType instanceof self && $this->getClass() === $otherType->getClass()) {
return new self($this->getClass(), $this->isNullable() || $otherType->isNullable());
}
if ($otherType instanceof NullType) {
return $this->makeNullable();
}
return new MixedType();
}
public function makeNullable(): Type
{
return new self($this->getClass(), true);
}
public function accepts(Type $type): bool
{
if ($type instanceof MixedType) {
return true;
}
if ($this->isNullable() && $type instanceof NullType) {
return true;
}
if ($type instanceof StaticType) {
return $this->checkSubclassAcceptability($type->getBaseClass());
}
if ($type instanceof UnionType && UnionTypeHelper::acceptsAll($this, $type)) {
return true;
}
if ($type->getClass() === null) {
return false;
}
return $this->checkSubclassAcceptability($type->getClass());
}
private function checkSubclassAcceptability(string $thatClass): bool
{
if ($this->getClass() === $thatClass) {
return true;
}
if (!$this->exists($this->getClass()) || !$this->exists($thatClass)) {
return false;
}
$thisReflection = new \ReflectionClass($this->getClass());
$thatReflection = new \ReflectionClass($thatClass);
if ($thisReflection->getName() === $thatReflection->getName()) {
// class alias
return true;
}
if ($thisReflection->isInterface() && $thatReflection->isInterface()) {
return $thatReflection->implementsInterface($this->getClass());
}
return $thatReflection->isSubclassOf($this->getClass());
}
public function describe(): string
{
return $this->class . ($this->nullable ? '|null' : '');
}
public function canAccessProperties(): bool
{
return true;
}
public function canCallMethods(): bool
{
return strtolower($this->class) !== 'stdclass';
}
public function isDocumentableNatively(): bool
{
return true;
}
}
src/Type/IntegerType.php 0000666 00000001351 13436751643 0011244 0 ustar 00 isNullable() || $otherType->isNullable());
}
if ($otherType instanceof FloatType) {
return new FloatType($this->isNullable() || $otherType->isNullable());
}
if ($otherType instanceof NullType) {
return $this->makeNullable();
}
return new MixedType();
}
public function describe(): string
{
return 'int' . ($this->nullable ? '|null' : '');
}
public function canAccessProperties(): bool
{
return false;
}
public function canCallMethods(): bool
{
return false;
}
}
src/Type/StaticResolvableType.php 0000666 00000000342 13436751643 0013114 0 ustar 00 nullable = $nullable;
}
public function describe(): string
{
return 'bool' . ($this->nullable ? '|null' : '');
}
public function canAccessProperties(): bool
{
return false;
}
public function canCallMethods(): bool
{
return false;
}
/**
* @return string|null
*/
public function getClass()
{
return null;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return [];
}
public function isNullable(): bool
{
return $this->nullable;
}
public function combineWith(Type $otherType): Type
{
if ($otherType instanceof BooleanType) {
return new self($this->isNullable() || $otherType->isNullable());
}
if ($otherType instanceof NullType) {
return $this->makeNullable();
}
return new MixedType();
}
public function makeNullable(): Type
{
return new self(true);
}
public function accepts(Type $type): bool
{
if ($type instanceof BooleanType) {
return true;
}
if ($this->isNullable() && $type instanceof NullType) {
return true;
}
return $type instanceof MixedType;
}
public function isDocumentableNatively(): bool
{
return true;
}
}
src/Type/MixedType.php 0000666 00000001435 13436751643 0010720 0 ustar 00 nullable = $nullable;
}
/**
* @return string|null
*/
public function getClass()
{
return null;
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return [];
}
public function isNullable(): bool
{
return $this->nullable;
}
public function combineWith(Type $otherType): Type
{
if ($otherType instanceof $this || $otherType instanceof IntegerType) {
return new self($this->isNullable() || $otherType->isNullable());
}
if ($otherType instanceof NullType) {
return $this->makeNullable();
}
return new MixedType();
}
public function makeNullable(): Type
{
return new self(true);
}
public function accepts(Type $type): bool
{
if ($type instanceof self || $type instanceof IntegerType) {
return true;
}
if ($this->isNullable() && $type instanceof NullType) {
return true;
}
if ($type instanceof UnionType && UnionTypeHelper::acceptsAll($this, $type)) {
return true;
}
return $type instanceof MixedType;
}
public function describe(): string
{
return 'float' . ($this->nullable ? '|null' : '');
}
public function canAccessProperties(): bool
{
return false;
}
public function canCallMethods(): bool
{
return false;
}
public function isDocumentableNatively(): bool
{
return true;
}
}
src/Type/FileTypeMapper.php 0000666 00000013215 13436751643 0011675 0 ustar 00 parser = $parser;
$this->cache = $cache;
$this->enableUnionTypes = $enableUnionTypes;
}
public function getTypeMap(string $fileName): array
{
$cacheKey = sprintf('%s-%d-v22-%d', $fileName, filemtime($fileName), $this->enableUnionTypes ? 1 : 0);
if (isset($this->memoryCache[$cacheKey])) {
return $this->memoryCache[$cacheKey];
}
$cachedResult = $this->cache->load($cacheKey);
if ($cachedResult === null) {
$typeMap = $this->createTypeMap($fileName);
$this->cache->save($cacheKey, $typeMap);
$this->memoryCache[$cacheKey] = $typeMap;
return $typeMap;
}
$this->memoryCache[$cacheKey] = $cachedResult;
return $cachedResult;
}
private function createTypeMap(string $fileName): array
{
$typeMap = [];
$patterns = [
'#@param\s+' . self::TYPE_PATTERN . '\s+\$[a-zA-Z0-9_]+#',
'#@var\s+' . self::TYPE_PATTERN . '#',
'#@var\s+\$[a-zA-Z0-9_]+\s+' . self::TYPE_PATTERN . '#',
'#@return\s+' . self::TYPE_PATTERN . '#',
'#@property(?:-read)?\s+' . self::TYPE_PATTERN . '\s+\$[a-zA-Z0-9_]+#',
];
/** @var \PhpParser\Node\Stmt\ClassLike|null $lastClass */
$lastClass = null;
$namespace = null;
$uses = [];
$nameScope = null;
$this->processNodes(
$this->parser->parseFile($fileName),
function (\PhpParser\Node $node) use ($patterns, &$typeMap, &$lastClass, &$namespace, &$uses, &$nameScope) {
if ($node instanceof Node\Stmt\ClassLike) {
$lastClass = $node;
} elseif ($node instanceof \PhpParser\Node\Stmt\Namespace_) {
$namespace = (string) $node->name;
$nameScope = null;
} 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;
}
$nameScope = null;
} 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);
}
}
$nameScope = null;
} elseif (!in_array(get_class($node), [
Node\Stmt\Property::class,
Node\Stmt\ClassMethod::class,
Node\Stmt\Function_::class,
Node\Expr\Assign::class,
Node\Stmt\Class_::class,
], true)) {
return;
}
$comment = CommentHelper::getDocComment($node);
if ($comment === null) {
return;
}
$className = $lastClass !== null ? $lastClass->name : null;
if ($className !== null && $namespace !== null) {
$className = sprintf('%s\\%s', $namespace, $className);
}
foreach ($patterns as $pattern) {
preg_match_all($pattern, $comment, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$typeString = $match[1];
if (isset($typeMap[$typeString])) {
continue;
}
if ($nameScope === null) {
$nameScope = new NameScope($namespace, $uses);
}
$typeMap[$typeString] = $this->getTypeFromTypeString($typeString, $className, $nameScope);
}
}
}
);
return $typeMap;
}
private function getTypeFromTypeString(string $typeString, string $className = null, NameScope $nameScope): Type
{
$typeParts = explode('|', $typeString);
$typePartsWithoutNull = array_values(array_filter($typeParts, function ($part) {
return strtolower($part) !== 'null';
}));
if (count($typePartsWithoutNull) === 0) {
return new NullType();
}
$isNullable = count($typeParts) !== count($typePartsWithoutNull);
if (count($typePartsWithoutNull) > 1) {
$otherTypes = [];
/** @var \PHPStan\Type\IterableType $iterableType */
$iterableType = null;
$onlyOneItemType = true;
foreach ($typePartsWithoutNull as $typePart) {
$type = TypehintHelper::getTypeObjectFromTypehint($typePart, false, $className, $nameScope);
if ($type instanceof IterableType) {
if ($iterableType !== null) {
if ($onlyOneItemType) {
$otherTypes[] = $iterableType;
}
$otherTypes[] = $type;
$onlyOneItemType = false;
} else {
$iterableType = $type;
}
} else {
$otherTypes[] = $type;
}
}
if ($iterableType !== null && $onlyOneItemType) {
return new UnionIterableType($iterableType->getItemType(), $isNullable, $otherTypes);
}
if ($this->enableUnionTypes) {
return new CommonUnionType($otherTypes, $isNullable);
}
return new MixedType();
}
return TypehintHelper::getTypeObjectFromTypehint($typePartsWithoutNull[0], $isNullable, $className, $nameScope);
}
/**
* @param \PhpParser\Node[]|\PhpParser\Node $node
* @param \Closure $nodeCallback
*/
private function processNodes($node, \Closure $nodeCallback)
{
if ($node instanceof Node) {
$nodeCallback($node);
foreach ($node->getSubNodeNames() as $subNodeName) {
$subNode = $node->{$subNodeName};
$this->processNodes($subNode, $nodeCallback);
}
} elseif (is_array($node)) {
foreach ($node as $subNode) {
$this->processNodes($subNode, $nodeCallback);
}
}
}
}
src/Type/NullType.php 0000666 00000001541 13436751643 0010562 0 ustar 00 makeNullable();
}
public function makeNullable(): Type
{
return $this;
}
public function accepts(Type $type): bool
{
return $type instanceof self || $type instanceof MixedType;
}
public function describe(): string
{
return 'null';
}
public function canAccessProperties(): bool
{
return false;
}
public function canCallMethods(): bool
{
return false;
}
public function isDocumentableNatively(): bool
{
return true;
}
}
src/Type/NestedArrayItemType.php 0000666 00000000657 13436751643 0012717 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 00000000352 13436751643 0010556 0 ustar 00 getBaseClass()) . ($this->isNullable() ? '|null' : '');
}
}
src/Type/UnionIterableType.php 0000666 00000006570 13436751643 0012417 0 ustar 00 itemType = $itemType;
$this->nullable = $nullable;
$this->types = $types;
}
/**
* @return string|null
*/
public function getClass()
{
return UnionTypeHelper::getClass($this->getTypes());
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
$classes = UnionTypeHelper::getReferencedClasses($this->getTypes());
$classes = array_merge($classes, $this->getItemType()->getReferencedClasses());
return $classes;
}
/**
* @return \PHPStan\Type\Type[]
*/
public function getTypes(): array
{
return $this->types;
}
public function combineWith(Type $otherType): Type
{
if ($otherType instanceof IterableType) {
$types = $this->getTypes();
if ($otherType instanceof UnionType) {
$otherTypesTemp = [];
foreach ($this->getTypes() as $otherOtherType) {
$otherTypesTemp[$otherOtherType->describe()] = $otherOtherType;
}
foreach ($otherType->getTypes() as $otherOtherType) {
$otherTypesTemp[$otherOtherType->describe()] = $otherOtherType;
}
$types = array_values($otherTypesTemp);
}
return new self(
$this->getItemType()->combineWith($otherType->getItemType()),
$this->isNullable() || $otherType->isNullable(),
$types
);
}
if ($otherType instanceof NullType) {
return $this->makeNullable();
}
return new MixedType();
}
public function makeNullable(): Type
{
return new self($this->getItemType(), true, $this->types);
}
public function accepts(Type $type): bool
{
if ($type instanceof MixedType) {
return true;
}
if ($this->isNullable() && $type instanceof NullType) {
return true;
}
$accepts = UnionTypeHelper::accepts($this, $type);
if ($accepts !== null && !$accepts) {
return false;
}
if ($type instanceof IterableType) {
return $this->getItemType()->accepts($type->getItemType());
}
foreach ($this->getTypes() as $otherType) {
if ($otherType->accepts($type)) {
return true;
}
}
return false;
}
public function describe(): string
{
return sprintf('%s[]|%s', $this->getItemType()->describe(), UnionTypeHelper::describe($this->getTypes(), $this->isNullable()));
}
public function canAccessProperties(): bool
{
return UnionTypeHelper::canAccessProperties($this->getTypes());
}
public function canCallMethods(): bool
{
return UnionTypeHelper::canCallMethods($this->getTypes());
}
public function isDocumentableNatively(): bool
{
return false;
}
public function resolveStatic(string $className): Type
{
$itemType = $this->getItemType();
if ($itemType instanceof StaticResolvableType) {
$itemType = $itemType->resolveStatic($className);
}
return new self(
$itemType,
$this->isNullable(),
UnionTypeHelper::resolveStatic($className, $this->getTypes())
);
}
public function changeBaseClass(string $className): StaticResolvableType
{
$itemType = $this->getItemType();
if ($itemType instanceof StaticResolvableType) {
$itemType = $itemType->changeBaseClass($className);
}
return new self(
$itemType,
$this->isNullable(),
UnionTypeHelper::changeBaseClass($className, $this->getTypes())
);
}
}
src/Type/Type.php 0000666 00000001101 13436751643 0007717 0 ustar 00 types = $types;
$this->isNullable = $isNullable;
}
/**
* @return \PHPStan\Type\Type[]
*/
public function getTypes(): array
{
return $this->types;
}
/**
* @return string|null
*/
public function getClass()
{
return UnionTypeHelper::getClass($this->getTypes());
}
/**
* @return string[]
*/
public function getReferencedClasses(): array
{
return UnionTypeHelper::getReferencedClasses($this->getTypes());
}
public function isNullable(): bool
{
return $this->isNullable;
}
public function combineWith(Type $otherType): Type
{
if ($otherType instanceof NullType) {
return $this->makeNullable();
}
$types = $this->getTypes();
if ($otherType instanceof UnionType) {
$otherTypesTemp = [];
foreach ($this->getTypes() as $otherOtherType) {
$otherTypesTemp[$otherOtherType->describe()] = $otherOtherType;
}
foreach ($otherType->getTypes() as $otherOtherType) {
$otherTypesTemp[$otherOtherType->describe()] = $otherOtherType;
}
$types = array_values($otherTypesTemp);
}
return new self(
$types,
$this->isNullable() || $otherType->isNullable()
);
}
public function makeNullable(): Type
{
return new self($this->getTypes(), true);
}
public function accepts(Type $type): bool
{
if ($type instanceof MixedType) {
return true;
}
if ($this->isNullable() && $type instanceof NullType) {
return true;
}
$accepts = UnionTypeHelper::accepts($this, $type);
if ($accepts !== null) {
return $accepts;
}
foreach ($this->getTypes() as $otherType) {
if ($otherType->accepts($type)) {
return true;
}
}
return false;
}
public function describe(): string
{
return UnionTypeHelper::describe($this->getTypes(), $this->isNullable());
}
public function canAccessProperties(): bool
{
return UnionTypeHelper::canAccessProperties($this->getTypes());
}
public function canCallMethods(): bool
{
return UnionTypeHelper::canCallMethods($this->getTypes());
}
public function isDocumentableNatively(): bool
{
return false;
}
public function resolveStatic(string $className): Type
{
return new self(UnionTypeHelper::resolveStatic($className, $this->getTypes()), $this->isNullable());
}
public function changeBaseClass(string $className): StaticResolvableType
{
return new self(UnionTypeHelper::changeBaseClass($className, $this->getTypes()), $this->isNullable());
}
}
src/Analyser/NodeScopeResolver.php 0000666 00000114434 13436751643 0013252 0 ustar 00 methods(string[]) */
private $earlyTerminatingMethodCalls;
/** @var \PHPStan\Reflection\ClassReflection|null */
private $anonymousClassReflection;
public function __construct(
Broker $broker,
Parser $parser,
\PhpParser\PrettyPrinter\Standard $printer,
FileTypeMapper $fileTypeMapper,
TypeSpecifier $typeSpecifier,
bool $polluteScopeWithLoopInitialAssignments,
bool $polluteCatchScopeWithTryAssignments,
bool $defineVariablesWithoutDefaultBranch,
array $earlyTerminatingMethodCalls
)
{
$this->broker = $broker;
$this->parser = $parser;
$this->printer = $printer;
$this->fileTypeMapper = $fileTypeMapper;
$this->typeSpecifier = $typeSpecifier;
$this->polluteScopeWithLoopInitialAssignments = $polluteScopeWithLoopInitialAssignments;
$this->polluteCatchScopeWithTryAssignments = $polluteCatchScopeWithTryAssignments;
$this->defineVariablesWithoutDefaultBranch = $defineVariablesWithoutDefaultBranch;
$this->earlyTerminatingMethodCalls = $earlyTerminatingMethodCalls;
}
/**
* @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
)
{
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());
}
}
}
$nodeScope = $scope;
if ($i === 0 && $closureBindScope !== null) {
$nodeScope = $closureBindScope;
}
$this->processNode($node, $nodeScope, $nodeCallback);
$scope = $this->lookForAssigns($scope, $node);
if ($node instanceof If_) {
if ($this->findEarlyTermination($node->stmts, $scope) !== null) {
$scope = $this->lookForTypeSpecificationsInEarlyTermination($scope, $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 instanceof Node\Stmt\DeclareDeclare
&& $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 = $this->lookForTypeSpecifications($scope, $node->args[0]->value);
} elseif (
$node instanceof Assign
&& $node->var instanceof Array_
) {
$scope = $this->lookForArrayDestructuringArray($scope, $node->var);
}
}
}
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);
}
}
} else {
if ($node instanceof Expr\Empty_) {
$scope = $this->specifyProperty($scope, $node->expr);
$scope = $this->assignVariable($scope, $node->expr);
}
}
}
private function lookForArrayDestructuringArray(Scope $scope, Node $node): Scope
{
if ($node instanceof Array_) {
foreach ($node->items as $item) {
$scope = $this->lookForArrayDestructuringArray($scope, $item->value);
}
} elseif ($node instanceof Variable && is_string($node->name)) {
$scope = $scope->assignVariable($node->name);
}
return $scope;
}
private function processNode(\PhpParser\Node $node, Scope $scope, \Closure $nodeCallback)
{
$nodeCallback($node, $scope);
if (
$node instanceof \PhpParser\Node\Stmt\ClassLike
) {
if ($node instanceof Node\Stmt\Trait_) {
return;
}
if (isset($node->namespacedName)) {
$scope = $scope->enterClass((string) $node->namespacedName);
} else {
$scope = $scope->enterAnonymousClass($this->anonymousClassReflection);
}
} 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
&& (is_string($node->class) || $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 ($argValueType->getClass() !== null) {
$scopeClass = $argValueType->getClass();
} elseif (
$argValue instanceof Expr\ClassConstFetch
&& $argValue->name === 'class'
&& $argValue->class instanceof Name
) {
$resolvedName = $scope->resolveName($argValue->class);
if ($resolvedName !== null) {
$scopeClass = $resolvedName;
}
} elseif ($argValue instanceof Node\Scalar\String_) {
$scopeClass = $argValue->value;
}
}
$closureBindScope = $scope->enterClosureBind($thisType, $scopeClass);
} elseif ($node instanceof \PhpParser\Node\Expr\Closure) {
$scope = $scope->enterAnonymousFunction($node->params, $node->uses, $node->returnType);
} elseif ($node instanceof Foreach_) {
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 ? $node->keyVar->name : null
);
} else {
if ($node->keyVar !== null && $node->keyVar instanceof Variable && is_string($node->keyVar->name)) {
$scope = $scope->assignVariable($node->keyVar->name);
}
if ($node->valueVar instanceof Array_) {
$scope = $this->lookForArrayDestructuringArray($scope, $node->valueVar);
} else {
$scope = $this->lookForAssigns($scope, $node->valueVar);
}
}
} elseif ($node instanceof Catch_) {
if (isset($node->types)) {
$nodeTypes = $node->types;
} elseif (isset($node->type)) {
$nodeTypes = [$node->type];
} else {
throw new \PHPStan\ShouldNotHappenException();
}
$scope = $scope->enterCatch(
$nodeTypes,
$node->var
);
} elseif ($node instanceof For_) {
foreach ($node->init as $initExpr) {
$scope = $this->lookForAssigns($scope, $initExpr);
}
foreach ($node->cond as $condExpr) {
$scope = $this->lookForAssigns($scope, $condExpr);
}
foreach ($node->loop as $loopExpr) {
$scope = $this->lookForAssigns($scope, $loopExpr);
}
} elseif ($node instanceof If_) {
$scope = $this->lookForAssigns($scope, $node->cond)->exitFirstLevelStatements();
$ifScope = $scope;
$this->processNode($node->cond, $scope, $nodeCallback);
$scope = $this->lookForTypeSpecifications($scope, $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;
foreach ($node->elseifs as $elseif) {
$scope = $elseifScope;
$scope = $this->lookForAssigns($scope, $elseif->cond)->exitFirstLevelStatements();
$this->processNode($elseif->cond, $scope, $nodeCallback);
$scope = $this->lookForTypeSpecifications($scope, $elseif->cond);
$this->processNode($elseif->cond, $scope, $specifyFetchedProperty);
$this->processNodes($elseif->stmts, $scope->enterFirstLevelStatements(), $nodeCallback);
$elseifScope = $this->lookForAssigns($elseifScope, $elseif->cond);
}
if ($node->else !== null) {
$this->processNode($node->else, $elseifScope, $nodeCallback);
}
return;
} elseif ($node instanceof Switch_) {
$scope = $this->lookForAssigns($scope, $node->cond);
$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) {
if ($caseNode->cond !== null) {
$switchScope = $this->lookForAssigns($switchScope, $caseNode->cond);
if ($switchConditionIsTrue) {
$switchScope = $this->lookForTypeSpecifications($switchScope, $caseNode->cond);
} elseif (
$switchConditionGetClassExpression !== null
&& $caseNode->cond instanceof Expr\ClassConstFetch
&& $caseNode->cond->class instanceof Name
&& strtolower($caseNode->cond->name) === 'class'
) {
$switchScope = $switchScope->specifyExpressionType(
$switchConditionGetClassExpression,
new ObjectType((string) $caseNode->cond->class, false)
);
}
}
$this->processNode($caseNode, $switchScope, $nodeCallback);
if ($this->findEarlyTermination($caseNode->stmts, $switchScope) !== null) {
$switchScope = $scope;
}
}
return;
} elseif ($node instanceof While_) {
$scope = $this->lookForAssigns($scope, $node->cond);
} elseif ($this->polluteCatchScopeWithTryAssignments && $node instanceof TryCatch) {
foreach ($node->stmts as $statement) {
$scope = $this->lookForAssigns($scope, $statement);
}
} elseif ($node instanceof Ternary) {
$scope = $this->lookForAssigns($scope, $node->cond);
} elseif ($node instanceof Do_) {
foreach ($node->stmts as $statement) {
$scope = $this->lookForAssigns($scope, $statement);
}
} 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)->getClass() === 'Closure'
&& $node->name === 'call'
&& isset($node->args[0])
) {
$closureCallScope = $scope->enterClosureBind($scope->getType($node->args[0]->value), 'static');
}
$scope = $scope->enterFunctionCall($node);
} elseif ($node instanceof Array_) {
foreach ($node->items as $item) {
$scope = $this->lookForAssigns($scope, $item->value);
}
} elseif ($node instanceof New_ && $node->class instanceof Class_) {
$node->args = [];
foreach ($node->class->stmts as $i => $statement) {
if (
$statement instanceof Node\Stmt\ClassMethod
&& $statement->name === '__construct'
) {
unset($node->class->stmts[$i]);
$node->class->stmts = array_values($node->class->stmts);
break;
}
}
$code = $this->printer->prettyPrint([$node]);
$classReflection = new \ReflectionClass(eval(sprintf('return %s', $code)));
$this->anonymousClassReflection = $this->broker->getClassFromReflection($classReflection);
} elseif ($node instanceof BooleanNot) {
$scope = $scope->enterNegation();
} elseif ($node instanceof Unset_) {
foreach ($node->vars as $unsetVar) {
if ($unsetVar instanceof Variable && is_string($unsetVar->name)) {
$scope = $scope->enterVariableAssign($unsetVar->name);
}
}
}
$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 Foreach_ && $subNodeName === 'stmts') {
$scope = $this->lookForAssigns($scope, $node->expr);
}
if ($node instanceof While_ && $subNodeName === 'stmts') {
$scope = $this->lookForTypeSpecifications($scope, $node->cond);
}
if ($node instanceof Isset_ && $subNodeName === 'vars') {
foreach ($node->vars as $issetVar) {
$scope = $this->specifyProperty($scope, $issetVar);
}
}
$this->processNodes($subNode, $scope, $nodeCallback, $argClosureBindScope);
} elseif ($subNode instanceof \PhpParser\Node) {
if ($node instanceof Coalesce && $subNodeName === 'left') {
$scope = $this->assignVariable($scope, $subNode);
}
if ($node instanceof Ternary) {
if ($subNodeName === 'if') {
$scope = $this->lookForTypeSpecifications($scope, $node->cond);
$this->processNode($node->cond, $scope, function (Node $node, Scope $inScope) use (&$scope) {
$this->specifyFetchedPropertyForInnerScope($node, $inScope, false, $scope);
});
} elseif ($subNodeName === 'else') {
$scope = $this->lookForTypeSpecificationsInEarlyTermination($scope, $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 = $this->lookForTypeSpecifications($scope, $node->left);
}
if ($node instanceof BooleanOr && $subNodeName === 'right') {
$scope = $this->lookForTypeSpecificationsInEarlyTermination($scope, $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);
}
if ($node instanceof Expr\Empty_ && $subNodeName === 'expr') {
$scope = $this->specifyProperty($scope, $node->expr);
$scope = $this->lookForEnterVariableAssign($scope, $node->expr);
}
$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, Node $node): Scope
{
if ($node instanceof Variable && is_string($node->name)) {
$scope = $scope->enterVariableAssign($node->name);
} elseif ($node instanceof ArrayDimFetch) {
while ($node instanceof ArrayDimFetch) {
$node = $node->var;
}
if ($node instanceof Variable && is_string($node->name)) {
$scope = $scope->enterVariableAssign($node->name);
}
} elseif ($node instanceof List_ || $node instanceof Array_) {
$listItems = isset($node->items) ? $node->items : $node->vars;
foreach ($listItems as $listItem) {
if ($listItem === null) {
continue;
}
$listItemValue = $listItem;
if ($listItemValue instanceof Expr\ArrayItem) {
$listItemValue = $listItemValue->value;
}
$scope = $this->lookForEnterVariableAssign($scope, $listItemValue);
}
}
return $scope;
}
private function lookForTypeSpecifications(Scope $scope, Node $node): Scope
{
$types = $this->typeSpecifier->specifyTypesInCondition(new SpecifiedTypes(), $scope, $node);
foreach ($types->getSureTypes() as $type) {
$scope = $scope->specifyExpressionType($type[0], $type[1]);
}
return $scope;
}
private function lookForTypeSpecificationsInEarlyTermination(Scope $scope, Node $node): Scope
{
$types = $this->typeSpecifier->specifyTypesInCondition(new SpecifiedTypes(), $scope, $node);
foreach ($types->getSureNotTypes() as $type) {
$scope = $scope->specifyExpressionType($type[0], $type[1]);
}
return $scope;
}
private function lookForAssigns(Scope $scope, \PhpParser\Node $node): Scope
{
if ($node instanceof StaticVar) {
$scope = $scope->assignVariable($node->name, $node->default !== null ? $scope->getType($node->default) : null);
} elseif ($node instanceof Static_) {
foreach ($node->vars as $var) {
$scope = $this->lookForAssigns($scope, $var);
}
} elseif ($node instanceof If_) {
$scope = $this->lookForAssigns($scope, $node->cond);
$statements = [
new StatementList($scope, array_merge([$node->cond], $node->stmts)),
new StatementList($scope, $node->else !== null ? $node->else->stmts : ($this->defineVariablesWithoutDefaultBranch ? null : [])),
];
foreach ($node->elseifs as $elseIf) {
$statements[] = new StatementList($scope, array_merge([$elseIf->cond], $elseIf->stmts));
}
$scope = $this->lookForAssignsInBranches($scope, $statements);
} elseif ($node instanceof TryCatch) {
$statements = [
new StatementList($scope, $node->stmts),
];
foreach ($node->catches as $catch) {
if (isset($catch->types)) {
$catchTypes = $catch->types;
} elseif (isset($catch->type)) {
$catchTypes = [$catch->type];
} else {
throw new \PHPStan\ShouldNotHappenException();
}
$statements[] = new StatementList($scope->enterCatch(
$catchTypes,
$catch->var
), $catch->stmts);
}
$scope = $this->lookForAssignsInBranches($scope, $statements);
} elseif ($node instanceof MethodCall || $node instanceof FuncCall || $node instanceof Expr\StaticCall) {
if ($node instanceof MethodCall) {
$scope = $this->lookForAssigns($scope, $node->var);
}
foreach ($node->args as $argument) {
$scope = $this->lookForAssigns($scope, $argument);
}
$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());
}
}
}
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 StringType(false), false));
}
} elseif ($node instanceof BinaryOp) {
$scope = $this->lookForAssigns($scope, $node->left);
$scope = $this->lookForAssigns($scope, $node->right);
} elseif ($node instanceof Arg) {
$scope = $this->lookForAssigns($scope, $node->value);
} elseif ($node instanceof BooleanNot) {
$scope = $this->lookForAssigns($scope, $node->expr);
} elseif ($node instanceof Ternary) {
$scope = $this->lookForAssigns($scope, $node->cond);
} elseif ($node instanceof List_) {
if (isset($node->items)) {
$nodeItems = $node->items;
} elseif (isset($node->vars)) {
$nodeItems = $node->vars;
} else {
throw new \PHPStan\ShouldNotHappenException();
}
foreach ($nodeItems as $item) {
if ($item === null) {
continue;
}
$itemValue = $item;
if ($itemValue instanceof ArrayItem) {
$itemValue = $itemValue->value;
}
if ($itemValue instanceof Variable && is_string($itemValue->name)) {
$scope = $scope->assignVariable($itemValue->name);
} elseif ($itemValue instanceof ArrayDimFetch && $itemValue->var instanceof Variable && is_string($itemValue->var->name)) {
$scope = $scope->assignVariable($itemValue->var->name);
} else {
$scope = $this->lookForAssigns($scope, $itemValue);
}
}
} elseif ($node instanceof Array_) {
foreach ($node->items as $item) {
$scope = $this->lookForAssigns($scope, $item->value);
}
} elseif ($node instanceof New_) {
foreach ($node->args as $arg) {
$scope = $this->lookForAssigns($scope, $arg);
}
} elseif ($node instanceof Do_) {
foreach ($node->stmts as $statement) {
$scope = $this->lookForAssigns($scope, $statement);
}
} elseif ($node instanceof Switch_) {
$statements = [];
$hasDefault = false;
foreach ($node->cases as $case) {
if ($case->cond === null) {
$hasDefault = true;
}
$statements[] = new StatementList($scope, $case->stmts);
}
if (!$hasDefault) {
$statements[] = new StatementList($scope, []);
}
$scope = $this->lookForAssignsInBranches($scope, $statements, true);
} elseif ($node instanceof Cast) {
$scope = $this->lookForAssigns($scope, $node->expr);
} elseif ($node instanceof For_) {
if ($this->polluteScopeWithLoopInitialAssignments) {
foreach ($node->init as $initExpr) {
$scope = $this->lookForAssigns($scope, $initExpr);
}
foreach ($node->cond as $condExpr) {
$scope = $this->lookForAssigns($scope, $condExpr);
}
}
$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);
} elseif ($node instanceof While_) {
if ($this->polluteScopeWithLoopInitialAssignments) {
$scope = $this->lookForAssigns($scope, $node->cond);
}
$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);
} elseif ($node instanceof ErrorSuppress) {
$scope = $this->lookForAssigns($scope, $node->expr);
} 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);
}
} elseif ($node instanceof Print_) {
$scope = $this->lookForAssigns($scope, $node->expr);
} elseif ($node instanceof Foreach_) {
$scope = $this->lookForAssigns($scope, $node->expr);
$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);
} elseif ($node instanceof Isset_) {
foreach ($node->vars as $var) {
$scope = $this->lookForAssigns($scope, $var);
}
} elseif ($node instanceof Expr\Empty_) {
$scope = $this->lookForAssigns($scope, $node->expr);
} elseif ($node instanceof ArrayDimFetch && $node->dim !== null) {
$scope = $this->lookForAssigns($scope, $node->dim);
} elseif ($node instanceof Expr\Closure) {
foreach ($node->uses as $closureUse) {
if (!$closureUse->byRef || $scope->hasVariableType($closureUse->var)) {
continue;
}
$scope = $scope->assignVariable($closureUse->var, new MixedType());
}
}
$scope = $this->updateScopeForVariableAssign($scope, $node);
return $scope;
}
private function updateScopeForVariableAssign(Scope $scope, \PhpParser\Node $node): Scope
{
if ($node instanceof Assign || $node instanceof AssignRef || $node instanceof Isset_) {
if ($node instanceof Assign || $node instanceof AssignRef) {
$vars = [$node->var];
} elseif ($node instanceof Isset_) {
$vars = $node->vars;
} else {
throw new \PHPStan\ShouldNotHappenException();
}
foreach ($vars as $var) {
$scope = $this->assignVariable(
$scope,
$var,
($node instanceof Assign || $node instanceof AssignRef) ? $scope->getType($node->expr) : null
);
}
if ($node instanceof Assign || $node instanceof AssignRef) {
$scope = $this->lookForAssigns($scope, $node->expr);
$comment = CommentHelper::getDocComment($node);
if ($comment !== null && $node->var instanceof Variable && is_string($node->var->name)) {
$variableName = $node->var->name;
$processVarAnnotation = function (string $matchedType, string $matchedVariableName) use ($scope, $variableName): Scope {
$fileTypeMap = $this->fileTypeMapper->getTypeMap($scope->getFile());
if (isset($fileTypeMap[$matchedType]) && $matchedVariableName === $variableName) {
return $scope->assignVariable($matchedVariableName, $fileTypeMap[$matchedType]);
}
return $scope;
};
if (preg_match('#@var\s+' . FileTypeMapper::TYPE_PATTERN . '\s+\$([a-zA-Z0-9_]+)#', $comment, $matches)) {
$scope = $processVarAnnotation($matches[1], $matches[2]);
} elseif (preg_match('#@var\s+\$([a-zA-Z0-9_]+)\s+' . FileTypeMapper::TYPE_PATTERN . '#', $comment, $matches)) {
$scope = $processVarAnnotation($matches[2], $matches[1]);
}
}
}
}
return $scope;
}
private function assignVariable(Scope $scope, Node $var, Type $subNodeType = null): Scope
{
if ($var instanceof Variable && is_string($var->name)) {
$scope = $scope->assignVariable($var->name, $subNodeType);
} elseif ($var instanceof ArrayDimFetch) {
$depth = 0;
while ($var instanceof ArrayDimFetch) {
$var = $var->var;
$depth++;
}
if ($var instanceof Variable && is_string($var->name)) {
$arrayType = ArrayType::createDeepArrayType(
new NestedArrayItemType($subNodeType !== null ? $subNodeType : new MixedType(), $depth),
false
);
if ($scope->hasVariableType($var->name)) {
$arrayType = $scope->getVariableType($var->name)->combineWith($arrayType);
}
$scope = $scope->assignVariable($var->name, $arrayType);
}
if (isset($var->dim)) {
$scope = $this->lookForAssigns($scope, $var->dim);
}
} else {
$scope = $this->lookForAssigns($scope, $var);
}
return $scope;
}
/**
* @param \PHPStan\Analyser\Scope $initialScope
* @param \PHPStan\Analyser\StatementList[] $statementsLists
* @param bool $isSwitchCase
* @return Scope
*/
private function lookForAssignsInBranches(Scope $initialScope, array $statementsLists, bool $isSwitchCase = false): Scope
{
/** @var \PHPStan\Analyser\Scope|null $intersectedScope */
$intersectedScope = null;
/** @var \PHPStan\Analyser\Scope|null $previousBranchScope */
$previousBranchScope = null;
$allBranchesScope = $initialScope;
foreach ($statementsLists as $i => $statementList) {
$statements = $statementList->getStatements();
$branchScope = $statementList->getScope();
if ($statements === null) {
continue;
}
$earlyTerminationStatement = null;
foreach ($statements as $statement) {
$branchScope = $this->lookForAssigns($branchScope, $statement);
$earlyTerminationStatement = $this->findStatementEarlyTermination($statement, $branchScope);
if ($earlyTerminationStatement !== null) {
if (!$isSwitchCase) {
$allBranchesScope = $allBranchesScope->addVariables($branchScope);
continue 2;
}
break;
}
}
$allBranchesScope = $allBranchesScope->addVariables($branchScope);
if ($intersectedScope === null) {
$intersectedScope = $initialScope->addVariables($branchScope);
} elseif ($isSwitchCase && $previousBranchScope !== null) {
$intersectedScope = $branchScope->addVariables($previousBranchScope);
} elseif ($earlyTerminationStatement === null || $earlyTerminationStatement instanceof Break_) {
$intersectedScope = $branchScope->intersectVariables($intersectedScope);
}
if ($earlyTerminationStatement === null) {
$previousBranchScope = $branchScope;
} else {
$previousBranchScope = null;
}
}
if ($intersectedScope !== null) {
return $intersectedScope->addVariables($allBranchesScope->intersectVariables($initialScope));
}
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);
if ($methodCalledOnType->getClass() === null) {
return null;
}
if (!$this->broker->hasClass($methodCalledOnType->getClass())) {
return null;
}
$classReflection = $this->broker->getClass($methodCalledOnType->getClass());
foreach (array_merge([$methodCalledOnType->getClass()], $classReflection->getParentClassesNames()) as $className) {
if (!isset($this->earlyTerminatingMethodCalls[$className])) {
continue;
}
if (in_array($statement->name, $this->earlyTerminatingMethodCalls[$className], true)) {
return $statement;
}
}
return null;
}
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);
if ($type->getClass() !== null && $this->broker->hasClass($type->getClass())) {
$classReflection = $this->broker->getClass($type->getClass());
$methodName = $functionCall->name;
if ($classReflection->hasMethod($methodName)) {
return $classReflection->getMethod($methodName);
}
}
} elseif (
$functionCall instanceof Expr\StaticCall
&& $functionCall->class instanceof Name
&& is_string($functionCall->name)) {
$className = (string) $functionCall->class;
if ($this->broker->hasClass($className)) {
$classReflection = $this->broker->getClass($className);
if ($classReflection->hasMethod($functionCall->name)) {
return $classReflection->getMethod($functionCall->name);
}
}
}
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);
$fileName = $traitReflection->getNativeReflection()->getFileName();
$parserNodes = $this->parser->parseFile($fileName);
$classScope = $classScope->changeAnalysedContextFile(
sprintf(
'%s (in context of %s)',
$fileName,
$classScope->getClass() !== null ? sprintf('class %s', $classScope->getClass()) : 'anonymous class'
)
);
$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();
if ($scope->getClass() !== null && $functionLike instanceof Node\Stmt\ClassMethod) {
$phpDocBlock = PhpDocBlock::resolvePhpDocBlockForMethod(
$this->broker,
$docComment,
$scope->getClass(),
$functionLike->name,
$file
);
$docComment = $phpDocBlock->getDocComment();
$file = $phpDocBlock->getFile();
}
$fileTypeMap = $this->fileTypeMapper->getTypeMap($file);
$phpDocParameterTypes = TypehintHelper::getParameterTypesFromPhpDoc(
$fileTypeMap,
array_map(function (Param $parameter): string {
return $parameter->name;
}, $functionLike->getParams()),
$docComment
);
$phpDocReturnType = TypehintHelper::getReturnTypeFromPhpDoc($fileTypeMap, $docComment);
}
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 00000013203 13436751643 0011417 0 ustar 00 broker = $broker;
$this->parser = $parser;
$this->registry = $registry;
$this->nodeScopeResolver = $nodeScopeResolver;
$this->printer = $printer;
$this->analyseExcludes = array_map(function (string $exclude) use ($fileHelper): string {
$normalized = $fileHelper->normalizePath($exclude);
if ($this->isFnmatchPattern($normalized)) {
return $normalized;
}
return $fileHelper->absolutizePath($normalized);
}, $analyseExcludes);
$this->ignoreErrors = $ignoreErrors;
$this->bootstrapFile = $bootstrapFile;
$this->fileHelper = $fileHelper;
}
/**
* @param string[] $files
* @param bool $onlyFiles
* @param \Closure|null $progressCallback
* @return string[]|\PHPStan\Analyser\Error[] errors
*/
public function analyse(array $files, bool $onlyFiles, \Closure $progressCallback = null): 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;
}
foreach ($files as $file) {
$file = $this->fileHelper->normalizePath($file);
try {
if ($this->isExcludedFromAnalysing($file)) {
if ($progressCallback !== null) {
$progressCallback($file);
}
continue;
}
$fileErrors = [];
$this->nodeScopeResolver->processNodes(
$this->parser->parseFile($file),
new Scope($this->broker, $this->printer, $file),
function (\PhpParser\Node $node, Scope $scope) use (&$fileErrors) {
if ($node instanceof \PhpParser\Node\Stmt\Trait_) {
return;
}
$classes = array_merge([get_class($node)], class_parents($node));
foreach ($this->registry->getRules($classes) as $rule) {
$ruleErrors = $this->createErrors(
$node,
$scope->getAnalysedContextFile(),
$rule->processNode($node, $scope)
);
$fileErrors = array_merge($fileErrors, $ruleErrors);
}
}
);
if ($progressCallback !== null) {
$progressCallback($file);
}
$errors = array_merge($errors, $fileErrors);
} catch (\PhpParser\Error $e) {
$errors[] = new Error($e->getMessage(), $file, $e->getStartLine() !== -1 ? $e->getStartLine() : null);
} catch (\PHPStan\AnalysedCodeException $e) {
$errors[] = new Error($e->getMessage(), $file);
} catch (\Throwable $t) {
$errors[] = new Error(sprintf('Internal error: %s', $t->getMessage()), $file);
}
}
$unmatchedIgnoredErrors = $this->ignoreErrors;
$errors = array_values(array_filter($errors, function (string $error) use (&$unmatchedIgnoredErrors): bool {
foreach ($this->ignoreErrors as $i => $ignore) {
if (\Nette\Utils\Strings::match($error, $ignore) !== null) {
unset($unmatchedIgnoredErrors[$i]);
return false;
}
}
return true;
}));
if (!$onlyFiles) {
foreach ($unmatchedIgnoredErrors as $unmatchedIgnoredError) {
$errors[] = sprintf(
'Ignored error pattern %s was not matched in reported errors.',
$unmatchedIgnoredError
);
}
}
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;
}
public function isExcludedFromAnalysing(string $file): bool
{
foreach ($this->analyseExcludes as $exclude) {
if (strpos($file, $exclude) === 0) {
return true;
}
if ($this->isFnmatchPattern($exclude) && fnmatch($exclude, $file, DIRECTORY_SEPARATOR === '\\' ? FNM_NOESCAPE : 0)) {
return true;
}
}
return false;
}
private function isFnmatchPattern(string $path): bool
{
return preg_match('~[*?[\]]~', $path) > 0;
}
}
src/Analyser/Error.php 0000666 00000001516 13436751643 0010736 0 ustar 00 message = $message;
$this->file = $file;
$this->line = $line;
}
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 __toString(): string
{
$message = trim($this->message, '.');
if ($this->line !== null) {
return sprintf('%s in %s on line %d', $message, $this->file, $this->line);
}
return sprintf('%s in %s', $message, $this->file);
}
}
src/Analyser/StatementList.php 0000666 00000001004 13436751643 0012435 0 ustar 00 scope = $scope;
$this->statements = $statements;
}
public function getScope(): Scope
{
return $this->scope;
}
/**
* @return \PhpParser\Node[]|null
*/
public function getStatements()
{
return $this->statements;
}
}
src/Analyser/NameScope.php 0000666 00000001607 13436751643 0011520 0 ustar 00 fullName(string)
*/
private $uses;
public function __construct(string $namespace = null, array $uses)
{
$this->namespace = $namespace;
$this->uses = $uses;
}
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 00000010165 13436751643 0012420 0 ustar 00 printer = $printer;
}
public function specifyTypesInCondition(
SpecifiedTypes $types,
Scope $scope,
Node $expr,
bool $negated = false,
int $source = self::SOURCE_UNKNOWN
): SpecifiedTypes
{
if ($expr instanceof Instanceof_ && $expr->class instanceof Name) {
$class = (string) $expr->class;
if ($class === 'self' && $scope->getClass() !== null) {
$type = new ObjectType($scope->getClass(), false);
} elseif ($class === 'static' && $scope->getClass() !== null) {
$type = new StaticType($scope->getClass(), false);
} else {
$type = new ObjectType($class, false);
}
$printedExpr = $this->printer->prettyPrintExpr($expr->expr);
if ($negated) {
if ($source === self::SOURCE_FROM_AND) {
return $types;
}
return $types->addSureNotType($expr->expr, $printedExpr, $type);
}
return $types->addSureType($expr->expr, $printedExpr, $type);
} elseif (
$expr instanceof FuncCall
&& $expr->name instanceof Name
&& isset($expr->args[0])
) {
$functionName = (string) $expr->name;
$argumentExpression = $expr->args[0]->value;
$specifiedType = null;
if (in_array($functionName, [
'is_int',
'is_integer',
'is_long',
], true)) {
$specifiedType = new IntegerType(false);
} elseif (in_array($functionName, [
'is_float',
'is_double',
'is_real',
], true)) {
$specifiedType = new FloatType(false);
} elseif ($functionName === 'is_null') {
$specifiedType = new NullType();
} elseif ($functionName === 'is_array') {
$specifiedType = new ArrayType(new MixedType(), false);
} elseif ($functionName === 'is_bool') {
$specifiedType = new TrueOrFalseBooleanType(false);
} elseif ($functionName === 'is_callable') {
$specifiedType = new CallableType(false);
} elseif ($functionName === 'is_resource') {
$specifiedType = new ResourceType(false);
} elseif ($functionName === 'is_iterable') {
$specifiedType = new IterableIterableType(new MixedType(), false);
} elseif ($functionName === 'is_string') {
$specifiedType = new StringType(false);
}
if ($specifiedType !== null) {
$printedExpr = $this->printer->prettyPrintExpr($argumentExpression);
if ($negated) {
return $types->addSureNotType($argumentExpression, $printedExpr, $specifiedType);
}
return $types->addSureType($argumentExpression, $printedExpr, $specifiedType);
}
} elseif ($expr instanceof BooleanAnd) {
if ($source !== self::SOURCE_UNKNOWN && $source !== self::SOURCE_FROM_AND) {
return $types;
}
$types = $this->specifyTypesInCondition($types, $scope, $expr->left, $negated, self::SOURCE_FROM_AND);
$types = $this->specifyTypesInCondition($types, $scope, $expr->right, $negated, self::SOURCE_FROM_AND);
} elseif ($expr instanceof BooleanOr) {
if ($negated) {
return $types;
}
$types = $this->specifyTypesInCondition($types, $scope, $expr->left, $negated, self::SOURCE_FROM_OR);
$types = $this->specifyTypesInCondition($types, $scope, $expr->right, $negated, self::SOURCE_FROM_OR);
} elseif ($expr instanceof Node\Expr\BooleanNot) {
if ($source === self::SOURCE_FROM_AND) {
return $types;
}
$types = $this->specifyTypesInCondition($types, $scope, $expr->expr, !$negated, $source);
}
return $types;
}
}
src/Analyser/Scope.php 0000666 00000120563 13436751643 0010722 0 ustar 00 broker = $broker;
$this->printer = $printer;
$this->file = $file;
$this->analysedContextFile = $analysedContextFile !== null ? $analysedContextFile : $file;
$this->declareStrictTypes = $declareStrictTypes;
$this->class = $class;
$this->function = $function;
$this->namespace = $namespace;
$this->variableTypes = $variablesTypes;
$this->inClosureBindScopeClass = $inClosureBindScopeClass;
$this->inAnonymousFunctionReturnType = $inAnonymousFunctionReturnType;
$this->anonymousClass = $anonymousClass;
$this->inFunctionCall = $inFunctionCall;
$this->negated = $negated;
$this->moreSpecificTypes = $moreSpecificTypes;
$this->inFirstLevelStatement = $inFirstLevelStatement;
$this->currentlyAssignedVariables = $currentlyAssignedVariables;
}
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->getFile(),
$this->getAnalysedContextFile(),
true
);
}
/**
* @return null|string
*/
public function getClass()
{
return $this->class;
}
/**
* @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\Type\Type[]
*/
public function getVariableTypes(): array
{
return $this->variableTypes;
}
public function hasVariableType(string $variableName): bool
{
return isset($this->variableTypes[$variableName]);
}
public function getVariableType(string $variableName): Type
{
if (!$this->hasVariableType($variableName)) {
throw new \PHPStan\Analyser\UndefinedVariableException($this, $variableName);
}
return $this->variableTypes[$variableName];
}
public function isInAnonymousFunction(): bool
{
return $this->inAnonymousFunctionReturnType !== null;
}
/**
* @return \PHPStan\Type\Type|null
*/
public function getAnonymousFunctionReturnType()
{
return $this->inAnonymousFunctionReturnType;
}
public function isInAnonymousClass(): bool
{
return $this->anonymousClass !== null;
}
public function getAnonymousClass(): ClassReflection
{
return $this->anonymousClass;
}
/**
* @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
{
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
) {
return new TrueOrFalseBooleanType(false);
}
if (
$node instanceof Node\Expr\UnaryMinus
|| $node instanceof Node\Expr\UnaryPlus
) {
return $this->getType($node->expr);
}
if ($node instanceof Node\Expr\BinaryOp\Mod) {
return new IntegerType(false);
}
if ($node instanceof Expr\BinaryOp\Concat) {
return new StringType(false);
}
if ($node instanceof Expr\BinaryOp\Spaceship) {
return new IntegerType(false);
}
if ($node instanceof Expr\Ternary) {
$elseType = $this->getType($node->else);
if ($node->if === null) {
return $this->getType($node->cond)->combineWith($elseType);
}
return $this->getType($node->if)->combineWith($elseType);
}
if ($node instanceof Expr\BinaryOp\Coalesce) {
return $this->getType($node->left)->combineWith($this->getType($node->right));
}
if ($node instanceof Expr\Clone_) {
return $this->getType($node->expr);
}
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($leftType->isNullable());
}
if ($rightType instanceof BooleanType) {
$rightType = new IntegerType($rightType->isNullable());
}
if ($node instanceof Expr\AssignOp\Div || $node instanceof Expr\BinaryOp\Div) {
if (!$leftType instanceof MixedType && !$rightType instanceof MixedType) {
return new FloatType(false);
}
}
if (
($leftType instanceof FloatType && !$rightType instanceof MixedType)
|| ($rightType instanceof FloatType && !$leftType instanceof MixedType)
) {
return new FloatType(false);
}
if ($leftType instanceof IntegerType && $rightType instanceof IntegerType) {
return new IntegerType(false);
}
}
if ($node instanceof LNumber) {
return new IntegerType(false);
} elseif ($node instanceof ConstFetch) {
$constName = strtolower((string) $node->name);
if ($constName === 'true') {
return new \PHPStan\Type\TrueBooleanType(false);
} elseif ($constName === 'false') {
return new \PHPStan\Type\FalseBooleanType(false);
} elseif ($constName === 'null') {
return new NullType();
}
} elseif ($node instanceof String_) {
return new StringType(false);
} elseif ($node instanceof DNumber) {
return new FloatType(false);
} elseif ($node instanceof Expr\Closure) {
return new ObjectType('Closure', false);
} 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->getClass(), false);
} elseif ($node->class->parts[0] === 'self') {
return new ObjectType($this->getClass(), false);
}
}
return new ObjectType((string) $node->class, false);
}
} elseif ($node instanceof Array_) {
$possiblyCallable = false;
if (count($node->items) === 2) {
$firstItem = $node->items[0]->value;
if (
(
$this->getType($firstItem)->getClass() !== null
|| $this->getType($firstItem) instanceof StringType
)
&& $this->getType($node->items[1]->value) instanceof StringType
) {
$possiblyCallable = true;
}
}
return new ArrayType($this->getCombinedType(array_map(function (Expr\ArrayItem $item): Type {
return $this->getType($item->value);
}, $node->items)), false, true, $possiblyCallable);
} elseif ($node instanceof Int_) {
return new IntegerType(false);
} elseif ($node instanceof Bool_) {
return new TrueOrFalseBooleanType(false);
} elseif ($node instanceof Double) {
return new FloatType(false);
} elseif ($node instanceof \PhpParser\Node\Expr\Cast\String_) {
return new StringType(false);
} elseif ($node instanceof \PhpParser\Node\Expr\Cast\Array_) {
return new ArrayType(new MixedType(), false);
} elseif ($node instanceof Object_) {
return new ObjectType('stdClass', false);
} elseif ($node instanceof Unset_) {
return new NullType();
} elseif ($node instanceof Node\Expr\ClassConstFetch) {
if ($node->class instanceof Name) {
$constantClass = (string) $node->class;
if ($constantClass === 'self') {
$constantClass = $this->getClass();
}
} elseif ($node->class instanceof Expr) {
$constantClassType = $this->getType($node->class);
if ($constantClassType->getClass() !== null) {
$constantClass = $constantClassType->getClass();
}
}
if (isset($constantClass)) {
$constantName = $node->name;
if (strtolower($constantName) === 'class') {
return new StringType(false);
}
if ($this->broker->hasClass($constantClass)) {
$constantClassReflection = $this->broker->getClass($constantClass);
if ($constantClassReflection->hasConstant($constantName)) {
$constant = $constantClassReflection->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)) {
return new MixedType();
}
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);
if (
$methodCalledOnType->getClass() !== null
&& $this->broker->hasClass($methodCalledOnType->getClass())
) {
$methodClassReflection = $this->broker->getClass(
$methodCalledOnType->getClass()
);
if (!$methodClassReflection->hasMethod($node->name)) {
return new MixedType();
}
$methodReflection = $methodClassReflection->getMethod($node->name);
foreach ($this->broker->getDynamicMethodReturnTypeExtensionsForClass($methodCalledOnType->getClass()) as $dynamicMethodReturnTypeExtension) {
if (!$dynamicMethodReturnTypeExtension->isMethodSupported($methodReflection)) {
continue;
}
return $dynamicMethodReturnTypeExtension->getTypeFromMethodCall($methodReflection, $node, $this);
}
$calledOnThis = $node->var instanceof Variable && is_string($node->var->name) && $node->var->name === 'this';
if (!$calledOnThis && $methodReflection->getReturnType() instanceof StaticResolvableType) {
return $methodReflection->getReturnType()->resolveStatic($methodCalledOnType->getClass());
}
return $methodReflection->getReturnType();
}
}
if ($node instanceof Expr\StaticCall && is_string($node->name) && $node->class instanceof Name) {
$calleeClass = $this->resolveName($node->class);
if ($calleeClass !== null && $this->broker->hasClass($calleeClass)) {
$staticMethodClassReflection = $this->broker->getClass($calleeClass);
if (!$staticMethodClassReflection->hasMethod($node->name)) {
return new MixedType();
}
$staticMethodReflection = $staticMethodClassReflection->getMethod($node->name);
foreach ($this->broker->getDynamicStaticMethodReturnTypeExtensionsForClass($calleeClass) as $dynamicStaticMethodReturnTypeExtension) {
if (!$dynamicStaticMethodReturnTypeExtension->isStaticMethodSupported($staticMethodReflection)) {
continue;
}
return $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall($staticMethodReflection, $node, $this);
}
if ($staticMethodReflection->getReturnType() instanceof StaticResolvableType) {
$nodeClassString = (string) $node->class;
if ($nodeClassString === 'parent' && $this->getClass() !== null) {
return $staticMethodReflection->getReturnType()->changeBaseClass($this->getClass());
}
return $staticMethodReflection->getReturnType()->resolveStatic($calleeClass);
}
return $staticMethodReflection->getReturnType();
}
}
if ($node instanceof PropertyFetch && is_string($node->name)) {
$propertyFetchedOnType = $this->getType($node->var);
if (
$propertyFetchedOnType->getClass() !== null
&& $this->broker->hasClass($propertyFetchedOnType->getClass())
) {
$propertyClassReflection = $this->broker->getClass(
$propertyFetchedOnType->getClass()
);
if (!$propertyClassReflection->hasProperty($node->name)) {
return new MixedType();
}
return $propertyClassReflection->getProperty($node->name, $this)->getType();
}
}
if ($node instanceof Expr\StaticPropertyFetch && is_string($node->name) && $node->class instanceof Name) {
$staticPropertyHolderClass = $this->resolveName($node->class);
if ($staticPropertyHolderClass !== null && $this->broker->hasClass($staticPropertyHolderClass)) {
$staticPropertyClassReflection = $this->broker->getClass(
$staticPropertyHolderClass
);
if (!$staticPropertyClassReflection->hasProperty($node->name)) {
return new MixedType();
}
return $staticPropertyClassReflection->getProperty($node->name, $this)->getType();
}
}
if ($node instanceof FuncCall && $node->name instanceof Name) {
$arrayFunctionsThatDependOnClosureReturnType = [
'array_map' => 0,
'array_reduce' => 1,
];
$functionName = (string) $node->name;
if (
isset($arrayFunctionsThatDependOnClosureReturnType[$functionName])
&& isset($node->args[$arrayFunctionsThatDependOnClosureReturnType[$functionName]])
&& $node->args[$arrayFunctionsThatDependOnClosureReturnType[$functionName]]->value instanceof Expr\Closure
) {
$closure = $node->args[$arrayFunctionsThatDependOnClosureReturnType[$functionName]]->value;
$anonymousFunctionType = $this->getFunctionType($closure->returnType, $closure->returnType === null, false);
if ($functionName === 'array_reduce') {
return $anonymousFunctionType;
}
return new ArrayType(
$anonymousFunctionType,
false
);
}
$arrayFunctionsThatDependOnArgumentType = [
'array_filter' => 0,
'array_unique' => 0,
'array_reverse' => 0,
];
if (
isset($arrayFunctionsThatDependOnArgumentType[$functionName])
&& isset($node->args[$arrayFunctionsThatDependOnArgumentType[$functionName]])
) {
$argumentValue = $node->args[$arrayFunctionsThatDependOnArgumentType[$functionName]]->value;
return $this->getType($argumentValue);
}
$arrayFunctionsThatCreateArrayBasedOnArgumentType = [
'array_fill' => 2,
'array_fill_keys' => 1,
];
if (
isset($arrayFunctionsThatCreateArrayBasedOnArgumentType[$functionName])
&& isset($node->args[$arrayFunctionsThatCreateArrayBasedOnArgumentType[$functionName]])
) {
$argumentValue = $node->args[$arrayFunctionsThatCreateArrayBasedOnArgumentType[$functionName]]->value;
return new ArrayType($this->getType($argumentValue), false, true);
}
if (!$this->broker->hasFunction($node->name, $this)) {
return new MixedType();
}
return $this->broker->getFunction($node->name, $this)->getReturnType();
}
return new MixedType();
}
/**
* @param \PhpParser\Node\Name $name
* @return string|null
*/
public function resolveName(Name $name)
{
$originalClass = (string) $name;
if ($originalClass === 'self' || $originalClass === 'static') {
return $this->getClass();
} elseif ($originalClass === 'parent' && $this->getClass() !== null && $this->broker->hasClass($this->getClass())) {
$currentClassReflection = $this->broker->getClass($this->getClass());
if ($currentClassReflection->getParentClass() !== false) {
return $currentClassReflection->getParentClass()->getName();
}
} else {
return $originalClass;
}
return null;
}
/**
* @param mixed $value
* @return Type|null
*/
private function getTypeFromValue($value)
{
if (is_int($value)) {
return new IntegerType(false);
} elseif (is_float($value)) {
return new FloatType(false);
} elseif (is_bool($value)) {
return new TrueOrFalseBooleanType(false);
} elseif ($value === null) {
return new NullType();
} elseif (is_string($value)) {
return new StringType(false);
} elseif (is_array($value)) {
return new ArrayType($this->getCombinedType(array_map(function ($value): Type {
return $this->getTypeFromValue($value);
}, $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();
}
$itemType = reset($types);
array_shift($types);
foreach ($types as $type) {
if ($itemType === null) {
$itemType = $type;
continue;
}
$itemType = $itemType->combineWith($type);
}
return $itemType;
}
public function isSpecified(Expr $node): bool
{
$exprString = $this->printer->prettyPrintExpr($node);
return isset($this->moreSpecificTypes[$exprString]);
}
public function enterClass(string $className): self
{
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$className,
null,
$this->getNamespace(),
[
'this' => new ThisType($className, false),
]
);
}
public function changeAnalysedContextFile(string $fileName): self
{
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$fileName,
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$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->getClass() !== null ? $this->broker->getClass($this->getClass()) : $this->getAnonymousClass(),
$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()] = $parameter->getType();
}
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$functionReflection,
$this->getNamespace(),
$variableTypes,
null,
null,
$this->anonymousClass
);
}
public function enterNamespace(string $namespaceName): self
{
return new self(
$this->broker,
$this->printer,
$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'] = $thisType;
} else {
unset($variableTypes['this']);
}
if ($scopeClass === 'static') {
$scopeClass = $this->getClass();
}
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$variableTypes,
$scopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes
);
}
public function enterAnonymousClass(ClassReflection $anonymousClass): self
{
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
null,
null,
$this->getNamespace(),
[
'this' => new MixedType(),
],
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$anonymousClass,
$this->getInFunctionCall()
);
}
/**
* @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] = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
}
foreach ($uses as $use) {
if (!$this->hasVariableType($use->var)) {
if ($use->byRef) {
$variableTypes[$use->var] = new MixedType();
}
continue;
}
$variableTypes[$use->var] = $this->getVariableType($use->var);
}
if ($this->hasVariableType('this')) {
$variableTypes['this'] = $this->getVariableType('this');
}
$returnType = $this->getFunctionType($returnTypehint, $returnTypehint === null, false);
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$variableTypes,
$this->inClosureBindScopeClass,
$returnType,
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCall()
);
}
private function isParameterValueNullable(Node\Param $parameter): bool
{
if ($parameter->default instanceof ConstFetch && $parameter->default->name instanceof Name) {
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
*/
private function getFunctionType($type = null, bool $isNullable, bool $isVariadic): Type
{
if ($isVariadic) {
return new ArrayType($this->getFunctionType(
$type,
$isNullable,
false
), false);
}
if ($type === null) {
return new MixedType();
} elseif ($type === 'string') {
return new StringType($isNullable);
} elseif ($type === 'int') {
return new IntegerType($isNullable);
} elseif ($type === 'bool') {
return new TrueOrFalseBooleanType($isNullable);
} elseif ($type === 'float') {
return new FloatType($isNullable);
} elseif ($type === 'callable') {
return new CallableType($isNullable);
} elseif ($type === 'array') {
return new ArrayType(new MixedType(), $isNullable);
} elseif ($type instanceof Name) {
$className = (string) $type;
if ($className === 'self') {
$className = $this->getClass();
} elseif (
$className === 'parent'
) {
if (
$this->getClass() !== null
&& $this->broker->hasClass($this->getClass())
) {
$classReflection = $this->broker->getClass($this->getClass());
} elseif ($this->isInAnonymousClass()) {
$classReflection = $this->getAnonymousClass();
} else {
return new NonexistentParentClassType(false);
}
if ($classReflection->getParentClass() !== false) {
return new ObjectType($classReflection->getParentClass()->getName(), $isNullable);
}
return new NonexistentParentClassType(false);
}
return new ObjectType($className, $isNullable);
} elseif ($type === 'iterable') {
return new IterableIterableType(new MixedType(), $isNullable);
} elseif ($type === 'void') {
return new VoidType();
} 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);
$variableTypes = $this->getVariableTypes();
if ($iterateeType instanceof IterableType) {
$variableTypes[$valueName] = $iterateeType->getItemType();
} else {
$variableTypes[$valueName] = new MixedType();
}
if ($keyName !== null) {
$variableTypes[$keyName] = new MixedType();
}
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$variableTypes,
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
null,
$this->isNegated(),
$this->moreSpecificTypes
);
}
/**
* @param \PhpParser\Node\Name[] $classes
* @param string $variableName
* @return Scope
*/
public function enterCatch(array $classes, string $variableName): self
{
$variableTypes = $this->getVariableTypes();
if (count($classes) === 1) {
$type = new ObjectType((string) $classes[0], false);
} else {
$type = new MixedType();
}
$variableTypes[$variableName] = $type;
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$variableTypes,
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
null,
$this->isNegated(),
$this->moreSpecificTypes
);
}
/**
* @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->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$functionCall,
$this->isNegated(),
$this->moreSpecificTypes,
$this->inFirstLevelStatement
);
}
public function enterVariableAssign(string $variableName): self
{
$currentlyAssignedVariables = $this->currentlyAssignedVariables;
$currentlyAssignedVariables[] = $variableName;
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes,
$this->isInFirstLevelStatement(),
$currentlyAssignedVariables
);
}
public function isInVariableAssign(string $variableName): bool
{
return in_array($variableName, $this->currentlyAssignedVariables, true);
}
public function assignVariable(
string $variableName,
Type $type = null
): self
{
$variableTypes = $this->getVariableTypes();
$variableTypes[$variableName] = $type !== null
? $type
: new MixedType();
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$variableTypes,
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes,
$this->inFirstLevelStatement
);
}
public function unsetVariable(string $variableName): self
{
if (!$this->hasVariableType($variableName)) {
return $this;
}
$variableTypes = $this->getVariableTypes();
unset($variableTypes[$variableName]);
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$variableTypes,
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes,
$this->inFirstLevelStatement
);
}
public function intersectVariables(Scope $otherScope): self
{
$ourVariableTypes = $this->getVariableTypes();
$theirVariableTypes = $otherScope->getVariableTypes();
$intersectedVariableTypes = [];
foreach ($ourVariableTypes as $name => $variableType) {
if (!isset($theirVariableTypes[$name])) {
continue;
}
$intersectedVariableTypes[$name] = $variableType->combineWith($theirVariableTypes[$name]);
}
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$intersectedVariableTypes,
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes,
$this->inFirstLevelStatement
);
}
public function addVariables(Scope $otherScope): self
{
$variableTypes = $this->getVariableTypes();
foreach ($otherScope->getVariableTypes() as $name => $variableType) {
if ($this->hasVariableType($name)) {
$variableType = $this->getVariableType($name)->combineWith($variableType);
}
$variableTypes[$name] = $variableType;
}
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$variableTypes,
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes,
$this->inFirstLevelStatement
);
}
public function specifyExpressionType(Expr $expr, Type $type): self
{
if ($expr instanceof Variable && is_string($expr->name)) {
$variableName = $expr->name;
$variableTypes = $this->getVariableTypes();
$variableTypes[$variableName] = $type;
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$variableTypes,
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes,
$this->inFirstLevelStatement
);
}
$exprString = $this->printer->prettyPrintExpr($expr);
return $this->addMoreSpecificTypes([
$exprString => $type,
]);
}
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->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCall(),
!$this->isNegated(),
$this->moreSpecificTypes,
$this->inFirstLevelStatement
);
}
public function enterFirstLevelStatements(): self
{
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes,
true,
$this->currentlyAssignedVariables
);
}
public function exitFirstLevelStatements(): self
{
return new self(
$this->broker,
$this->printer,
$this->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$this->getInFunctionCall(),
$this->isNegated(),
$this->moreSpecificTypes,
false,
$this->currentlyAssignedVariables
);
}
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->getFile(),
$this->getAnalysedContextFile(),
$this->isDeclareStrictTypes(),
$this->getClass(),
$this->getFunction(),
$this->getNamespace(),
$this->getVariableTypes(),
$this->inClosureBindScopeClass,
$this->getAnonymousFunctionReturnType(),
$this->isInAnonymousClass() ? $this->getAnonymousClass() : null,
$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;
}
$class = $this->inClosureBindScopeClass !== null ? $this->inClosureBindScopeClass : $this->getClass();
if ($class !== null && $this->broker->hasClass($class)) {
$currentClassReflection = $this->broker->getClass($class);
} elseif ($this->isInAnonymousClass()) {
$currentClassReflection = $this->getAnonymousClass();
} 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());
}
}
src/Analyser/SpecifiedTypes.php 0000666 00000002347 13436751643 0012570 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 addSureType(Node $expr, string $exprString, Type $type): self
{
$types = $this->sureTypes;
if (isset($types[$exprString])) {
$type = $types[$exprString][1]->combineWith($type);
}
$types[$exprString] = [
$expr,
$type,
];
return new self(
$types,
$this->sureNotTypes
);
}
public function addSureNotType(Node $expr, string $exprString, Type $type): self
{
$types = $this->sureNotTypes;
if (isset($types[$exprString])) {
$type = $types[$exprString][1]->combineWith($type);
}
$types[$exprString] = [
$expr,
$type,
];
return new self(
$this->sureTypes,
$types
);
}
}
src/Analyser/UndefinedVariableException.php 0000666 00000001123 13436751643 0015065 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 00000000303 13436751643 0013012 0 ustar 00 functionName = $functionName;
}
public function getFunctionName(): string
{
return $this->functionName;
}
}
src/Broker/Broker.php 0000666 00000016721 13436751643 0010543 0 ustar 00 propertiesClassReflectionExtensions = $propertiesClassReflectionExtensions;
$this->methodsClassReflectionExtensions = $methodsClassReflectionExtensions;
foreach (array_merge($propertiesClassReflectionExtensions, $methodsClassReflectionExtensions, $dynamicMethodReturnTypeExtensions) as $extension) {
if ($extension instanceof BrokerAwareClassReflectionExtension) {
$extension->setBroker($this);
}
}
foreach ($dynamicMethodReturnTypeExtensions as $dynamicMethodReturnTypeExtension) {
$this->dynamicMethodReturnTypeExtensions[$dynamicMethodReturnTypeExtension->getClass()][] = $dynamicMethodReturnTypeExtension;
}
foreach ($dynamicStaticMethodReturnTypeExtensions as $dynamicStaticMethodReturnTypeExtension) {
$this->dynamicStaticMethodReturnTypeExtensions[$dynamicStaticMethodReturnTypeExtension->getClass()][] = $dynamicStaticMethodReturnTypeExtension;
}
$this->functionReflectionFactory = $functionReflectionFactory;
$this->fileTypeMapper = $fileTypeMapper;
}
/**
* @param string $className
* @return \PHPStan\Type\DynamicMethodReturnTypeExtension[]
*/
public function getDynamicMethodReturnTypeExtensionsForClass(string $className): array
{
return $this->getDynamicExtensionsForType($this->dynamicMethodReturnTypeExtensions, $className);
}
/**
* @param string $className
* @return \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[]
*/
public function getDynamicStaticMethodReturnTypeExtensionsForClass(string $className): array
{
return $this->getDynamicExtensionsForType($this->dynamicStaticMethodReturnTypeExtensions, $className);
}
/**
* @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()) 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);
$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): \PHPStan\Reflection\ClassReflection
{
return new ClassReflection(
$this,
$this->propertiesClassReflectionExtensions,
$this->methodsClassReflectionExtensions,
$reflectionClass
);
}
public function hasClass(string $className): bool
{
try {
return class_exists($className) || interface_exists($className) || trait_exists($className);
} catch (\Throwable $t) {
throw new \PHPStan\Broker\ClassAutoloadingException(
$className,
$t
);
}
}
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);
$phpDocParameterTypes = [];
$phpDocReturnType = null;
if ($reflectionFunction->getFileName() !== false && $reflectionFunction->getDocComment() !== false) {
$fileTypeMap = $this->fileTypeMapper->getTypeMap($reflectionFunction->getFileName());
$docComment = $reflectionFunction->getDocComment();
$phpDocParameterTypes = TypehintHelper::getParameterTypesFromPhpDoc(
$fileTypeMap,
array_map(function (\ReflectionParameter $parameter): string {
return $parameter->getName();
}, $reflectionFunction->getParameters()),
$docComment
);
$phpDocReturnType = TypehintHelper::getReturnTypeFromPhpDoc($fileTypeMap, $docComment);
}
$this->functionReflections[$lowerCasedFunctionName] = $this->functionReflectionFactory->create(
$reflectionFunction,
$phpDocParameterTypes,
$phpDocReturnType
);
}
return $this->functionReflections[$lowerCasedFunctionName];
}
public function hasFunction(\PhpParser\Node\Name $nameNode, Scope $scope): 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)
{
$name = (string) $nameNode;
if ($scope !== null && $scope->getNamespace() !== null && !$nameNode->isFullyQualified()) {
$namespacedName = sprintf('%s\\%s', $scope->getNamespace(), $name);
if (function_exists($namespacedName)) {
return $namespacedName;
}
}
if (function_exists($name)) {
return $name;
}
return null;
}
}
src/Broker/ClassAutoloadingException.php 0000666 00000001042 13436751643 0014420 0 ustar 00 getMessage(),
$functionName
), 0, $previous);
$this->className = $functionName;
}
public function getClassName(): string
{
return $this->className;
}
}
src/Broker/ClassNotFoundException.php 0000666 00000000757 13436751643 0013722 0 ustar 00 className = $functionName;
}
public function getClassName(): string
{
return $this->className;
}
}
src/Broker/BrokerFactory.php 0000666 00000004257 13436751643 0012074 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);
$annotationsPropertiesClassReflectionExtension = $this->container->getByType(AnnotationsPropertiesClassReflectionExtension::class);
$phpDefectClassReflectionExtension = $this->container->getByType(PhpDefectClassReflectionExtension::class);
return new Broker(
array_merge([$phpClassReflectionExtension, $annotationsPropertiesClassReflectionExtension, $phpDefectClassReflectionExtension], $tagToService($this->container->findByTag(self::PROPERTIES_CLASS_REFLECTION_EXTENSION_TAG))),
array_merge([$phpClassReflectionExtension], $tagToService($this->container->findByTag(self::METHODS_CLASS_REFLECTION_EXTENSION_TAG))),
$tagToService($this->container->findByTag(self::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG)),
$tagToService($this->container->findByTag(self::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG)),
$this->container->getByType(FunctionReflectionFactory::class),
$this->container->getByType(FileTypeMapper::class)
);
}
}
src/Reflection/FunctionReflectionFactory.php 0000666 00000000426 13436751643 0015310 0 ustar 00 methodReflectionFactory = $methodReflectionFactory;
$this->fileTypeMapper = $fileTypeMapper;
}
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->properties[$classReflection->getName()])) {
$this->properties[$classReflection->getName()] = $this->createProperties($classReflection);
}
return $this->properties[$classReflection->getName()][$propertyName];
}
/**
* @param \PHPStan\Reflection\ClassReflection $classReflection
* @return \PHPStan\Reflection\PropertyReflection[]
*/
private function createProperties(ClassReflection $classReflection): array
{
$properties = [];
foreach ($classReflection->getNativeReflection()->getProperties() as $propertyReflection) {
$propertyName = $propertyReflection->getName();
$declaringClassReflection = $this->broker->getClass($propertyReflection->getDeclaringClass()->getName());
if ($propertyReflection->getDocComment() === false) {
$type = new MixedType();
} elseif (!$declaringClassReflection->getNativeReflection()->isAnonymous() && $declaringClassReflection->getNativeReflection()->getFileName() !== false) {
$phpDocBlock = PhpDocBlock::resolvePhpDocBlockForProperty(
$this->broker,
$propertyReflection->getDocComment(),
$declaringClassReflection->getName(),
$propertyName,
$declaringClassReflection->getNativeReflection()->getFileName()
);
$typeMap = $this->fileTypeMapper->getTypeMap($phpDocBlock->getFile());
$typeString = $this->getPropertyAnnotationTypeString($phpDocBlock->getDocComment());
if (isset($typeMap[$typeString])) {
$type = $typeMap[$typeString];
} else {
$type = new MixedType();
}
} else {
$type = new MixedType();
}
$properties[$propertyName] = new PhpPropertyReflection(
$declaringClassReflection,
$type,
$propertyReflection
);
}
return $properties;
}
/**
* @param string $phpDoc
* @return string|null
*/
private function getPropertyAnnotationTypeString(string $phpDoc)
{
$count = preg_match_all('#@var\s+' . FileTypeMapper::TYPE_PATTERN . '#', $phpDoc, $matches);
if ($count !== 1) {
return null;
}
return $matches[1][0];
}
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
return $classReflection->getNativeReflection()->hasMethod($methodName);
}
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
{
if (!isset($this->methods[$classReflection->getName()])) {
$this->methods[$classReflection->getName()] = $this->createMethods($classReflection);
}
$nativeMethodReflection = $classReflection->getNativeReflection()->getMethod($methodName);
return $this->methods[$classReflection->getName()][$nativeMethodReflection->getName()];
}
/**
* @param \PHPStan\Reflection\ClassReflection $classReflection
* @return \PHPStan\Reflection\MethodReflection[]
*/
private function createMethods(ClassReflection $classReflection): array
{
$methods = [];
foreach ($classReflection->getNativeReflection()->getMethods() as $methodReflection) {
$declaringClass = $this->broker->getClass($methodReflection->getDeclaringClass()->getName());
$phpDocParameterTypes = [];
$phpDocReturnType = null;
if (!$declaringClass->getNativeReflection()->isAnonymous() && $declaringClass->getNativeReflection()->getFileName() !== false) {
if ($methodReflection->getDocComment() !== false) {
$phpDocBlock = PhpDocBlock::resolvePhpDocBlockForMethod(
$this->broker,
$methodReflection->getDocComment(),
$declaringClass->getName(),
$methodReflection->getName(),
$declaringClass->getNativeReflection()->getFileName()
);
$typeMap = $this->fileTypeMapper->getTypeMap($phpDocBlock->getFile());
$phpDocParameterTypes = TypehintHelper::getParameterTypesFromPhpDoc(
$typeMap,
array_map(function (\ReflectionParameter $parameterReflection): string {
return $parameterReflection->getName();
}, $methodReflection->getParameters()),
$phpDocBlock->getDocComment()
);
$phpDocReturnType = TypehintHelper::getReturnTypeFromPhpDoc($typeMap, $phpDocBlock->getDocComment());
}
}
$methods[$methodReflection->getName()] = $this->methodReflectionFactory->create(
$declaringClass,
$methodReflection,
$phpDocParameterTypes,
$phpDocReturnType
);
}
return $methods;
}
}
src/Reflection/Php/UniversalObjectCratesClassReflectionExtension.php 0000666 00000002224 13436751643 0022044 0 ustar 00 classes = array_values(array_filter($classes, function (string $class): bool {
return self::exists($class);
}));
}
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
if ($classReflection->getNativeReflection()->hasProperty($propertyName)) {
return false;
}
foreach ($this->classes 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 13436751643 0013636 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 00000006221 13436751643 0017765 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
&& $this->realReturnType->isNullable() !== $phpDocReturnType->isNullable()
) {
$phpDocReturnType = null;
}
$this->returnType = TypehintHelper::decideType($this->realReturnType, $phpDocReturnType);
}
return $this->returnType;
}
}
src/Reflection/Php/PhpParameterFromParserNodeReflection.php 0000666 00000003470 13436751643 0020123 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 && $this->defaultValue->name instanceof Name) {
$phpDocType = $phpDocType->makeNullable();
}
}
$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 00000015447 13436751643 0014623 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;
}
public function getPrototype(): MethodReflection
{
try {
$prototypeReflection = $this->reflection->getPrototype();
$prototypeDeclaringClass = $this->broker->getClassFromReflection($prototypeReflection->getDeclaringClass());
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
{
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() === '__construct'
&& $this->declaringClass->getName() === 'ArrayObject'
&& count($this->parameters) === 1
) {
// PHP bug #71077
$this->parameters[] = new DummyParameter(
'flags',
new IntegerType(false),
true
);
$this->parameters[] = new DummyParameter(
'iterator_class',
new StringType(false),
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),
false
);
$this->parameters[] = new DummyParameter(
'fetchColumn',
new IntegerType(false),
true
);
$this->parameters[] = new DummyParameter(
'colno',
new MixedType(),
true
);
$this->parameters[] = new DummyParameter(
'constructorArgs',
new ArrayType(new MixedType(), false),
true
);
}
}
return $this->parameters;
}
public function isVariadic(): bool
{
$isNativelyVariadic = $this->reflection->isVariadic();
if (
!$isNativelyVariadic
&& $this->declaringClass->getName() === 'ReflectionMethod'
&& $this->reflection->getName() === 'invoke'
) {
return true;
}
if (!$isNativelyVariadic && $this->declaringClass->getNativeReflection()->getFileName() !== false) {
$key = sprintf('variadic-method-%s-%s-v2', $this->declaringClass->getName(), $this->reflection->getName());
$cachedResult = $this->cache->load($key);
if ($cachedResult === null) {
$nodes = $this->parser->parseFile($this->declaringClass->getNativeReflection()->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) {
$returnType = $this->reflection->getReturnType();
$phpDocReturnType = $this->phpDocReturnType;
if (
$returnType !== null
&& $phpDocReturnType !== null
&& $returnType->allowsNull() !== $phpDocReturnType->isNullable()
) {
$phpDocReturnType = null;
}
$this->returnType = TypehintHelper::decideTypeFromReflection(
$returnType,
$phpDocReturnType,
$this->declaringClass->getName()
);
}
return $this->returnType;
}
}
src/Reflection/Php/PhpPropertyReflection.php 0000666 00000002024 13436751643 0015212 0 ustar 00 declaringClass = $declaringClass;
$this->type = $type;
$this->reflection = $reflection;
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
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;
}
}
src/Reflection/Php/PhpMethodFromParserNodeReflection.php 0000666 00000003013 13436751643 0017414 0 ustar 00 declaringClass = $declaringClass;
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
public function getPrototype(): MethodReflection
{
return $this->declaringClass->getMethod($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();
}
}
src/Reflection/Php/PhpParameterReflection.php 0000666 00000002724 13436751643 0015315 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 = $phpDocType->makeNullable();
}
$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();
}
}
src/Reflection/Php/UniversalObjectCrateProperty.php 0000666 00000001401 13436751643 0016524 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();
}
}
src/Reflection/Php/PhpMethodReflectionFactory.php 0000666 00000001172 13436751643 0016141 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 ObjectType(Name::class, false);
}
}
src/Reflection/PhpParser/PhpParserNameClassReflectionExtension.php 0000666 00000001501 13436751643 0021462 0 ustar 00 isSubclassOf(Node::class)
&& ($classReflection->getNativeReflection()->hasProperty('name') || $classReflection->getName() === \PhpParser\Node\FunctionLike::class)
&& $propertyName === 'namespacedName';
}
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
{
return new NamespacedNameProperty($classReflection);
}
}
src/Reflection/PropertiesClassReflectionExtension.php 0000666 00000000473 13436751643 0017214 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/BrokerAwareClassReflectionExtension.php 0000666 00000000275 13436751643 0017264 0 ustar 00 broker = $broker;
$this->propertiesClassReflectionExtensions = $propertiesClassReflectionExtensions;
$this->methodsClassReflectionExtensions = $methodsClassReflectionExtensions;
$this->reflection = $reflection;
}
public function getNativeReflection(): \ReflectionClass
{
return $this->reflection;
}
/**
* @return bool|\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 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): MethodReflection
{
if (!isset($this->methods[$methodName])) {
foreach ($this->methodsClassReflectionExtensions as $extension) {
if ($extension->hasMethod($this, $methodName)) {
return $this->methods[$methodName] = $extension->getMethod($this, $methodName);
}
}
}
if (!isset($this->methods[$methodName])) {
throw new \PHPStan\Reflection\MissingMethodFromReflectionException($this->getName(), $methodName);
}
return $this->methods[$methodName];
}
public function getProperty(string $propertyName, Scope $scope = null): PropertyReflection
{
if (!isset($this->properties[$propertyName])) {
$privateProperty = null;
$publicProperty = null;
foreach ($this->propertiesClassReflectionExtensions as $extension) {
if ($extension->hasProperty($this, $propertyName)) {
$property = $extension->getProperty($this, $propertyName);
if ($scope !== null && $scope->canAccessProperty($property)) {
return $this->properties[$propertyName] = $property;
}
$this->properties[$propertyName] = $property;
}
}
}
if (!isset($this->properties[$propertyName])) {
throw new \PHPStan\Reflection\MissingPropertyFromReflectionException($this->getName(), $propertyName);
}
return $this->properties[$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 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 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 00000004735 13436751643 0020571 0 ustar 00 [
'y' => 'int',
'm' => 'int',
'd' => 'int',
'h' => 'int',
'i' => 'int',
's' => 'int',
'invert' => 'int',
'days' => 'mixed',
],
'DOMDocument' => [
'actualEncoding' => 'string',
'config' => 'DOMConfiguration',
'doctype' => 'DOMDocumentType',
'documentElement' => 'DOMElement',
'documentURI' => 'string',
'encoding' => 'string',
'formatOutput' => 'bool',
'implementation' => 'DOMImplementation',
'preserveWhiteSpace' => 'bool',
'recover' => 'bool',
'resolveExternals' => 'bool',
'standalone' => 'bool',
'strictErrorChecking' => 'bool',
'substituteEntities' => 'bool',
'validateOnParse' => 'bool',
'version' => 'string',
'xmlEncoding' => 'string',
'xmlStandalone' => 'bool',
'xmlVersion' => 'string',
],
'ZipArchive' => [
'status' => 'int',
'statusSys' => 'int',
'numFiles' => 'int',
'filename' => 'string',
'comment' => 'string',
],
];
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
$classWithProperties = $this->getClassWithProperties($classReflection);
if ($classWithProperties === null) {
return false;
}
return isset($this->properties[$classWithProperties->getName()][$propertyName]);
}
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
{
$classWithProperties = $this->getClassWithProperties($classReflection);
$typeString = $this->properties[$classWithProperties->getName()][$propertyName];
return new PhpDefectPropertyReflection(
$classWithProperties,
TypehintHelper::getTypeObjectFromTypehint($typeString, false)
);
}
/**
* @param \PHPStan\Reflection\ClassReflection $classReflection
* @return \PHPStan\Reflection\ClassReflection|null
*/
private function getClassWithProperties(ClassReflection $classReflection)
{
if (isset($this->properties[$classReflection->getName()])) {
return $classReflection;
}
foreach ($classReflection->getParents() as $parentClass) {
if (isset($this->properties[$parentClass->getName()])) {
return $parentClass;
}
}
return null;
}
}
src/Reflection/PhpDefect/PhpDefectPropertyReflection.php 0000666 00000001530 13436751643 0017441 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;
}
}
src/Reflection/MissingPropertyFromReflectionException.php 0000666 00000000647 13436751643 0020061 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(false),
true
);
}
}
return $this->parameters;
}
public function isVariadic(): bool
{
$isNativelyVariadic = $this->reflection->isVariadic();
if (!$isNativelyVariadic && $this->reflection->getFileName() !== false) {
$key = sprintf('variadic-function-%s-v2', $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 = $node->name;
if ((string) $node->namespacedName) {
$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) {
$returnType = $this->reflection->getReturnType();
$phpDocReturnType = $this->phpDocReturnType;
if (
$returnType !== null
&& $phpDocReturnType !== null
&& $returnType->allowsNull() !== $phpDocReturnType->isNullable()
) {
$phpDocReturnType = null;
}
$this->returnType = TypehintHelper::decideTypeFromReflection(
$returnType,
$phpDocReturnType
);
}
return $this->returnType;
}
}
src/Reflection/Annotations/AnnotationsPropertiesClassReflectionExtension.php 0000666 00000005026 13436751643 0023726 0 ustar 00 fileTypeMapper = $fileTypeMapper;
}
public function setBroker(Broker $broker)
{
$this->broker = $broker;
}
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
if (!isset($this->properties[$classReflection->getName()])) {
$this->properties[$classReflection->getName()] = $this->createProperties($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
* @return \PHPStan\Reflection\PropertyReflection[]
*/
private function createProperties(ClassReflection $classReflection): array
{
$properties = [];
foreach ($classReflection->getParentClassesNames() as $parentClassName) {
if (!$this->broker->hasClass($parentClassName)) {
continue;
}
$properties += $this->createProperties($this->broker->getClass($parentClassName));
}
$fileName = $classReflection->getNativeReflection()->getFileName();
if ($fileName === false) {
return $properties;
}
$docComment = $classReflection->getNativeReflection()->getDocComment();
if ($docComment === false) {
return $properties;
}
$typeMap = $this->fileTypeMapper->getTypeMap($fileName);
preg_match_all('#@property(?:-read)?\s+' . FileTypeMapper::TYPE_PATTERN . '\s+\$([a-zA-Z0-9_]+)#', $docComment, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$typeString = $match[1];
if (!isset($typeMap[$typeString])) {
continue;
}
$properties[$match[2]] = new AnnotationPropertyReflection($classReflection, $typeMap[$typeString]);
}
return $properties;
}
}
src/Reflection/Annotations/AnnotationPropertyReflection.php 0000666 00000001533 13436751643 0020347 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;
}
}
src/Reflection/ObsoleteClassConstantReflection.php 0000666 00000001772 13436751643 0016454 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 13436751643 0011644 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 13436751643 0010557 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
{
return $this->traverser->traverse($this->parser->parse($sourceCode));
}
}
src/Parser/FunctionCallStatementFinder.php 0000666 00000001711 13436751643 0014716 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 00000014562 13436751643 0012345 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('autoload-file', 'a', InputOption::VALUE_OPTIONAL, 'Project\'s additional autoload file path'),
]);
}
public function getAliases(): array
{
return ['analyze'];
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$autoloadFile = $input->getOption('autoload-file');
if ($autoloadFile !== null && is_file($autoloadFile)) {
require_once $autoloadFile;
}
$currentWorkingDirectory = getcwd();
$fileHelper = new FileHelper($currentWorkingDirectory);
$rootDir = $fileHelper->normalizePath(__DIR__ . '/../..');
$tmpDir = $rootDir . '/tmp';
$confDir = $rootDir . '/conf';
$configurator = new Configurator();
$configurator->defaultExtensions = [];
$configurator->setDebugMode(true);
$configurator->setTempDirectory($tmpDir);
$projectConfigFile = $input->getOption('configuration');
$levelOption = $input->getOption(self::OPTION_LEVEL);
$defaultLevelUsed = false;
if ($projectConfigFile === null && $levelOption === null) {
$levelOption = self::DEFAULT_LEVEL;
$defaultLevelUsed = true;
}
$configFiles = [$confDir . '/config.neon'];
if ($levelOption !== null) {
$levelConfigFile = sprintf('%s/config.level%s.neon', $confDir, $levelOption);
if (!is_file($levelConfigFile)) {
$output->writeln(sprintf('Level config file %s was not found.', $levelConfigFile));
return 1;
}
$configFiles[] = $levelConfigFile;
}
if ($projectConfigFile !== null) {
if (!is_file($projectConfigFile)) {
$output->writeln(sprintf('Project config file at path %s does not exist.', $projectConfigFile));
return 1;
}
$configFiles[] = $projectConfigFile;
}
foreach ($configFiles as $configFile) {
$configurator->addConfig($configFile);
}
$parameters = [
'rootDir' => $rootDir,
'tmpDir' => $tmpDir,
'currentWorkingDirectory' => $currentWorkingDirectory,
];
$configurator->addParameters($parameters);
$container = $configurator->createContainer();
$consoleStyle = new ErrorsConsoleStyle($input, $output);
$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, increase the memory_limit directive in your php.ini file here:\n%s\n\nIf you can't or don't want to change the system-wide memory limit, run PHPStan like this:\n%s",
file_get_contents($memoryLimitFile),
php_ini_loaded_file(),
sprintf('php -d memory_limit=XX %s', implode(' ', $_SERVER['argv']))
));
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.'
);
}
$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 $this->handleReturn(1, $memoryLimitFile);
} elseif ($container->parameters['customRulesetUsed']) {
$defaultLevelUsed = false;
}
foreach ($container->parameters['autoload_files'] as $autoloadFile) {
require_once $autoloadFile;
}
if (count($container->parameters['autoload_directories']) > 0) {
$robotLoader = new \Nette\Loaders\RobotLoader();
$robotLoader->setCacheStorage(new \Nette\Caching\Storages\MemoryStorage());
foreach ($container->parameters['autoload_directories'] as $directory) {
$robotLoader->addDirectory($directory);
}
$robotLoader->register();
}
$application = $container->getByType(AnalyseApplication::class);
return $this->handleReturn(
$application->analyse(
$input->getArgument('paths'),
$consoleStyle,
$defaultLevelUsed
),
$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/ErrorsConsoleStyle.php 0000666 00000005607 13436751643 0013272 0 ustar 00 showProgress = !$input->getOption(self::OPTION_NO_PROGRESS);
$this->output = $output;
}
public function table(array $headers, array $rows)
{
$application = new Application();
$dimensions = $application->getTerminalDimensions();
$terminalWidth = $dimensions[0] ?: self::MAX_LINE_LENGTH;
$maxHeaderWidth = strlen($headers[0]);
foreach ($rows as $row) {
$length = strlen($row[0]);
if ($maxHeaderWidth === null || $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/AnalyseApplication.php 0000666 00000007451 13436751643 0013231 0 ustar 00 analyser = $analyser;
$this->memoryLimitFile = $memoryLimitFile;
$this->fileHelper = $fileHelper;
}
/**
* @param string[] $paths
* @param \Symfony\Component\Console\Style\StyleInterface $style
* @param bool $defaultLevelUsed
* @return int
*/
public function analyse(array $paths, StyleInterface $style, bool $defaultLevelUsed): 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);
} elseif (is_file($path)) {
$files[] = $path;
} else {
$finder = new Finder();
foreach ($finder->files()->name('*.php')->in($path) as $fileInfo) {
$files[] = $fileInfo->getPathname();
$onlyFiles = false;
}
}
}
$this->updateMemoryLimitFile();
$progressStarted = false;
$fileOrder = 0;
$errors = array_merge($errors, $this->analyser->analyse(
$files,
$onlyFiles,
function () use ($style, &$progressStarted, $files, &$fileOrder) {
if (!$progressStarted) {
$style->progressStart(count($files));
$progressStarted = true;
}
$style->progressAdvance();
if ($fileOrder % 100 === 0) {
$this->updateMemoryLimitFile();
}
$fileOrder++;
}
));
if ($progressStarted) {
$style->progressFinish();
}
if (count($errors) === 0) {
$style->success('No errors');
if ($defaultLevelUsed) {
$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;
}
$currentDir = $this->fileHelper->normalizePath(dirname($paths[0]));
$cropFilename = function (string $filename) use ($currentDir): string {
if ($currentDir !== '' && strpos($filename, $currentDir) === 0) {
return substr($filename, strlen($currentDir) + 1);
}
return $filename;
};
$fileErrors = [];
$notFileSpecificErrors = [];
$totalErrorsCount = count($errors);
foreach ($errors as $error) {
if (is_string($error)) {
$notFileSpecificErrors[] = [$error];
continue;
}
if (!isset($fileErrors[$error->getFile()])) {
$fileErrors[$error->getFile()] = [];
}
$fileErrors[$error->getFile()][] = $error;
}
foreach ($fileErrors as $file => $errors) {
$rows = [];
foreach ($errors as $error) {
$rows[] = [
(string) $error->getLine(),
$error->getMessage(),
];
}
$style->table(['Line', $cropFilename($file)], $rows);
}
if (count($notFileSpecificErrors) > 0) {
$style->table(['Error'], $notFileSpecificErrors);
}
$style->error(sprintf($totalErrorsCount === 1 ? 'Found %d error' : 'Found %d errors', $totalErrorsCount));
return 1;
}
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 00000003156 13436751643 0015116 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];
}
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 00000002051 13436751643 0013134 0 ustar 00 getType($node->expr);
if ($expressionType->isNullable()) {
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 00000004204 13436751643 0013743 0 ustar 00 printer = $printer;
}
/**
* @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 $isAnonymousFunction
* @return string[]
*/
public function checkReturnType(
Scope $scope,
Type $returnType,
Expr $returnValue = null,
string $emptyReturnStatementMessage,
string $voidMessage,
string $typeMismatchMessage,
bool $isAnonymousFunction = false
): array
{
if ($returnValue === null) {
if ($returnType instanceof VoidType || $returnType instanceof MixedType) {
return [];
}
return [
sprintf(
$emptyReturnStatementMessage,
$returnType->describe()
),
];
}
$returnValueType = $scope->getType($returnValue);
if ($returnType instanceof ThisType && !$returnValueType instanceof ThisType) {
if ($returnType->isNullable() && $returnValueType instanceof 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 (!$returnType->accepts($returnValueType) && (!$isAnonymousFunction || $returnValueType->isDocumentableNatively())) {
return [
sprintf(
$typeMismatchMessage,
$returnType->describe(),
$returnValueType->describe()
),
];
}
return [];
}
}
src/Rules/Exceptions/CatchedExceptionExistenceRule.php 0000666 00000002507 13436751643 0017215 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('Catched class %s not found.', $class);
continue;
}
$classReflection = $this->broker->getClass($class);
if (!$classReflection->isInterface() && !$classReflection->getNativeReflection()->implementsInterface(\Throwable::class)) {
$errors[] = sprintf('Catched class %s is not an exception.', $classReflection->getName());
}
}
return $errors;
}
}
src/Rules/Classes/InstantiationRule.php 0000666 00000005441 13436751643 0014233 0 ustar 00 broker = $broker;
$this->check = $check;
}
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;
if ($class === 'static') {
return [];
}
if ($class === 'self') {
$class = $scope->getClass();
if ($class === null) {
return [];
}
}
if (!$this->broker->hasClass($class)) {
return [
sprintf('Instantiated class %s not found.', $class),
];
}
$classReflection = $this->broker->getClass($class);
if ($classReflection->isInterface()) {
return [
sprintf('Cannot instantiate interface %s.', $classReflection->getName()),
];
}
if ($classReflection->isAbstract()) {
return [
sprintf('Instantiated class %s is abstract.', $classReflection->getName()),
];
}
if (!$classReflection->hasMethod('__construct') && !$classReflection->hasMethod($class)) {
if (count($node->args) > 0) {
return [
sprintf(
'Class %s does not have a constructor and must be instantiated without any parameters.',
$classReflection->getName()
),
];
}
return [];
}
return $this->check->check(
$classReflection->hasMethod('__construct') ? $classReflection->getMethod('__construct') : $classReflection->getMethod($class),
$scope,
$node,
[
'Class ' . $classReflection->getName() . ' constructor invoked with %d parameter, %d required.',
'Class ' . $classReflection->getName() . ' constructor invoked with %d parameters, %d required.',
'Class ' . $classReflection->getName() . ' constructor invoked with %d parameter, at least %d required.',
'Class ' . $classReflection->getName() . ' constructor invoked with %d parameters, at least %d required.',
'Class ' . $classReflection->getName() . ' constructor invoked with %d parameter, %d-%d required.',
'Class ' . $classReflection->getName() . ' constructor invoked with %d parameters, %d-%d required.',
'Parameter #%d %s of class ' . $classReflection->getName() . ' constructor expects %s, %s given.',
'', // constructor does not have a return type
]
);
}
}
src/Rules/Classes/ExistingClassesInPropertiesRule.php 0000666 00000002437 13436751643 0017065 0 ustar 00 broker = $broker;
}
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
{
$className = $scope->getClass();
if ($className === null) {
return [];
}
$classReflection = $this->broker->getClass($className);
$propertyReflection = $classReflection->getProperty($node->name, $scope);
$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()->getName(),
$node->name,
$referencedClass
);
}
return $errors;
}
}
src/Rules/Classes/RequireParentConstructCallRule.php 0000666 00000006112 13436751643 0016672 0 ustar 00 broker = $broker;
}
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') {
return [];
}
$className = $scope->getClass();
if ($className === null) {
return []; // anonymous class
}
$classReflection = $this->broker->getClass($className);
if ($classReflection->isInterface()) {
return [];
}
if ($this->callsParentConstruct($node)) {
if ($classReflection->getParentClass() === false) {
return [
sprintf(
'%s::__construct() calls parent constructor but does not extend any class.',
$className
),
];
}
if ($this->getParentConstructorClass($classReflection) === false) {
return [
sprintf(
'%s::__construct() calls parent constructor but parent does not have one.',
$className
),
];
}
} else {
$parentClass = $this->getParentConstructorClass($classReflection);
if ($parentClass !== false) {
return [
sprintf(
'%s::__construct() does not call parent constructor from %s.',
$className,
$parentClass->getName()
),
];
}
}
return [];
}
private function callsParentConstruct(Node $parserNode): bool
{
if (!isset($parserNode->stmts)) {
return false;
}
foreach ($parserNode->stmts as $statement) {
if ($statement instanceof \PhpParser\Node\Expr\StaticCall) {
if (((string) $statement->class === 'parent') && $statement->name === '__construct') {
return true;
}
} else {
if ($this->callsParentConstruct($statement)) {
return true;
}
}
}
return false;
}
/**
* @param \PHPStan\Reflection\ClassReflection $classReflection
* @return \PHPStan\Reflection\ClassReflection|boolean
*/
private function getParentConstructorClass(ClassReflection $classReflection)
{
while ($classReflection->getParentClass() !== false) {
if (
(
$classReflection->getParentClass()->hasMethod('__construct')
&& $classReflection->getParentClass()->getMethod('__construct')->getDeclaringClass()->getName() === $classReflection->getParentClass()->getName()
) || (
$classReflection->getParentClass()->hasMethod($classReflection->getParentClass()->getName())
&& $classReflection->getParentClass()->getMethod($classReflection->getParentClass()->getName())->getDeclaringClass()->getName() === $classReflection->getParentClass()->getName()
)
) {
return $classReflection->getParentClass();
}
$classReflection = $classReflection->getParentClass();
}
return false;
}
}
src/Rules/Classes/ExistingClassInInstanceOfRule.php 0000666 00000002310 13436751643 0016420 0 ustar 00 broker = $broker;
}
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;
if ($name === 'self' || $name === 'static') {
if ($scope->getClass() === null && !$scope->isInAnonymousClass()) {
return [
sprintf('Using %s outside of class scope.', $name),
];
}
if ($name === 'static') {
return [];
}
if ($name === 'self') {
$name = $scope->getClass();
}
}
if (!$this->broker->hasClass($name)) {
return [
sprintf('Class %s not found.', $name),
];
}
return [];
}
}
src/Rules/Classes/AccessStaticPropertiesRule.php 0000666 00000005054 13436751643 0016035 0 ustar 00 broker = $broker;
}
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) || !($node->class instanceof Node\Name)) {
return [];
}
$name = $node->name;
$currentClass = $scope->getClass();
if ($currentClass === null) {
return [];
}
$currentClassReflection = $this->broker->getClass($currentClass);
$class = (string) $node->class;
if ($class === 'self' || $class === 'static') {
$class = $currentClass;
}
if ($class === 'parent') {
if ($currentClassReflection->getParentClass() === false) {
return [
sprintf(
'%s::%s() accesses parent::$%s but %s does not extend any class.',
$currentClass,
$scope->getFunctionName(),
$name,
$currentClass
),
];
}
$currentMethodReflection = $currentClassReflection->getMethod(
$scope->getFunctionName()
);
if (!$currentMethodReflection->isStatic()) {
// calling parent::method() from instance method
return [];
}
$class = $currentClassReflection->getParentClass()->getName();
}
if (!$this->broker->hasClass($class)) {
return [
sprintf(
'Access to static property $%s on an unknown class %s.',
$name,
$class
),
];
}
$classReflection = $this->broker->getClass($class);
if (!$classReflection->hasProperty($name)) {
if ($scope->isSpecified($node)) {
return [];
}
return [
sprintf(
'Access to an undefined static property %s::$%s.',
$classReflection->getName(),
$name
),
];
}
$property = $classReflection->getProperty($name, $scope);
if (!$property->isStatic()) {
return [
sprintf(
'Static access to instance property %s::$%s.',
$property->getDeclaringClass()->getName(),
$name
),
];
}
if (!$scope->canAccessProperty($property)) {
return [
sprintf(
'Cannot access property %s::$%s from current scope.',
$property->getDeclaringClass()->getName(),
$name
),
];
}
return [];
}
}
src/Rules/Classes/UnusedConstructorParametersRule.php 0000666 00000002420 13436751643 0017136 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 [];
}
if ($scope->getClass() !== null) {
$message = sprintf('Constructor of class %s has an unused parameter $%%s.', $scope->getClass());
} else {
$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/TypesAssignedToPropertiesRule.php 0000666 00000006071 13436751643 0016551 0 ustar 00 broker = $broker;
}
public function getNodeType(): string
{
return Assign::class;
}
/**
* @param \PhpParser\Node\Expr\Assign $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[]
*/
public function processNode(Node $node, Scope $scope): array
{
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;
$propertyType = $scope->getType($propertyFetch);
$assignedValueType = $scope->getType($node->expr);
if (!$propertyType->accepts($assignedValueType)) {
$propertyDescription = $this->describeProperty($propertyFetch, $scope);
if ($propertyDescription === null) {
return [];
}
return [
sprintf(
'%s (%s) does not accept %s.',
$propertyDescription,
$propertyType->describe(),
$assignedValueType->describe()
),
];
}
return [];
}
/**
* @param \PhpParser\Node\Expr\PropertyFetch|\PhpParser\Node\Expr\StaticPropertyFetch $propertyFetch
* @param \PHPStan\Analyser\Scope $scope
* @return string|null
*/
private function describeProperty($propertyFetch, Scope $scope)
{
if ($propertyFetch instanceof Node\Expr\PropertyFetch) {
if (!is_string($propertyFetch->name)) {
return null;
}
$propertyHolderType = $scope->getType($propertyFetch->var);
if ($propertyHolderType->getClass() === null) {
return null;
}
$property = $this->findPropertyReflection($propertyHolderType->getClass(), $propertyFetch->name);
if ($property === null) {
return null;
}
return sprintf('Property %s::$%s', $property->getDeclaringClass()->getName(), $propertyFetch->name);
} elseif ($propertyFetch instanceof Node\Expr\StaticPropertyFetch) {
if (
!($propertyFetch->class instanceof Node\Name)
|| !is_string($propertyFetch->name)
) {
return null;
}
$property = $this->findPropertyReflection($scope->resolveName($propertyFetch->class), $propertyFetch->name);
if ($property === null) {
return null;
}
return sprintf('Static property %s::$%s', $property->getDeclaringClass()->getName(), $propertyFetch->name);
}
return null;
}
/**
* @param string $className
* @param string $propertyName
* @return \PHPStan\Reflection\PropertyReflection|null
*/
private function findPropertyReflection(string $className, string $propertyName)
{
if (!$this->broker->hasClass($className)) {
return null;
}
$propertyClass = $this->broker->getClass($className);
if (!$propertyClass->hasProperty($propertyName)) {
return null;
}
return $propertyClass->getProperty($propertyName);
}
}
src/Rules/Classes/DefaultValueTypesAssignedToPropertiesRule.php 0000666 00000003113 13436751643 0021045 0 ustar 00 broker = $broker;
}
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
{
if ($scope->getClass() === null || !$this->broker->hasClass($scope->getClass())) {
return [];
}
$classReflection = $this->broker->getClass($scope->getClass());
$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->getProperty($property->name);
$propertyType = $propertyReflection->getType();
$defaultValueType = $scope->getType($property->default);
if ($propertyType->accepts($defaultValueType)) {
continue;
}
$errors[] = sprintf(
'%s %s::$%s (%s) does not accept default value of type %s.',
$node->isStatic() ? 'Static property' : 'Property',
$scope->getClass(),
$property->name,
$propertyType->describe(),
$defaultValueType->describe()
);
}
return $errors;
}
}
src/Rules/Classes/AccessPropertiesRule.php 0000666 00000005105 13436751643 0014662 0 ustar 00 broker = $broker;
$this->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(\PhpParser\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->canAccessProperties()) {
return [
sprintf('Cannot access property $%s on %s.', $node->name, $type->describe()),
];
}
$propertyClass = $type->getClass();
if ($propertyClass === null) {
return [];
}
$name = $node->name;
if (!$this->broker->hasClass($propertyClass)) {
return [
sprintf(
'Access to property $%s on an unknown class %s.',
$name,
$propertyClass
),
];
}
$propertyClassReflection = $this->broker->getClass($propertyClass);
if (!$propertyClassReflection->hasProperty($name)) {
if ($scope->isSpecified($node)) {
return [];
}
$parentClassReflection = $propertyClassReflection->getParentClass();
while ($parentClassReflection !== false) {
if ($parentClassReflection->hasProperty($name)) {
return [
sprintf(
'Access to private property $%s of parent class %s.',
$name,
$parentClassReflection->getName()
),
];
}
$parentClassReflection = $parentClassReflection->getParentClass();
}
return [
sprintf(
'Access to an undefined property %s::$%s.',
$propertyClass,
$name
),
];
}
$propertyReflection = $propertyClassReflection->getProperty($name, $scope);
if (!$scope->canAccessProperty($propertyReflection)) {
return [
sprintf(
'Cannot access property %s::$%s from current scope.',
$propertyReflection->getDeclaringClass()->getName(),
$name
),
];
}
return [];
}
}
src/Rules/Classes/ClassConstantRule.php 0000666 00000005010 13436751643 0014156 0 ustar 00 broker = $broker;
}
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
{
$class = $node->class;
if ($class instanceof \PhpParser\Node\Name) {
$className = (string) $class;
} elseif ($class instanceof Node\Expr) {
$classType = $scope->getType($class);
if ($classType->getClass() !== null) {
$className = $classType->getClass();
} else {
return [];
}
} else {
throw new \PHPStan\ShouldNotHappenException();
}
if ($className === 'self' || $className === 'static') {
if ($scope->getClass() === null && !$scope->isInAnonymousClass()) {
return [
sprintf('Using %s outside of class scope.', $className),
];
}
if ($className === 'static') {
return [];
}
if ($className === 'self') {
$className = $scope->getClass();
}
}
$constantName = $node->name;
if ($scope->getClass() !== null && $className === 'parent') {
$currentClassReflection = $this->broker->getClass($scope->getClass());
if ($currentClassReflection->getParentClass() === false) {
return [
sprintf(
'Access to parent::%s but %s does not extend any class.',
$constantName,
$scope->getClass()
),
];
}
$className = $currentClassReflection->getParentClass()->getName();
}
if (!$this->broker->hasClass($className)) {
return [
sprintf('Class %s not found.', $className),
];
}
if ($constantName === 'class') {
return [];
}
$classReflection = $this->broker->getClass($className);
if (!$classReflection->hasConstant($constantName)) {
return [
sprintf(
'Access to undefined constant %s::%s.',
$classReflection->getName(),
$constantName
),
];
}
$constantReflection = $classReflection->getConstant($constantName);
if (!$scope->canAccessConstant($constantReflection)) {
return [
sprintf('Cannot access constant %s::%s from current scope.', $constantReflection->getDeclaringClass()->getName(), $constantName),
];
}
return [];
}
}
src/Rules/Comparison/StrictComparisonOfDifferentTypesRule.php 0000666 00000003774 13436751643 0020577 0 ustar 00 getType($node->left);
$rightType = $scope->getType($node->right);
if (
$leftType instanceof MixedType
|| $rightType instanceof MixedType
|| $leftType instanceof NullType
|| $rightType instanceof NullType
) {
return [];
}
if ($leftType instanceof UnionType || $rightType instanceof UnionType) {
if ($leftType instanceof UnionType) {
$unionType = $leftType;
$otherType = $rightType;
} else {
$unionType = $rightType;
$otherType = $leftType;
}
$isSameType = $unionType->accepts($otherType);
} elseif ($leftType instanceof BooleanType && $rightType instanceof BooleanType) {
$isSameType = $leftType->accepts($rightType) || $rightType->accepts($leftType);
} elseif ($leftType instanceof StaticResolvableType || $rightType instanceof StaticResolvableType) {
$isSameType = $leftType->accepts($rightType) || $rightType->accepts($leftType);
} else {
$isSameType = get_class($leftType) === get_class($rightType);
}
if (!$isSameType) {
return [
sprintf(
'Strict comparison using %s between %s and %s will always evaluate to false.',
$node instanceof Node\Expr\BinaryOp\Identical ? '===' : '!==',
$leftType->describe(),
$rightType->describe()
),
];
}
return [];
}
}
src/Rules/RuleLevelHelper.php 0000666 00000000523 13436751643 0012215 0 ustar 00 name)) {
return false;
}
return $expression->name === 'this';
}
}
src/Rules/Rule.php 0000666 00000000633 13436751643 0010067 0 ustar 00 checkArgumentTypes = $checkArgumentTypes;
}
/**
* @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;
} 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) {
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);
if (!$parameterType->accepts($argumentValueType)) {
$errors[] = sprintf(
$messages[6],
$i + 1,
sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName()),
$parameterType->describe(),
$argumentValueType->describe()
);
}
}
return $errors;
}
}
src/Rules/Methods/ReturnTypeRule.php 0000666 00000003034 13436751643 0013532 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 [];
}
return $this->returnTypeCheck->checkReturnType(
$scope,
$method->getReturnType(),
$node->expr,
sprintf(
'Method %s::%s() should return %%s but empty return statement found.',
$method->getDeclaringClass()->getName(),
$method->getName()
),
sprintf(
'Method %s::%s() with return type void returns %%s but should not return anything.',
$method->getDeclaringClass()->getName(),
$method->getName()
),
sprintf(
'Method %s::%s() should return %%s but returns %%s.',
$method->getDeclaringClass()->getName(),
$method->getName()
)
);
}
}
src/Rules/Methods/CallStaticMethodsRule.php 0000666 00000011630 13436751643 0014761 0 ustar 00 broker = $broker;
$this->check = $check;
}
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
{
if (!is_string($node->name)) {
return [];
}
$name = $node->name;
$currentClass = $scope->getClass();
if ($currentClass === null) {
return [];
}
$currentClassReflection = $this->broker->getClass($currentClass);
if (!($node->class instanceof Name)) {
return [];
}
$class = (string) $node->class;
if ($class === 'self' || $class === 'static') {
$class = $currentClass;
}
if ($class === 'parent') {
if ($currentClassReflection->getParentClass() === false) {
return [
sprintf(
'%s::%s() calls to parent::%s() but %s does not extend any class.',
$currentClass,
$scope->getFunctionName(),
$name,
$currentClass
),
];
}
$currentMethodReflection = $currentClassReflection->getMethod(
$scope->getFunctionName()
);
if (!$currentMethodReflection->isStatic()) {
if ($name === '__construct' && $currentClassReflection->getParentClass()->hasMethod('__construct')) {
return $this->check->check(
$currentClassReflection->getParentClass()->getMethod('__construct'),
$scope,
$node,
[
'Parent constructor invoked with %d parameter, %d required.',
'Parent constructor invoked with %d parameters, %d required.',
'Parent constructor invoked with %d parameter, at least %d required.',
'Parent constructor invoked with %d parameters, at least %d required.',
'Parent constructor invoked with %d parameter, %d-%d required.',
'Parent constructor invoked with %d parameters, %d-%d required.',
'Parameter #%d %s of parent constructor expects %s, %s given.',
'', // constructor does not have a return type
]
);
}
return [];
}
$class = $currentClassReflection->getParentClass()->getName();
}
if (!$this->broker->hasClass($class)) {
return [
sprintf(
'Call to static method %s() on an unknown class %s.',
$name,
$class
),
];
}
$classReflection = $this->broker->getClass($class);
if (!$classReflection->hasMethod($name)) {
return [
sprintf(
'Call to an undefined static method %s::%s().',
$classReflection->getName(),
$name
),
];
}
$method = $classReflection->getMethod($name);
if (!$method->isStatic()) {
$function = $scope->getFunction();
if (
!$function instanceof MethodReflection
|| $function->isStatic()
|| (
$scope->getClass() !== $class
&& !$currentClassReflection->isSubclassOf($class)
)
) {
return [
sprintf(
'Static call to instance method %s::%s().',
$method->getDeclaringClass()->getName(),
$method->getName()
),
];
}
}
if (!$scope->canCallMethod($method)) {
return [
sprintf(
'Call to %s %s %s() of class %s.',
$method->isPrivate() ? 'private' : 'protected',
$method->isStatic() ? 'static method' : 'method',
$method->getName(),
$method->getDeclaringClass()->getName()
),
];
}
$lowercasedMethodName = sprintf(
'%s %s',
$method->isStatic() ? 'static method' : 'method',
$method->getDeclaringClass()->getName() . '::' . $method->getName() . '()'
);
$methodName = sprintf(
'%s %s',
$method->isStatic() ? 'Static method' : 'Method',
$method->getDeclaringClass()->getName() . '::' . $method->getName() . '()'
);
$errors = $this->check->check(
$method,
$scope,
$node,
[
$methodName . ' invoked with %d parameter, %d required.',
$methodName . ' invoked with %d parameters, %d required.',
$methodName . ' invoked with %d parameter, at least %d required.',
$methodName . ' invoked with %d parameters, at least %d required.',
$methodName . ' invoked with %d parameter, %d-%d required.',
$methodName . ' invoked with %d parameters, %d-%d required.',
'Parameter #%d %s of ' . $lowercasedMethodName . ' expects %s, %s given.',
'Result of ' . $lowercasedMethodName . ' (void) is used.',
]
);
if ($method->getName() !== $name) {
$errors[] = sprintf('Call to %s with incorrect case: %s', $lowercasedMethodName, $name);
}
return $errors;
}
}
src/Rules/Methods/ExistingClassesInTypehintsRule.php 0000666 00000001767 13436751643 0016733 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->getClass(),
$node->name
),
sprintf(
'Return typehint of method %s::%s() has invalid type %%s.',
$scope->getClass(),
$node->name
)
);
}
}
src/Rules/Methods/CallMethodsRule.php 0000666 00000007326 13436751643 0013620 0 ustar 00 broker = $broker;
$this->check = $check;
$this->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->canCallMethods()) {
return [
sprintf('Cannot call method %s() on %s.', $node->name, $type->describe()),
];
}
$methodClass = $type->getClass();
if ($methodClass === null) {
return [];
}
$name = $node->name;
if (!$this->broker->hasClass($methodClass)) {
return [
sprintf(
'Call to method %s() on an unknown class %s.',
$name,
$methodClass
),
];
}
$methodClassReflection = $this->broker->getClass($methodClass);
if (!$methodClassReflection->hasMethod($name)) {
$parentClassReflection = $methodClassReflection->getParentClass();
while ($parentClassReflection !== false) {
if ($parentClassReflection->hasMethod($name)) {
return [
sprintf(
'Call to private method %s() of parent class %s.',
$parentClassReflection->getMethod($name)->getName(),
$parentClassReflection->getName()
),
];
}
$parentClassReflection = $parentClassReflection->getParentClass();
}
return [
sprintf(
'Call to an undefined method %s::%s().',
$methodClassReflection->getName(),
$name
),
];
}
$methodReflection = $methodClassReflection->getMethod($name);
$messagesMethodName = $methodReflection->getDeclaringClass()->getName() . '::' . $methodReflection->getName() . '()';
if (!$scope->canCallMethod($methodReflection)) {
return [
sprintf('Cannot call method %s from current scope.', $messagesMethodName),
];
}
$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.',
]
);
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/AppendedArrayItemTypeRule.php 0000666 00000002102 13436751643 0015442 0 ustar 00 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 (!$assignedToType->getItemType()->accepts($assignedValueType)) {
return [
sprintf(
'Array (%s) does not accept %s.',
$assignedToType->describe(),
$assignedValueType->describe()
),
];
}
return [];
}
}
src/Rules/FunctionDefinitionCheck.php 0000666 00000006510 13436751643 0013714 0 ustar 00 broker = $broker;
}
/**
* @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 && $scope->getClass() !== null) {
if (!$this->broker->hasClass($scope->getClass())) {
return [];
}
return $this->checkParametersAcceptor(
$this->broker->getClass($scope->getClass())->getMethod($function->name),
$parameterMessage,
$returnMessage
);
}
if ($function instanceof Function_) {
$functionName = $function->name;
if (isset($function->namespacedName)) {
$functionName = $function->namespacedName;
}
return $this->checkParametersAcceptor(
$this->broker->getFunction(new Name($functionName)),
$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)
&& !$this->broker->hasClass($class)
) {
$errors[] = sprintf($parameterMessage, $param->name, $class);
}
}
$returnType = $function->getReturnType() instanceof NullableType
? (string) $function->getReturnType()->type
: (string) $function->getReturnType();
if (
$returnType
&& !in_array($returnType, self::VALID_TYPEHINTS, true)
&& !$this->broker->hasClass($returnType)
) {
$errors[] = sprintf($returnMessage, $returnType);
}
return $errors;
}
private function checkParametersAcceptor(
ParametersAcceptor $parametersAcceptor,
string $parameterMessage,
string $returnMessage
): array
{
$errors = [];
foreach ($parametersAcceptor->getParameters() as $parameter) {
foreach ($parameter->getType()->getReferencedClasses() as $class) {
if (!$this->broker->hasClass($class)) {
$errors[] = sprintf($parameterMessage, $parameter->getName(), $class);
}
}
if ($parameter->getType() instanceof NonexistentParentClassType) {
$errors[] = sprintf($parameterMessage, $parameter->getName(), $parameter->getType()->describe());
}
}
foreach ($parametersAcceptor->getReturnType()->getReferencedClasses() as $class) {
if (!$this->broker->hasClass($class)) {
$errors[] = sprintf($returnMessage, $class);
}
}
if ($parametersAcceptor->getReturnType() instanceof NonexistentParentClassType) {
$errors[] = sprintf($returnMessage, $parametersAcceptor->getReturnType()->describe());
}
return $errors;
}
}
src/Rules/Functions/CallToNonExistentFunctionRule.php 0000666 00000002325 13436751643 0017043 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) {
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 00000003111 13436751643 0014073 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 [];
}
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()
)
);
}
}
src/Rules/Functions/CallToFunctionParametersRule.php 0000666 00000003447 13436751643 0016676 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.',
]
);
}
}
src/Rules/Functions/ClosureReturnTypeRule.php 0000666 00000002165 13436751643 0015440 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 [];
}
return $this->returnTypeCheck->checkReturnType(
$scope,
$scope->getAnonymousFunctionReturnType(),
$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.',
true
);
}
}
src/Rules/Functions/PrintfParametersRule.php 0000666 00000005336 13436751643 0015253 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 = (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
}
$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/ExistingClassesInTypehintsRule.php 0000666 00000001723 13436751643 0017270 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 13436751643 0015421 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/ExistingClassesInClosureTypehintsRule.php 0000666 00000001601 13436751643 0020620 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/RegistryFactory.php 0000666 00000001130 13436751643 0012311 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/Registry.php 0000666 00000001771 13436751643 0010774 0 ustar 00 register($rule);
}
}
private function register(Rule $rule)
{
if (!isset($this->rules[$rule->getNodeType()])) {
$this->rules[$rule->getNodeType()] = [];
}
$this->rules[$rule->getNodeType()][] = $rule;
}
/**
* @param string[] $nodeTypes
* @return \PHPStan\Rules\Rule[]
*/
public function getRules(array $nodeTypes): array
{
$rules = [];
foreach ($nodeTypes as $nodeType) {
if (!isset($this->rules[$nodeType])) {
continue;
}
$classRules = $this->rules[$nodeType];
foreach ($classRules as $classRule) {
$classRuleClass = get_class($classRule);
if (!array_key_exists($classRuleClass, $rules)) {
$rules[$classRuleClass] = $classRule;
}
}
}
return array_values($rules);
}
}
src/Rules/Variables/DefinedVariableRule.php 0000666 00000001622 13436751643 0014723 0 ustar 00 name)) {
return [];
}
if (in_array($node->name, [
'GLOBALS',
'_SERVER',
'_GET',
'_POST',
'_FILES',
'_COOKIE',
'_SESSION',
'_REQUEST',
'_ENV',
], true)) {
return [];
}
if ($scope->isInVariableAssign($node->name)) {
return [];
}
if (!$scope->hasVariableType($node->name)) {
return [
sprintf('Undefined variable: $%s', $node->name),
];
}
return [];
}
}
src/Rules/Variables/ThisVariableRule.php 0000666 00000002107 13436751643 0014273 0 ustar 00 name) || $node->name !== 'this') {
return [];
}
if ($scope->getClass() === null && !$scope->isInAnonymousClass()) {
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->getClass() !== null ? $scope->getClass() : 'AnonymousClass',
$function->getName()
),
];
}
return [];
}
}
src/Rules/Variables/DefinedVariableInAnonymousFunctionUseRule.php 0000666 00000001247 13436751643 0021311 0 ustar 00 byRef) {
return [];
}
if (!$scope->hasVariableType($node->var)) {
return [
sprintf('Undefined variable: $%s', $node->var),
];
}
return [];
}
}
src/Rules/Variables/VariableCloningRule.php 0000666 00000001652 13436751643 0014761 0 ustar 00 getType($node->expr);
if ($type instanceof MixedType) {
return [];
}
if ($type->getClass() !== null) {
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/PhpDoc/PhpDocBlock.php 0000666 00000006223 13436751643 0011374 0 ustar 00 docComment = $docComment;
$this->file = $file;
}
public function getDocComment(): string
{
return $this->docComment;
}
public function getFile(): string
{
return $this->file;
}
public static function resolvePhpDocBlockForProperty(
Broker $broker,
string $docComment,
string $class,
string $propertyName,
string $file
): self
{
return self::resolvePhpDocBlock(
$broker,
$docComment,
$class,
$propertyName,
$file,
'hasProperty',
'getProperty',
__FUNCTION__
);
}
public static function resolvePhpDocBlockForMethod(
Broker $broker,
string $docComment,
string $class,
string $methodName,
string $file
): self
{
return self::resolvePhpDocBlock(
$broker,
$docComment,
$class,
$methodName,
$file,
'hasMethod',
'getMethod',
__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()->getNativeReflection();
$phpDocBlockFromClass = self::resolvePhpDocBlockFromClass(
$broker,
$parentClassReflection,
$name,
$hasMethodName,
$getMethodName,
$resolveMethodName
);
if ($phpDocBlockFromClass !== null) {
return $phpDocBlockFromClass;
}
} else {
foreach ($classReflection->getInterfaces() as $interface) {
$phpDocBlockFromClass = self::resolvePhpDocBlockFromClass(
$broker,
$interface->getNativeReflection(),
$name,
$hasMethodName,
$getMethodName,
$resolveMethodName
);
if ($phpDocBlockFromClass !== null) {
return $phpDocBlockFromClass;
}
}
}
}
return new self($docComment, $file);
}
/**
* @param \PHPStan\Broker\Broker $broker
* @param \ReflectionClass $classReflection
* @param string $name
* @param string $hasMethodName
* @param string $getMethodName
* @param string $resolveMethodName
* @return self|null
*/
private static function resolvePhpDocBlockFromClass(
Broker $broker,
\ReflectionClass $classReflection,
string $name,
string $hasMethodName,
string $getMethodName,
string $resolveMethodName
)
{
if ($classReflection->getFileName() !== false && $classReflection->$hasMethodName($name)) {
$parentMethodReflection = $classReflection->$getMethodName($name);
if ($parentMethodReflection->getDocComment() !== false) {
return self::$resolveMethodName(
$broker,
$parentMethodReflection->getDocComment(),
$classReflection->getName(),
$name,
$classReflection->getFileName()
);
}
}
return null;
}
}
build.xml 0000666 00000005276 13436751643 0006417 0 ustar 00
CODE_OF_CONDUCT.md 0000666 00000006227 13436751643 0007372 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 13436751643 0007347 0 ustar 00 *
!/log
!.*
composer.json 0000666 00000001556 13436751643 0007315 0 ustar 00 {
"name": "phpstan/phpstan",
"description": "PHPStan - PHP Static Analysis Tool",
"license": ["MIT"],
"minimum-stability": "dev",
"prefer-stable": true,
"bin": [
"bin/phpstan"
],
"extra": {
"branch-alias": {
"dev-master": "0.6-dev"
}
},
"require": {
"php": "~7.0",
"nette/bootstrap": "~2.4",
"nette/caching": "~2.4",
"nette/di": "~2.4",
"nette/robot-loader": "~2.4",
"nette/utils": "~2.4",
"nikic/php-parser": "^2.1 || ^3.0.2",
"symfony/console": "~2.8 || ~3.0",
"symfony/finder": "~2.8 || ~3.0"
},
"require-dev": {
"consistence/coding-standard": "~0.12.0",
"satooshi/php-coveralls": "^1.0",
"phing/phing": "^2.13.0",
"phpunit/phpunit": "^5.6",
"slevomat/coding-standard": "dev-php7#d4a1a9cd4e"
},
"autoload": {
"psr-4": {"PHPStan\\": "src/"}
},
"autoload-dev": {
"classmap": ["tests/PHPStan", "tests/TestCase.php"]
}
}
.travis.yml 0000666 00000001263 13436751643 0006677 0 ustar 00 language: php
php:
- 7.0
- 7.1
matrix:
include:
- php: 7.0
env: dependencies=lowest
- php: 7.0
env: dependencies=highest
- php: 7.1
env: dependencies=lowest
- php: 7.1
env: dependencies=highest
before_script:
- composer self-update
- if [ -z "$dependencies" ]; then composer install; fi;
- if [ "$dependencies" = "lowest" ]; then composer update --prefer-lowest -n; fi;
- if [ "$dependencies" = "highest" ]; then composer update -n; fi;
- stty cols 80 # weird Travis CI issue with PHPUnit ResultPrinter https://github.com/sebastianbergmann/phpunit/issues/1976
script:
- vendor/bin/phing
after_script:
- php vendor/bin/coveralls -v
.coveralls.yml 0000666 00000000142 13436751643 0007354 0 ustar 00 service_name: travis-ci
coverage_clover: tests/tmp/clover.xml
json_path: tests/tmp/coveralls.json
bin/phpstan 0000666 00000001414 13436751643 0006734 0 ustar 00 #!/usr/bin/env php
setCatchExceptions(false);
$application->add(new AnalyseCommand());
$application->run();
.gitignore 0000666 00000000065 13436751643 0006555 0 ustar 00 /composer.lock
/conf/config.local.yml
/vendor
/.idea
conf/config.level0.neon 0000666 00000003635 13436751643 0011034 0 ustar 00 parameters:
customRulesetUsed: false
checkThisOnly: true
services:
-
class: PHPStan\Rules\Classes\AccessPropertiesRule
arguments:
checkThisOnly: %checkThisOnly%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\AccessStaticPropertiesRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\ClassConstantRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\ExistingClassInInstanceOfRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\ExistingClassesInPropertiesRule
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\CatchedExceptionExistenceRule
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\PrintfParametersRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Functions\UnusedClosureUsesRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Methods\CallMethodsRule
arguments:
checkThisOnly: %checkThisOnly%
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\Variables\ThisVariableRule
tags:
- phpstan.rules.rule
conf/config.level5.neon 0000666 00000000147 13436751643 0011034 0 ustar 00 includes:
- config.level4.neon
parameters:
checkFunctionArgumentTypes: true
enableUnionTypes: true
conf/config.level3.neon 0000666 00000001256 13436751643 0011034 0 ustar 00 includes:
- config.level2.neon
services:
-
class: PHPStan\Rules\Arrays\AppendedArrayItemTypeRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\DefaultValueTypesAssignedToPropertiesRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Classes\TypesAssignedToPropertiesRule
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\Variables\VariableCloningRule
tags:
- phpstan.rules.rule
conf/config.neon 0000666 00000006103 13436751643 0007637 0 ustar 00 parameters:
bootstrap: null
excludes_analyse: []
autoload_directories: []
autoload_files: []
checkFunctionArgumentTypes: false
enableUnionTypes: false
polluteScopeWithLoopInitialAssignments: false
polluteCatchScopeWithTryAssignments: false
defineVariablesWithoutDefaultBranch: false
ignoreErrors: []
universalObjectCratesClasses:
- stdClass
- SimpleXMLElement
earlyTerminatingMethodCalls: []
memoryLimitFile: %tmpDir%/.memory_limit
services:
-
class: Nette\Caching\Cache
arguments:
storage: @cachingStorage
namespace: PHPStan
-
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\Analyser\Analyser
arguments:
analyseExcludes: %excludes_analyse%
ignoreErrors: %ignoreErrors%
bootstrapFile: %bootstrap%
-
class: PHPStan\Analyser\NodeScopeResolver
arguments:
polluteScopeWithLoopInitialAssignments: %polluteScopeWithLoopInitialAssignments%
polluteCatchScopeWithTryAssignments: %polluteCatchScopeWithTryAssignments%
defineVariablesWithoutDefaultBranch: %defineVariablesWithoutDefaultBranch%
earlyTerminatingMethodCalls: %earlyTerminatingMethodCalls%
-
class: PHPStan\Analyser\TypeSpecifier
-
class: PHPStan\Broker\BrokerFactory
-
class: PHPStan\Command\AnalyseApplication
arguments:
memoryLimitFile: %memoryLimitFile%
-
class: PHPStan\FileHelper
arguments:
workingDirectory: %currentWorkingDirectory%
-
class: PHPStan\Parser\CachedParser
arguments:
originalParser: @directParser
-
class: PHPStan\Parser\FunctionCallStatementFinder
-
implement: PHPStan\Reflection\FunctionReflectionFactory
-
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\FunctionCallParametersCheck
arguments:
checkArgumentTypes: %checkFunctionArgumentTypes%
-
class: PHPStan\Rules\FunctionDefinitionCheck
-
class: PHPStan\Rules\FunctionReturnTypeCheck
-
class: PHPStan\Rules\RegistryFactory
-
class: PHPStan\Rules\RuleLevelHelper
-
class: PHPStan\Rules\UnusedFunctionParametersCheck
-
class: PHPStan\Type\FileTypeMapper
arguments:
enableUnionTypes: %enableUnionTypes%
broker:
class: PHPStan\Broker\Broker
factory: @PHPStan\Broker\BrokerFactory::create
cachingStorage:
class: Nette\Caching\Storages\FileStorage
arguments:
dir: %rootDir%/tmp/cache
autowired: no
directParser:
class: PHPStan\Parser\DirectParser
autowired: no
registry:
class: PHPStan\Rules\Registry
factory: @PHPStan\Rules\RegistryFactory::create
conf/config.level4.neon 0000666 00000000345 13436751643 0011033 0 ustar 00 includes:
- config.level3.neon
services:
-
class: PHPStan\Rules\Cast\UselessCastRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Comparison\StrictComparisonOfDifferentTypesRule
tags:
- phpstan.rules.rule
conf/config.level2.neon 0000666 00000000103 13436751643 0011021 0 ustar 00 includes:
- config.level1.neon
parameters:
checkThisOnly: false
conf/config.level1.neon 0000666 00000000362 13436751643 0011027 0 ustar 00 includes:
- config.level0.neon
services:
-
class: PHPStan\Rules\Variables\DefinedVariableRule
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Variables\DefinedVariableInAnonymousFunctionUseRule
tags:
- phpstan.rules.rule
.gitattributes 0000666 00000000052 13436751643 0007454 0 ustar 00 /build export-ignore
/tests export-ignore