Update dependency tomasvotruba/type-coverage to ^2.3.0

This commit is contained in:
renovate[bot] 2026-07-30 01:28:42 +00:00
parent 4f08958e05
commit 7e531fa24d
200 changed files with 15475 additions and 40 deletions

View File

@ -3,7 +3,7 @@
"phpstan/phpstan": "^2.2.7", "phpstan/phpstan": "^2.2.7",
"phpstan/extension-installer": "^1.4.3", "phpstan/extension-installer": "^1.4.3",
"phpstan/phpstan-strict-rules": "^2.0.12", "phpstan/phpstan-strict-rules": "^2.0.12",
"tomasvotruba/type-coverage": "^2.2.2", "tomasvotruba/type-coverage": "^2.3.0",
"ergebnis/phpstan-rules": "^2.13.1" "ergebnis/phpstan-rules": "^2.13.1"
}, },
"config": { "config": {

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "f960036a066807c312a2d48672c1e045", "content-hash": "3579343022b2a28c7195fe30b1752bfb",
"packages": [], "packages": [],
"packages-dev": [ "packages-dev": [
{ {
@ -246,29 +246,31 @@
}, },
{ {
"name": "tomasvotruba/type-coverage", "name": "tomasvotruba/type-coverage",
"version": "2.2.2", "version": "2.3.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/TomasVotruba/type-coverage.git", "url": "https://github.com/TomasVotruba/type-coverage.git",
"reference": "25f298265c823e8fb0505169135855e1e6a91021" "reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/25f298265c823e8fb0505169135855e1e6a91021", "url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
"reference": "25f298265c823e8fb0505169135855e1e6a91021", "reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^8.4", "php": "^8.4",
"phpstan/phpstan": "^2.2" "phpstan/phpstan": "^2.2",
"webmozart/assert": "^1.11 || ^2.1"
}, },
"require-dev": { "require-dev": {
"phpstan/extension-installer": "^1.4", "phpstan/extension-installer": "^1.4",
"phpunit/phpunit": "^12.5", "phpunit/phpunit": "^13.2",
"rector/jack": "^1.0", "rector/jack": "^1.0",
"rector/rector": "^2.4", "rector/rector": "^2.5",
"shipmonk/composer-dependency-analyser": "^1.8", "shipmonk/composer-dependency-analyser": "^1.8",
"symplify/easy-coding-standard": "^13.1", "symfony/dom-crawler": "^8.1",
"symplify/easy-coding-standard": "^13.2",
"tomasvotruba/unused-public": "^2.2", "tomasvotruba/unused-public": "^2.2",
"tracy/tracy": "^2.12" "tracy/tracy": "^2.12"
}, },
@ -276,12 +278,14 @@
"extra": { "extra": {
"phpstan": { "phpstan": {
"includes": [ "includes": [
"config/extension.neon" "config/extension.neon",
"packages/type-perfect/config/extension.neon"
] ]
} }
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Rector\\TypePerfect\\": "packages/type-perfect/src",
"TomasVotruba\\TypeCoverage\\": "src" "TomasVotruba\\TypeCoverage\\": "src"
} }
}, },
@ -296,7 +300,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/TomasVotruba/type-coverage/issues", "issues": "https://github.com/TomasVotruba/type-coverage/issues",
"source": "https://github.com/TomasVotruba/type-coverage/tree/2.2.2" "source": "https://github.com/TomasVotruba/type-coverage/tree/2.3.0"
}, },
"funding": [ "funding": [
{ {
@ -308,7 +312,73 @@
"type": "github" "type": "github"
} }
], ],
"time": "2026-06-07T12:35:00+00:00" "time": "2026-07-29T20:32:37+00:00"
},
{
"name": "webmozart/assert",
"version": "2.4.1",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70",
"reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-date": "*",
"ext-filter": "*",
"php": "^8.2"
},
"suggest": {
"ext-intl": "",
"ext-simplexml": "",
"ext-spl": ""
},
"type": "library",
"extra": {
"psalm": {
"pluginClass": "Webmozart\\Assert\\PsalmPlugin"
},
"branch-alias": {
"dev-master": "2.0-dev",
"dev-feature/2-0": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"assert",
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/2.4.1"
},
"time": "2026-06-15T15:31:57+00:00"
} }
], ],
"aliases": [], "aliases": [],

View File

@ -251,45 +251,49 @@
}, },
{ {
"name": "tomasvotruba/type-coverage", "name": "tomasvotruba/type-coverage",
"version": "2.2.2", "version": "2.3.0",
"version_normalized": "2.2.2.0", "version_normalized": "2.3.0.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/TomasVotruba/type-coverage.git", "url": "https://github.com/TomasVotruba/type-coverage.git",
"reference": "25f298265c823e8fb0505169135855e1e6a91021" "reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/25f298265c823e8fb0505169135855e1e6a91021", "url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
"reference": "25f298265c823e8fb0505169135855e1e6a91021", "reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"php": "^8.4", "php": "^8.4",
"phpstan/phpstan": "^2.2" "phpstan/phpstan": "^2.2",
"webmozart/assert": "^1.11 || ^2.1"
}, },
"require-dev": { "require-dev": {
"phpstan/extension-installer": "^1.4", "phpstan/extension-installer": "^1.4",
"phpunit/phpunit": "^12.5", "phpunit/phpunit": "^13.2",
"rector/jack": "^1.0", "rector/jack": "^1.0",
"rector/rector": "^2.4", "rector/rector": "^2.5",
"shipmonk/composer-dependency-analyser": "^1.8", "shipmonk/composer-dependency-analyser": "^1.8",
"symplify/easy-coding-standard": "^13.1", "symfony/dom-crawler": "^8.1",
"symplify/easy-coding-standard": "^13.2",
"tomasvotruba/unused-public": "^2.2", "tomasvotruba/unused-public": "^2.2",
"tracy/tracy": "^2.12" "tracy/tracy": "^2.12"
}, },
"time": "2026-06-07T12:35:00+00:00", "time": "2026-07-29T20:32:37+00:00",
"type": "phpstan-extension", "type": "phpstan-extension",
"extra": { "extra": {
"phpstan": { "phpstan": {
"includes": [ "includes": [
"config/extension.neon" "config/extension.neon",
"packages/type-perfect/config/extension.neon"
] ]
} }
}, },
"installation-source": "dist", "installation-source": "dist",
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Rector\\TypePerfect\\": "packages/type-perfect/src",
"TomasVotruba\\TypeCoverage\\": "src" "TomasVotruba\\TypeCoverage\\": "src"
} }
}, },
@ -304,7 +308,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/TomasVotruba/type-coverage/issues", "issues": "https://github.com/TomasVotruba/type-coverage/issues",
"source": "https://github.com/TomasVotruba/type-coverage/tree/2.2.2" "source": "https://github.com/TomasVotruba/type-coverage/tree/2.3.0"
}, },
"funding": [ "funding": [
{ {
@ -317,6 +321,75 @@
} }
], ],
"install-path": "../tomasvotruba/type-coverage" "install-path": "../tomasvotruba/type-coverage"
},
{
"name": "webmozart/assert",
"version": "2.4.1",
"version_normalized": "2.4.1.0",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
"reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/2ccb7c2e821038c03a3e6e1700c570c158c55f70",
"reference": "2ccb7c2e821038c03a3e6e1700c570c158c55f70",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-date": "*",
"ext-filter": "*",
"php": "^8.2"
},
"suggest": {
"ext-intl": "",
"ext-simplexml": "",
"ext-spl": ""
},
"time": "2026-06-15T15:31:57+00:00",
"type": "library",
"extra": {
"psalm": {
"pluginClass": "Webmozart\\Assert\\PsalmPlugin"
},
"branch-alias": {
"dev-master": "2.0-dev",
"dev-feature/2-0": "2.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
},
{
"name": "Woody Gilk",
"email": "woody.gilk@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
"keywords": [
"assert",
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/2.4.1"
},
"install-path": "../webmozart/assert"
} }
], ],
"dev": true, "dev": true,
@ -325,6 +398,7 @@
"phpstan/extension-installer", "phpstan/extension-installer",
"phpstan/phpstan", "phpstan/phpstan",
"phpstan/phpstan-strict-rules", "phpstan/phpstan-strict-rules",
"tomasvotruba/type-coverage" "tomasvotruba/type-coverage",
"webmozart/assert"
] ]
} }

View File

@ -3,7 +3,7 @@
'name' => '__root__', 'name' => '__root__',
'pretty_version' => 'dev-main', 'pretty_version' => 'dev-main',
'version' => 'dev-main', 'version' => 'dev-main',
'reference' => 'd3ff5fec0e41d2e2b5ffa06594925d02356a03c0', 'reference' => '4f08958e05d5bfc330b2f0b05252a7b7ddbe0558',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@ -13,7 +13,7 @@
'__root__' => array( '__root__' => array(
'pretty_version' => 'dev-main', 'pretty_version' => 'dev-main',
'version' => 'dev-main', 'version' => 'dev-main',
'reference' => 'd3ff5fec0e41d2e2b5ffa06594925d02356a03c0', 'reference' => '4f08958e05d5bfc330b2f0b05252a7b7ddbe0558',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@ -56,13 +56,22 @@
'dev_requirement' => true, 'dev_requirement' => true,
), ),
'tomasvotruba/type-coverage' => array( 'tomasvotruba/type-coverage' => array(
'pretty_version' => '2.2.2', 'pretty_version' => '2.3.0',
'version' => '2.2.2.0', 'version' => '2.3.0.0',
'reference' => '25f298265c823e8fb0505169135855e1e6a91021', 'reference' => '7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644',
'type' => 'phpstan-extension', 'type' => 'phpstan-extension',
'install_path' => __DIR__ . '/../tomasvotruba/type-coverage', 'install_path' => __DIR__ . '/../tomasvotruba/type-coverage',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => true, 'dev_requirement' => true,
), ),
'webmozart/assert' => array(
'pretty_version' => '2.4.1',
'version' => '2.4.1.0',
'reference' => '2ccb7c2e821038c03a3e6e1700c570c158c55f70',
'type' => 'library',
'install_path' => __DIR__ . '/../webmozart/assert',
'aliases' => array(),
'dev_requirement' => true,
),
), ),
); );

View File

@ -138,3 +138,406 @@ vendor/bin/phpstan
<br> <br>
Happy coding! Happy coding!
<br>
---
# Type Perfect
[![Downloads](https://img.shields.io/packagist/dt/rector/type-perfect.svg?style=flat-square)](https://packagist.org/packages/rector/type-perfect/stats)
Next level type declaration check PHPStan rules.
We use these sets to improve code quality of our clients' code beyond PHPStan features.
* These rules make skipped object types explicit, param types narrow and help you to fill more accurate object type hints.
* **They're easy to enable, even if your code does not pass level 0**
* They're effortless to resolve and make your code instantly more solid and reliable.
If you care about code quality and type safety, add these 10 rules to your CI.
<br>
These rules ship with the package [installed above](#install), there is nothing else to require.
*Migrating from `rector/type-perfect`? Remove it from your `composer.json`, the rules live here now.*
<br>
Every rule is opt-in and disabled by default, so you can pick the ones that fit your project. See [Configure](#configure) below for the full list.
The 3 checks below are the simplest ones to start with:
```yaml
parameters:
type_perfect:
no_isset_on_object: true
no_empty_on_object: true
no_array_access_on_object: true
no_param_type_removal: true
```
The first one makes sure we don't miss a chance to use `instanceof` to make further code know about exact object type:
```php
private ?SomeType $someType = null;
if (! empty($this->someType)) {
// ...
}
if (! isset($this->someType)) {
// ...
}
// here we only know, that $this->someType is not empty/null
```
:no_good:
```php
if (! $this->someType instanceof SomeType) {
return;
}
// here we know $this->someType is exactly SomeType
```
:heavy_check_mark:
<br>
Second rule (`no_array_access_on_object`) checks we use explicit object methods over magic array access:
```php
$article = new Article();
$id = $article['id'];
// we have no idea, what the type is
```
:no_good:
```php
$id = $article->getId();
// we know the type is int
```
:heavy_check_mark:
<br>
Last rule (`no_param_type_removal`) checks that all interface implementations follow the same method signature as the interface:
```php
interface SomeInterface
{
public function doSomething(int $value): void;
}
final class SomeClass implements SomeInterface
{
public function doSomething($value): void { // ... }
}
```
:no_good:
```php
final class SomeClass implements SomeInterface
{
public function doSomething(int $value): void { // ... }
}
```
:heavy_check_mark:
<br>
## Configure
All rules are enabled by configuration and disabled by default. We take them from the simplest to more powerful, in the same order we apply them on legacy projects.
You can enable them all at once:
```yaml
parameters:
type_perfect:
# the 3 checks above
no_isset_on_object: true
no_empty_on_object: true
no_array_access_on_object: true
no_param_type_removal: true
no_mixed_property: true
no_mixed_caller: true
null_over_false: true
narrow_param: true
narrow_return: true
```
Or one by one:
<br>
## 1. Null over False
```yaml
parameters:
type_perfect:
null_over_false: true
```
Bool types are typically used for on/off, yes/no responses. But sometimes, the `false` is misused as *no-result* response, where `null` would be more accurate:
```php
public function getProduct()
{
if (...) {
return $product;
}
return false;
}
```
:no_good:
We should use `null` instead, as it enabled strict type declaration in form of `?Product` since PHP 7.1:
```php
public function getProduct(): ?Product
{
if (...) {
return $product;
}
return null;
}
```
:heavy_check_mark:
<br>
## 2. No mixed Property
```yaml
parameters:
type_perfect:
no_mixed_property: true
```
This rule focuses on PHPStan blind spot while fetching a property. If we have a property with unknown type, PHPStan is not be able to analyse it. It silently ignores it.
```php
private $someType;
public function run()
{
$this->someType->vale;
}
```
It doesn't see there is a typo in `vale` property name. It should be `value`
:no_good:
```php
private SomeType $someType;
public function run()
{
$this->someType->value;
}
```
This rule makes sure all property fetches know their type they're called on.
:heavy_check_mark:
<br>
## 3. No mixed Caller
```yaml
parameters:
type_perfect:
no_mixed_caller: true
```
Same as above, only for method calls:
```php
private $someType;
public function run()
{
$this->someType->someMetho(1, 2);
}
```
It doesn't see there is a typo in `someMetho` name, and that the 2nd parameter must be `string`.
:no_good:
```php
private SomeType $someType;
public function run()
{
$this->someType->someMethod(1, 'active');
}
```
This group makes sure methods call know their type they're called on.
:heavy_check_mark:
<br>
## 4. Narrow Param Types
The more narrow param type we have, the reliable the code is. `string` beats `mixed`, `int` beats `scalar` and `ExactObject` beats `stdClass`.
```yaml
parameters:
type_perfect:
narrow_param: true
```
In case of `private`, but also `public` method calls, our project often knows exact types that are passed in it:
```php
// in one file
$product->addPrice(100.52);
// another file
$product->addPrice(52.05);
```
But out of from fear and "just to be safe", we keep the `addPrice()` param type empty, `mixed` or in a docblock.
:no_good:
If, in 100 % cases the `float` type is passed, PHPStan knows it can be added and improve further analysis:
```diff
-/**
- * @param float $price
- */
-public function addPrice($price)
+public function addPrice(float $price)
{
$this->price = $price;
}
```
That's where this group comes in. It checks all the passed types, and tells us know how to narrow the param type declaration.
:heavy_check_mark:
<br>
## 5. Narrow Return Types
Last but not least, the more narrow return type, the more reliable the code.
```yaml
parameters:
type_perfect:
narrow_return: true
```
Where does it help? Let's say we have 2 types of talks, that do have different behavior:
```php
final class ConferenceTalk extends Talk
{
public function bookHotel()
{
// ...
}
}
final class MeetupTalk extends Talk
{
public function bookTrain()
{
// ...
}
}
```
Then we have a factory (repository, or services) that returns generic `Talk` type:
```php
final class TalkFactory
{
public function createConferenceTalk(): Talk
{
return new ConferenceTalk();
}
public function createMeetupTalk(): Talk
{
return new MeetupTalk();
}
}
```
In this case we've just lost strict type and have to verify the type on runtime:
```php
$talk instanceof ConferenceTalk
```
:no_good:
That's where this group comes in. In case we return the exact type, we should use exact type in return type declaration to keep the code as reliable as possible:
```diff
final class TalkFactory
{
- public function createConferenceTalk(): Talk
+ public function createConferenceTalk(): ConferenceTalk
{
return new ConferenceTalk();
}
- public function createMeetupTalk(): Talk
+ public function createMeetupTalk(): MeetupTalk
{
return new MeetupTalk();
}
}
```
:heavy_check_mark:
Add sets one by one, fix what you find helpful and ignore the rest.
<br>
Happy coding!

View File

@ -7,4 +7,9 @@ use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType;
return new Configuration() return new Configuration()
// PhpParser classes are provided by phpstan/phpstan, no need to require nikic/php-parser directly // PhpParser classes are provided by phpstan/phpstan, no need to require nikic/php-parser directly
->ignoreErrorsOnPackage('nikic/php-parser', [ErrorType::SHADOW_DEPENDENCY]); ->ignoreErrorsOnPackage('nikic/php-parser', [ErrorType::SHADOW_DEPENDENCY])
// type-perfect test fixtures are static analysis inputs, not real code, and reference 3rd-party classes on purpose
->ignoreErrorsOnPath(
__DIR__ . '/packages/type-perfect/tests',
[ErrorType::UNKNOWN_CLASS, ErrorType::SHADOW_DEPENDENCY]
);

View File

@ -6,26 +6,30 @@
"keywords": ["static analysis", "phpstan-extension"], "keywords": ["static analysis", "phpstan-extension"],
"require": { "require": {
"php": "^8.4", "php": "^8.4",
"phpstan/phpstan": "^2.2" "phpstan/phpstan": "^2.2",
"webmozart/assert": "^1.11 || ^2.1"
}, },
"require-dev": { "require-dev": {
"phpstan/extension-installer": "^1.4", "phpstan/extension-installer": "^1.4",
"phpunit/phpunit": "^12.5", "phpunit/phpunit": "^13.2",
"symplify/easy-coding-standard": "^13.1", "symplify/easy-coding-standard": "^13.2",
"rector/rector": "^2.4", "rector/rector": "^2.5",
"tracy/tracy": "^2.12", "tracy/tracy": "^2.12",
"tomasvotruba/unused-public": "^2.2", "tomasvotruba/unused-public": "^2.2",
"rector/jack": "^1.0", "rector/jack": "^1.0",
"shipmonk/composer-dependency-analyser": "^1.8" "shipmonk/composer-dependency-analyser": "^1.8",
"symfony/dom-crawler": "^8.1"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"TomasVotruba\\TypeCoverage\\": "src" "TomasVotruba\\TypeCoverage\\": "src",
"Rector\\TypePerfect\\": "packages/type-perfect/src"
} }
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"TomasVotruba\\TypeCoverage\\Tests\\": "tests" "TomasVotruba\\TypeCoverage\\Tests\\": "tests",
"Rector\\TypePerfect\\Tests\\": "packages/type-perfect/tests"
} }
}, },
"scripts": { "scripts": {
@ -43,7 +47,8 @@
"extra": { "extra": {
"phpstan": { "phpstan": {
"includes": [ "includes": [
"config/extension.neon" "config/extension.neon",
"packages/type-perfect/config/extension.neon"
] ]
} }
} }

View File

@ -0,0 +1,25 @@
The MIT License
---------------
Copyright (c) 2020 Tomas Votruba (https://tomasvotruba.com)
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,94 @@
parametersSchema:
# see https://doc.nette.org/en/schema for configuration
type_perfect: structure([
narrow_param: bool()
narrow_return: bool()
no_mixed: bool()
null_over_false: bool()
# replace for no_mixed
no_mixed_property: bool()
no_mixed_caller: bool()
no_param_type_removal: bool()
no_array_access_on_object: bool()
no_isset_on_object: bool()
no_empty_on_object: bool()
])
# defaults
parameters:
type_perfect:
narrow_param: false
narrow_return: false
no_mixed: false
null_over_false: false
# priority override of no_mixed
no_mixed_property: false
no_mixed_caller: false
no_param_type_removal: false
no_array_access_on_object: false
no_isset_on_object: false
no_empty_on_object: false
rules:
# turn on by the group name
# no_param_type_removal
- Rector\TypePerfect\Rules\NoParamTypeRemovalRule
# no_array_access_on_object
- Rector\TypePerfect\Rules\NoArrayAccessOnObjectRule
# no_isset_on_object
- Rector\TypePerfect\Rules\NoIssetOnObjectRule
# no_empty_on_object
- Rector\TypePerfect\Rules\NoEmptyOnObjectRule
# narrow_param
- Rector\TypePerfect\Rules\NarrowPublicClassMethodParamTypeRule
- Rector\TypePerfect\Rules\NarrowPrivateClassMethodParamTypeRule
# narrow_return
- Rector\TypePerfect\Rules\NarrowReturnObjectTypeRule
# no_mixed
- Rector\TypePerfect\Rules\NoMixedPropertyFetcherRule
- Rector\TypePerfect\Rules\NoMixedMethodCallerRule
# null_over_false
- Rector\TypePerfect\Rules\ReturnNullOverFalseRule
services:
-
factory: Rector\TypePerfect\Configuration
arguments:
- %type_perfect%
- Rector\TypePerfect\NodeFinder\ClassMethodNodeFinder
- Rector\TypePerfect\NodeFinder\MethodCallNodeFinder
- Rector\TypePerfect\NodeFinder\ReturnNodeFinder
- Rector\TypePerfect\PhpDoc\ApiDocStmtAnalyzer
- Rector\TypePerfect\Printer\NodeComparator
- Rector\TypePerfect\Reflection\ReflectionParser
- Rector\TypePerfect\Reflection\MethodNodeAnalyser
- Rector\TypePerfect\Matcher\Collector\PublicClassMethodMatcher
- Rector\TypePerfect\Matcher\ClassMethodCallReferenceResolver
- Rector\TypePerfect\Printer\CollectorMetadataPrinter
- Rector\TypePerfect\Guard\EmptyIssetGuard
# for NarrowPublicClassMethodParamTypeByCallerTypeRule
-
class: Rector\TypePerfect\Collector\ClassMethod\PublicClassMethodParamTypesCollector
tags: [phpstan.collector]
-
class: Rector\TypePerfect\Collector\MethodCall\MethodCallArgTypesCollector
tags: [phpstan.collector]
-
class: Rector\TypePerfect\Collector\MethodCallableNode\MethodCallableCollector
tags:
- phpstan.collector

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Collector\ClassMethod;
use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Reflection\ClassReflection;
use Rector\TypePerfect\Configuration;
use Rector\TypePerfect\Matcher\Collector\PublicClassMethodMatcher;
use Rector\TypePerfect\PhpDoc\ApiDocStmtAnalyzer;
use Rector\TypePerfect\Printer\CollectorMetadataPrinter;
/**
* @implements Collector<ClassMethod, array{class-string, string, string, int}>
*/
final readonly class PublicClassMethodParamTypesCollector implements Collector
{
public function __construct(
private ApiDocStmtAnalyzer $apiDocStmtAnalyzer,
private PublicClassMethodMatcher $publicClassMethodMatcher,
private CollectorMetadataPrinter $collectorMetadataPrinter,
private Configuration $configuration
) {
}
public function getNodeType(): string
{
return ClassMethod::class;
}
/**
* @param ClassMethod $node
* @return array{class-string, string, string, int}|null
*/
public function processNode(Node $node, Scope $scope): ?array
{
if (! $this->configuration->isNarrowParamEnabled()) {
return null;
}
if ($node->params === []) {
return null;
}
if ($this->publicClassMethodMatcher->shouldSkipClassMethod($node)) {
return null;
}
// only if the class has no parents/implementers, to avoid class method required by contracts
$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return null;
}
if ($this->apiDocStmtAnalyzer->hasApiDoc($node, $classReflection)) {
return null;
}
if ($this->publicClassMethodMatcher->shouldSkipClassReflection($classReflection)) {
return null;
}
$methodName = $node->name->toString();
// is this method required by parent contract? skip it
if ($this->publicClassMethodMatcher->isUsedByParentClassOrInterface($classReflection, $methodName)) {
return null;
}
$printedParamTypesString = $this->collectorMetadataPrinter->printParamTypesToString(
$node,
$classReflection,
$scope
);
return [$classReflection->getName(), $methodName, $printedParamTypesString, $node->getLine()];
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Collector\MethodCall;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Reflection\ExtendedMethodReflection;
use Rector\TypePerfect\Configuration;
use Rector\TypePerfect\Matcher\ClassMethodCallReferenceResolver;
use Rector\TypePerfect\Printer\CollectorMetadataPrinter;
use Rector\TypePerfect\ValueObject\MethodCallReference;
/**
* @implements Collector<MethodCall, array{string, string}>
*/
final readonly class MethodCallArgTypesCollector implements Collector
{
public function __construct(
private ClassMethodCallReferenceResolver $classMethodCallReferenceResolver,
private CollectorMetadataPrinter $collectorMetadataPrinter,
private Configuration $configuration
) {
}
public function getNodeType(): string
{
return MethodCall::class;
}
/**
* @param MethodCall $node
* @return array{string, string}|null
*/
public function processNode(Node $node, Scope $scope): ?array
{
if (! $this->configuration->isNarrowParamEnabled()) {
return null;
}
if ($node->isFirstClassCallable() || $node->getArgs() === [] || ! $node->name instanceof Identifier) {
return null;
}
$methodCalledOnType = $scope->getType($node->var);
$methodReflection = $scope->getMethodReflection($methodCalledOnType, $node->name->name);
if (! $methodReflection instanceof ExtendedMethodReflection) {
return null;
}
$classMethodCallReference = $this->classMethodCallReferenceResolver->resolve($node, $scope, true);
if (! $classMethodCallReference instanceof MethodCallReference) {
return null;
}
$classMethodReference = $this->createClassMethodReference($classMethodCallReference);
$stringArgTypesString = $this->collectorMetadataPrinter->printArgTypesAsString(
$node,
$methodReflection,
$scope
);
return [$classMethodReference, $stringArgTypesString];
}
private function createClassMethodReference(MethodCallReference $classMethodCallReference): string
{
$className = $classMethodCallReference->getClass();
$methodName = $classMethodCallReference->getMethod();
return $className . '::' . $methodName;
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Collector\MethodCallableNode;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Node\MethodCallableNode;
use Rector\TypePerfect\Configuration;
use Rector\TypePerfect\Matcher\ClassMethodCallReferenceResolver;
use Rector\TypePerfect\ValueObject\MethodCallReference;
/**
* @implements Collector<MethodCallableNode, array<string>|null>
*
* PHPStan has special node for first class callables of MethodCall
*
* @see https://github.com/phpstan/phpstan-src/blob/511c1e435fb43b8eb0ac310e6aa3230147963790/src/Analyser/NodeScopeResolver.php#L1936
*/
final readonly class MethodCallableCollector implements Collector
{
public function __construct(
private ClassMethodCallReferenceResolver $classMethodCallReferenceResolver,
private Configuration $configuration
) {
}
public function getNodeType(): string
{
return MethodCallableNode::class;
}
/**
* @param MethodCallableNode $node
* @return array{string}|null
*/
public function processNode(Node $node, Scope $scope): ?array
{
if (! $this->configuration->isNarrowParamEnabled()) {
return null;
}
$classMethodCallReference = $this->classMethodCallReferenceResolver->resolve($node, $scope, true);
if (! $classMethodCallReference instanceof MethodCallReference) {
return null;
}
$classMethodReference = $this->createClassMethodReference($classMethodCallReference);
// special case that should skip everything
return [$classMethodReference];
}
private function createClassMethodReference(MethodCallReference $classMethodCallReference): string
{
$className = $classMethodCallReference->getClass();
$methodName = $classMethodCallReference->getMethod();
return $className . '::' . $methodName;
}
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect;
final class Configuration
{
/**
* @param array<string, mixed> $parameters
*/
public function __construct(
private array $parameters
) {
// enabed by default in tests
if (defined('PHPUNIT_COMPOSER_INSTALL')) {
$this->parameters['narrow_param'] = true;
$this->parameters['narrow_return'] = true;
$this->parameters['no_mixed'] = true;
$this->parameters['null_over_false'] = true;
$this->parameters['no_param_type_removal'] = true;
$this->parameters['no_array_access_on_object'] = true;
$this->parameters['no_isset_on_object'] = true;
$this->parameters['no_empty_on_object'] = true;
}
}
public function isNarrowParamEnabled(): bool
{
return $this->parameters['narrow_param'] ?? false;
}
public function isNarrowReturnEnabled(): bool
{
return $this->parameters['narrow_return'] ?? false;
}
public function isNoMixedPropertyEnabled(): bool
{
if ($this->parameters['no_mixed_property']) {
return true;
}
return $this->parameters['no_mixed'] ?? false;
}
public function isNoMixedCallerEnabled(): bool
{
if ($this->parameters['no_mixed_caller']) {
return true;
}
return $this->parameters['no_mixed'] ?? false;
}
public function isNoFalsyReturnEnabled(): bool
{
return $this->parameters['null_over_false'] ?? false;
}
public function isNoParamTypeRemovalEnabled(): bool
{
return $this->parameters['no_param_type_removal'] ?? false;
}
public function isNoArrayAccessOnObjectEnabled(): bool
{
return $this->parameters['no_array_access_on_object'] ?? false;
}
public function isNoIssetOnObjectEnabled(): bool
{
return $this->parameters['no_isset_on_object'] ?? false;
}
public function isNoEmptyOnObjectEnabled(): bool
{
return $this->parameters['no_empty_on_object'] ?? false;
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Enum\Types;
final class ResolvedTypes
{
public const string UNKNOWN_TYPES = 'unknown_types';
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Guard;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Variable;
use PHPStan\Analyser\Scope;
use PHPStan\Type\TypeCombinator;
final class EmptyIssetGuard
{
public function isLegal(Expr $expr, Scope $scope): bool
{
if ($expr instanceof ArrayDimFetch) {
return true;
}
if (! $expr instanceof Variable) {
return true;
}
if ($expr->name instanceof Expr) {
return true;
}
if (! $scope->hasVariableType($expr->name)->yes()) {
return true;
}
$varType = $scope->getNativeType($expr);
$varType = TypeCombinator::removeNull($varType);
return $varType->getObjectClassNames() === [];
}
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Matcher;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Node\MethodCallableNode;
use PHPStan\Type\ThisType;
use PHPStan\Type\TypeCombinator;
use Rector\TypePerfect\ValueObject\MethodCallReference;
final class ClassMethodCallReferenceResolver
{
public function resolve(
MethodCall|MethodCallableNode $methodCallOrMethodCallable,
Scope $scope,
bool $allowThisType
): ?MethodCallReference {
if ($methodCallOrMethodCallable instanceof MethodCallableNode) {
$methodName = $methodCallOrMethodCallable->getName();
$variable = $methodCallOrMethodCallable->getVar();
} else {
$methodName = $methodCallOrMethodCallable->name;
$variable = $methodCallOrMethodCallable->var;
}
if ($methodName instanceof Expr) {
return null;
}
$callerType = $scope->getType($variable);
// remove optional nullable type
if (TypeCombinator::containsNull($callerType)) {
$callerType = TypeCombinator::removeNull($callerType);
}
if (! $allowThisType && $callerType instanceof ThisType) {
return null;
}
if (count($callerType->getObjectClassNames()) !== 1) {
return null;
}
// move to the class where method is defined, e.g. parent class defines the method, so it should be checked there
$className = $callerType->getObjectClassNames()[0];
$methodNameString = $methodName->toString();
return new MethodCallReference($className, $methodNameString);
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Matcher\Collector;
use PhpParser\Comment\Doc;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Reflection\ClassReflection;
final class PublicClassMethodMatcher
{
/**
* @var string[]
*/
private const array SKIPPED_TYPES = [
'PHPUnit\Framework\TestCase',
'Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator',
];
public function shouldSkipClassReflection(ClassReflection $classReflection): bool
{
// skip interface as required, traits as unable to detect for sure
if (! $classReflection->isClass()) {
return true;
}
return array_any(self::SKIPPED_TYPES, fn (string $skippedType): bool => $classReflection->is($skippedType));
}
public function isUsedByParentClassOrInterface(ClassReflection $classReflection, string $methodName): bool
{
// is this method required by parent contract? skip it
foreach ($classReflection->getInterfaces() as $parentInterfaceReflection) {
if ($parentInterfaceReflection->hasMethod($methodName)) {
return true;
}
}
return array_any($classReflection->getParents(), fn (ClassReflection $parentClassReflection): bool => $parentClassReflection->hasMethod($methodName));
}
public function shouldSkipClassMethod(ClassMethod $classMethod): bool
{
if ($classMethod->isMagic()) {
return true;
}
if ($classMethod->isStatic()) {
return true;
}
// skip attributes
if ($classMethod->attrGroups !== []) {
return true;
}
if (! $classMethod->isPublic()) {
return true;
}
$doc = $classMethod->getDocComment();
// skip symfony action
return $doc instanceof Doc && str_contains($doc->getText(), '@Route');
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\NodeFinder;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use Rector\TypePerfect\Reflection\ReflectionParser;
final readonly class ClassMethodNodeFinder
{
public function __construct(
private ReflectionParser $reflectionParser,
) {
}
public function findByMethodCall(MethodCall $methodCall, Scope $scope): ?ClassMethod
{
$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return null;
}
$classLike = $this->reflectionParser->parseClassReflection($classReflection);
if (! $classLike instanceof Class_) {
return null;
}
if (! $methodCall->name instanceof Identifier) {
return null;
}
$methodCallName = $methodCall->name->toString();
$classMethod = $classLike->getMethod($methodCallName);
if (! $classMethod instanceof ClassMethod) {
return null;
}
if (! $classMethod->isPrivate()) {
return null;
}
return $classMethod;
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\NodeFinder;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Stmt\Class_;
use PhpParser\NodeFinder;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use Rector\TypePerfect\Printer\NodeComparator;
use Rector\TypePerfect\Reflection\ReflectionParser;
use Webmozart\Assert\Assert;
final readonly class MethodCallNodeFinder
{
private NodeFinder $nodeFinder;
public function __construct(
private ReflectionParser $reflectionParser,
private NodeComparator $nodeComparator,
) {
$this->nodeFinder = new NodeFinder();
}
/**
* @return MethodCall[]
*/
public function findUsages(MethodCall $methodCall, Scope $scope): array
{
$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return [];
}
$classLike = $this->reflectionParser->parseClassReflection($classReflection);
if (! $classLike instanceof Class_) {
return [];
}
$methodCalls = $this->nodeFinder->find($classLike, function (Node $node) use ($methodCall): bool {
if (! $node instanceof MethodCall) {
return false;
}
if (! $this->nodeComparator->areNodesEqual($node->var, $methodCall->var)) {
return false;
}
return $this->nodeComparator->areNodesEqual($node->name, $methodCall->name);
});
Assert::allIsInstanceOf($methodCalls, MethodCall::class);
return $methodCalls;
}
}

View File

@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\NodeFinder;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Return_;
use PhpParser\NodeTraverser;
use Rector\TypePerfect\NodeVisitor\CallableNodeVisitor;
final readonly class ReturnNodeFinder
{
public function findOnlyReturnsExpr(ClassMethod $classMethod): Expr|null
{
$returns = $this->findReturnsWithValues($classMethod);
if (count($returns) !== 1) {
return null;
}
$onlyReturn = $returns[0];
return $onlyReturn->expr;
}
/**
* @return Return_[]
*/
public function findReturnsWithValues(ClassMethod $classMethod): array
{
$returns = [];
$this->traverseNodesWithCallable((array) $classMethod->stmts, static function (
Node $node
) use (&$returns) {
// skip different scope
if ($node instanceof FunctionLike) {
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
}
if (! $node instanceof Return_) {
return null;
}
if (! $node->expr instanceof Expr) {
return null;
}
$returns[] = $node;
});
return $returns;
}
/**
* @param callable(Node $node): (int|Node|null) $callable
* @param Node[] $nodes
*/
private function traverseNodesWithCallable(array $nodes, callable $callable): void
{
$nodeTraverser = new NodeTraverser();
$callableNodeVisitor = new CallableNodeVisitor($callable);
$nodeTraverser->addVisitor($callableNodeVisitor);
$nodeTraverser->traverse($nodes);
}
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\NodeVisitor;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Expression;
use PhpParser\NodeVisitorAbstract;
final class CallableNodeVisitor extends NodeVisitorAbstract
{
/**
* @var callable(Node): (int|Node|null)
*/
private $callable;
/**
* @param callable(Node $node): (int|Node|null) $callable
*/
public function __construct(callable $callable)
{
$this->callable = $callable;
}
public function enterNode(Node $node): int|Node|null
{
$originalNode = $node;
$callable = $this->callable;
/** @var int|Node|null $newNode */
$newNode = $callable($node);
if ($originalNode instanceof Stmt && $newNode instanceof Expr) {
return new Expression($newNode);
}
return $newNode;
}
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\PhpDoc;
use PhpParser\Comment\Doc;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\Reflection\ClassReflection;
final class ApiDocStmtAnalyzer
{
public function hasApiDoc(ClassMethod $classMethod, ClassReflection $classReflection): bool
{
if ($this->hasClassReflectionApiDoc($classReflection)) {
return true;
}
$docComment = $classMethod->getDocComment();
if (! $docComment instanceof Doc) {
return false;
}
return str_contains($docComment->getText(), '@api');
}
private function hasClassReflectionApiDoc(ClassReflection $classReflection): bool
{
if (! $classReflection->getResolvedPhpDoc() instanceof ResolvedPhpDocBlock) {
return false;
}
$resolvedPhpDocBlock = $classReflection->getResolvedPhpDoc();
return str_contains($resolvedPhpDocBlock->getPhpDocString(), '@api');
}
}

View File

@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Printer;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PhpParser\Node\IntersectionType as NodeIntersectionType;
use PhpParser\Node\Name;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\NullableType;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\UnionType;
use PhpParser\PrettyPrinter\Standard;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ExtendedMethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\ClosureType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ThisType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType as PHPStanUnionType;
use PHPStan\Type\VerbosityLevel;
use Rector\TypePerfect\Enum\Types\ResolvedTypes;
final readonly class CollectorMetadataPrinter
{
private Standard $standard;
public function __construct(
) {
$this->standard = new Standard();
}
public function printArgTypesAsString(
MethodCall $methodCall,
ExtendedMethodReflection $extendedMethodReflection,
Scope $scope
): string {
$parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
$scope,
$methodCall->getArgs(),
$extendedMethodReflection->getVariants(),
$extendedMethodReflection->getNamedArgumentsVariants()
);
$parameters = $parametersAcceptor->getParameters();
$stringArgTypes = [];
foreach ($methodCall->getArgs() as $i => $arg) {
$argType = $scope->getType($arg->value);
// we have no idea, nothing we can do
if ($argType instanceof MixedType) {
return ResolvedTypes::UNKNOWN_TYPES;
}
if ($argType instanceof IntersectionType) {
return ResolvedTypes::UNKNOWN_TYPES;
}
if ($argType instanceof GenericObjectType) {
return ResolvedTypes::UNKNOWN_TYPES;
}
if ($argType instanceof PHPStanUnionType) {
return ResolvedTypes::UNKNOWN_TYPES;
}
if ($argType instanceof ThisType) {
$argType = new ObjectType($argType->getClassName());
}
if (array_key_exists($i, $parameters)) {
$defaultValueType = $parameters[$i]->getDefaultValue();
if ($defaultValueType instanceof Type) {
$argType = TypeCombinator::union($argType, $defaultValueType);
}
}
$printedArgType = $this->printTypeToString($argType);
$printedArgType = $this->normalizeDateTime($printedArgType);
$stringArgTypes[] = $printedArgType;
}
return implode('|', $stringArgTypes);
}
public function printParamTypesToString(
ClassMethod $classMethod,
ClassReflection $classReflection,
Scope $scope
): string {
$className = $classReflection->getName();
$parametersReflection = [];
if ($classReflection->hasMethod($classMethod->name->name)) {
$methodReflection = $classReflection->getMethod($classMethod->name->name, $scope);
$variants = $methodReflection->getVariants();
if (count($variants) === 1) {
$parametersReflection = $variants[0]->getParameters();
}
}
$printedParamTypes = [];
foreach ($classMethod->params as $i => $param) {
if ($param->type === null) {
$printedParamTypes[] = '';
continue;
}
$phpdocType = null;
if (array_key_exists($i, $parametersReflection)) {
$paramphpdocType = $parametersReflection[$i]->getPhpDocType();
if (! $paramphpdocType instanceof MixedType) {
$phpdocType = $paramphpdocType;
}
}
if ($phpdocType instanceof Type) {
$printedParamType = $this->printTypeToString($phpdocType);
} else {
$paramType = $this->transformSelfToClassName($param->type, $className);
if ($paramType instanceof NullableType) {
// unite to phpstan type
$paramType = new UnionType([$paramType->type, new Identifier('null')]);
}
if ($paramType instanceof UnionType || $paramType instanceof NodeIntersectionType) {
$paramType = $this->resolveSortedTypes($paramType, $className);
}
$printedParamType = $this->standard->prettyPrint([$paramType]);
$printedParamType = str_replace('\Closure', 'callable', $printedParamType);
$printedParamType = ltrim($printedParamType, '\\');
$printedParamType = str_replace('|\\', '|', $printedParamType);
}
// to avoid DateTime vs DateTimeImmutable vs DateTimeInterface conflicts
$printedParamType = $this->normalizeDateTime($printedParamType);
$printedParamTypes[] = $printedParamType;
}
return implode('|', $printedParamTypes);
}
private function transformSelfToClassName(Node $node, ?string $className): Node
{
if (! $node instanceof Name || $className === null) {
return $node;
}
if ($node->toString() !== 'self') {
return $node;
}
return new FullyQualified($className);
}
private function resolveSortedTypes(
UnionType|NodeIntersectionType $paramType,
?string $className
): UnionType|NodeIntersectionType {
$typeNames = [];
foreach ($paramType->types as $type) {
if ($type instanceof NodeIntersectionType) {
foreach ($type->types as $intersectionType) {
/** @var Identifier|Name $intersectionType */
$intersectionType = $this->transformSelfToClassName($intersectionType, $className);
$typeNames[] = (string) $intersectionType;
}
continue;
}
/** @var Identifier|Name $type */
$type = $this->transformSelfToClassName($type, $className);
$typeNames[] = (string) $type;
}
sort($typeNames);
$types = [];
foreach ($typeNames as $typeName) {
$types[] = new Identifier($typeName);
}
if ($paramType instanceof NodeIntersectionType) {
return new NodeIntersectionType($types);
}
return new UnionType($types);
}
private function printTypeToString(Type $type): string
{
if ($type->isClassString()->yes()) {
return 'string';
}
if ($type->isArray()->yes()) {
return 'array';
}
if ($type->isBoolean()->yes()) {
return 'bool';
}
if ($type instanceof IntegerRangeType) {
return 'int';
}
if ($type instanceof ClosureType) {
return 'callable';
}
if (count($type->getEnumCases()) === 1) {
return $type->getEnumCases()[0]
->getClassName();
}
return $type->describe(VerbosityLevel::typeOnly());
}
private function normalizeDateTime(string $printedType): string
{
if ($printedType === 'DateTimeImmutable') {
return 'DateTimeInterface';
}
if ($printedType === 'DateTime') {
return 'DateTimeInterface';
}
return $printedType;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Printer;
use PhpParser\Node;
use PHPStan\Node\Printer\Printer;
final readonly class NodeComparator
{
public function __construct(
private Printer $printer
) {
}
public function areNodesEqual(Node $firstNode, Node $secondNode): bool
{
// remove comments from nodes
$firstNode->setAttribute('comments', null);
$secondNode->setAttribute('comments', null);
return $this->printer->prettyPrint([$firstNode]) === $this->printer->prettyPrint([$secondNode]);
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Reflection;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
final class MethodNodeAnalyser
{
public function hasParentVendorLock(Scope $scope, string $methodName): bool
{
return $this->matchFirstParentClassMethod($scope, $methodName) instanceof MethodReflection;
}
public function matchFirstParentClassMethod(Scope $scope, string $methodName): ?MethodReflection
{
$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return null;
}
// the classes have highter priority, e.g. priority in class covariance
foreach ($classReflection->getParents() as $parentClassReflection) {
if ($parentClassReflection->hasNativeMethod($methodName)) {
return $parentClassReflection->getNativeMethod($methodName);
}
}
foreach ($classReflection->getAncestors() as $ancestorClassReflection) {
if ($classReflection === $ancestorClassReflection) {
continue;
}
if (! $ancestorClassReflection->hasNativeMethod($methodName)) {
continue;
}
return $ancestorClassReflection->getNativeMethod($methodName);
}
return null;
}
}

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Reflection;
use PhpParser\Node;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\NodeFinder;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\Parser;
use PhpParser\ParserFactory;
use PHPStan\Reflection\ClassReflection;
use Throwable;
final class ReflectionParser
{
/**
* @var array<string, ClassLike>
*/
private array $classesByFilename = [];
private readonly Parser $parser;
public function __construct()
{
$parserFactory = new ParserFactory();
$this->parser = $parserFactory->createForNewestSupportedVersion();
}
public function parseClassReflection(ClassReflection $classReflection): ?ClassLike
{
$fileName = $classReflection->getFileName();
if ($fileName === null) {
return null;
}
return $this->parseFilenameToClass($fileName);
}
private function parseFilenameToClass(string $fileName): ClassLike|null
{
if (isset($this->classesByFilename[$fileName])) {
return $this->classesByFilename[$fileName];
}
try {
/** @var string $fileContents */
$fileContents = file_get_contents($fileName);
$stmts = $this->parser->parse($fileContents);
if (! is_array($stmts)) {
return null;
}
// complete namespacedName variables
$nodeTraverser = new NodeTraverser();
$nodeTraverser->addVisitor(new NameResolver());
$nodeTraverser->traverse($stmts);
} catch (Throwable) {
// not reachable
return null;
}
$classLike = $this->findFirstClassLike($stmts);
if (! $classLike instanceof ClassLike) {
return null;
}
$this->classesByFilename[$fileName] = $classLike;
return $classLike;
}
/**
* @param Node[] $nodes
*/
private function findFirstClassLike(array $nodes): ?ClassLike
{
$nodeFinder = new NodeFinder();
return $nodeFinder->findFirstInstanceOf($nodes, ClassLike::class);
}
}

View File

@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Rules;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use Rector\TypePerfect\Configuration;
use Rector\TypePerfect\NodeFinder\ClassMethodNodeFinder;
use Rector\TypePerfect\NodeFinder\MethodCallNodeFinder;
/**
* @see \Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\NarrowPrivateClassMethodParamTypeRuleTest
* @implements Rule<MethodCall>
*/
final readonly class NarrowPrivateClassMethodParamTypeRule implements Rule
{
public const string ERROR_MESSAGE = 'Parameter %d should use "%s" type as the only type passed to this method';
public function __construct(
private Configuration $configuration,
private MethodCallNodeFinder $methodCallNodeFinder,
private ClassMethodNodeFinder $classMethodNodeFinder
) {
}
/**
* @return class-string<Node>
*/
public function getNodeType(): string
{
return MethodCall::class;
}
/**
* @param MethodCall $node
* @return RuleError[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $this->configuration->isNarrowParamEnabled() || $node->isFirstClassCallable()) {
return [];
}
$args = $node->getArgs();
if ($args === []) {
return [];
}
if (! $node->var instanceof Variable) {
return [];
}
if (! is_string($node->var->name)) {
return [];
}
if ($node->var->name !== 'this') {
return [];
}
return $this->validateArgVsParamTypes($args, $node, $scope);
}
/**
* @param Arg[] $args
* @return RuleError[]
*/
private function validateArgVsParamTypes(array $args, MethodCall $methodCall, Scope $scope): array
{
$methodCallUses = $this->methodCallNodeFinder->findUsages($methodCall, $scope);
if (count($methodCallUses) > 1) {
return [];
}
$classMethod = $this->classMethodNodeFinder->findByMethodCall($methodCall, $scope);
if (! $classMethod instanceof ClassMethod) {
return [];
}
/** @var Param[] $params */
$params = $classMethod->getParams();
$errorMessages = [];
foreach ($args as $position => $arg) {
$param = $params[$position] ?? [];
if (! $param instanceof Param) {
continue;
}
if ($param->variadic) {
return [];
}
$paramRuleError = $this->validateParam($param, $position, $arg->value, $scope);
if (! $paramRuleError instanceof RuleError) {
continue;
}
// @todo test double failed type
$errorMessages[] = $paramRuleError;
}
return $errorMessages;
}
private function validateParam(Param $param, int $position, Expr $expr, Scope $scope): ?RuleError
{
$type = $param->type;
// @todo some static type mapper from php-parser to PHPStan?
if (! $type instanceof FullyQualified) {
return null;
}
$argType = $scope->getType($expr);
if ($argType instanceof MixedType) {
return null;
}
if ($argType instanceof TemplateType) {
return null;
}
// not solveable yet, work with PHP 8 code only
if ($argType instanceof UnionType) {
return null;
}
if ($argType instanceof IntersectionType || $argType instanceof GenericObjectType) {
return null;
}
$objectType = new ObjectType($type->toString());
if ($objectType->equals($argType)) {
return null;
}
$classReflection = $objectType->getClassReflection();
if ($classReflection instanceof ClassReflection && $classReflection->isAbstract()) {
return null;
}
// handle weird type substration cases
$paramTypeAsString = $objectType->describe(VerbosityLevel::typeOnly());
$argTypeAsString = $argType->describe(VerbosityLevel::typeOnly());
if ($paramTypeAsString === $argTypeAsString) {
return null;
}
$errorMessage = sprintf(self::ERROR_MESSAGE, $position + 1, $argTypeAsString);
return RuleErrorBuilder::message($errorMessage)
->identifier('typePerfect.narrowPrivateClassMethodParamType')
->line($param->getLine())
->build();
}
}

View File

@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Rules;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\CollectedDataNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use Rector\TypePerfect\Collector\ClassMethod\PublicClassMethodParamTypesCollector;
use Rector\TypePerfect\Collector\MethodCall\MethodCallArgTypesCollector;
use Rector\TypePerfect\Collector\MethodCallableNode\MethodCallableCollector;
use Rector\TypePerfect\Configuration;
use Rector\TypePerfect\Enum\Types\ResolvedTypes;
/**
* @see \Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\NarrowPublicClassMethodParamTypeRuleTest
*
* @implements Rule<CollectedDataNode>
*/
final readonly class NarrowPublicClassMethodParamTypeRule implements Rule
{
public const string ERROR_MESSAGE = 'Parameters should have "%s" types as the only types passed to this method';
public function __construct(
private Configuration $configuration
) {
}
/**
* @return class-string<Node>
*/
public function getNodeType(): string
{
return CollectedDataNode::class;
}
/**
* @param CollectedDataNode $node
* @return RuleError[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $this->configuration->isNarrowParamEnabled()) {
return [];
}
$publicClassMethodCollector = $node->get(PublicClassMethodParamTypesCollector::class);
$classMethodReferenceToArgTypes = $this->resolveClassMethodReferenceToArgTypes($node);
$ruleErrors = [];
foreach ($publicClassMethodCollector as $filePath => $declarations) {
foreach ($declarations as [$className, $methodName, $paramTypesString, $line]) {
$uniqueCollectedArgTypesString = $this->resolveUniqueArgTypesString(
$classMethodReferenceToArgTypes,
$className,
$methodName
);
if ($uniqueCollectedArgTypesString === null) {
continue;
}
if ($paramTypesString === $uniqueCollectedArgTypesString) {
continue;
}
$ruleErrors[] = RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $uniqueCollectedArgTypesString))
->identifier('typePerfect.narrowPublicClassMethodParamType')
->file($filePath)
->line($line)
->build();
}
}
return $ruleErrors;
}
/**
* @return array<string, string[]>
*/
private function resolveClassMethodReferenceToArgTypes(CollectedDataNode $collectedDataNode): array
{
$methodCallArgTypesByFilePath = $collectedDataNode->get(MethodCallArgTypesCollector::class);
// these should be skipped completely, as we have no idea what is being passed there
$methodCallablesByFilePath = $collectedDataNode->get(MethodCallableCollector::class);
$methodFirstClassCallables = $this->flattenCollectedData($methodCallablesByFilePath);
// group call references and types
$classMethodReferenceToTypes = [];
foreach ($methodCallArgTypesByFilePath as $methodCallArgTypes) {
foreach ($methodCallArgTypes as [$classMethodReference, $argTypesString]) {
// skip it
if (in_array($classMethodReference, $methodFirstClassCallables, true)) {
continue;
}
$classMethodReferenceToTypes[$classMethodReference][] = $argTypesString;
}
}
// resolve unique values
foreach ($classMethodReferenceToTypes as $key => $value) {
$classMethodReferenceToTypes[$key] = array_unique($value);
}
return $classMethodReferenceToTypes;
}
/**
* @param array<string, string[]> $classMethodReferenceToArgTypes
*/
private function resolveUniqueArgTypesString(
array $classMethodReferenceToArgTypes,
string $className,
string $methodName
): ?string {
$currentClassMethodReference = $className . '::' . $methodName;
$collectedArgTypes = $classMethodReferenceToArgTypes[$currentClassMethodReference] ?? null;
if ($collectedArgTypes === null) {
return null;
}
// we need exactly one type
if (count($collectedArgTypes) !== 1) {
return null;
}
// one of the arg types could not be resolved, we're not sure
if (in_array(ResolvedTypes::UNKNOWN_TYPES, $collectedArgTypes, true)) {
return null;
}
return $collectedArgTypes[0];
}
/**
* @param array<string, list<(array<string>|null)>> $methodCallablesByFilePath
* @return string[]
*/
private function flattenCollectedData(array $methodCallablesByFilePath): array
{
$methodFirstClassCallables = [];
foreach ($methodCallablesByFilePath as $methodCallables) {
foreach ($methodCallables as $methodCallable) {
// the collector returns null for nodes it cannot resolve
if (! isset($methodCallable[0])) {
continue;
}
$methodFirstClassCallables[] = $methodCallable[0];
}
}
return $methodFirstClassCallables;
}
}

View File

@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Rules;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use Rector\TypePerfect\Configuration;
use Rector\TypePerfect\NodeFinder\ReturnNodeFinder;
use Rector\TypePerfect\Reflection\MethodNodeAnalyser;
/**
* @see \Rector\TypePerfect\Tests\Rules\NarrowReturnObjectTypeRule\NarrowReturnObjectTypeRuleTest
*
* @implements Rule<ClassMethod>
*/
final readonly class NarrowReturnObjectTypeRule implements Rule
{
public const string ERROR_MESSAGE = 'Provide more specific return type "%s" over abstract one';
public function __construct(
private ReturnNodeFinder $returnNodeFinder,
private MethodNodeAnalyser $methodNodeAnalyser,
private Configuration $configuration
) {
}
/**
* @return class-string<Node>
*/
public function getNodeType(): string
{
return ClassMethod::class;
}
/**
* @param ClassMethod $node
* @return RuleError[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $this->configuration->isNarrowReturnEnabled()) {
return [];
}
if (! $node->returnType instanceof FullyQualified) {
return [];
}
if ($this->shouldSkipScope($scope)) {
return [];
}
$returnObjectType = new ObjectType($node->returnType->toString());
if ($this->shouldSkipReturnObjectType($returnObjectType)) {
return [];
}
$methodName = $node->name->toString();
if ($this->methodNodeAnalyser->hasParentVendorLock($scope, $methodName)) {
return [];
}
$returnExpr = $this->returnNodeFinder->findOnlyReturnsExpr($node);
if (! $returnExpr instanceof Expr) {
return [];
}
$returnExprType = $scope->getType($returnExpr);
if ($this->shouldSkipReturnExprType($returnExprType)) {
return [];
}
if ($returnObjectType->equals($returnExprType)) {
return [];
}
// is subtype?
if (! $returnObjectType->isSuperTypeOf($returnExprType)->yes()) {
return [];
}
if (count($returnExprType->getObjectClassNames()) !== 1) {
return [];
}
$errorMessage = sprintf(self::ERROR_MESSAGE, $returnExprType->getObjectClassNames()[0]);
return [
RuleErrorBuilder::message($errorMessage)
->identifier('typePerfect.narrowReturnObjectType')
->build(),
];
}
private function shouldSkipReturnObjectType(ObjectType $objectType): bool
{
$classReflection = $objectType->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return true;
}
// cannot be more precise if final class
return $classReflection->isFinal();
}
private function shouldSkipScope(Scope $scope): bool
{
$classReflection = $scope->getClassReflection();
if (! $classReflection instanceof ClassReflection) {
return true;
}
return ! $classReflection->isClass();
}
private function shouldSkipReturnExprType(Type $type): bool
{
if (count($type->getObjectClassNames()) !== 1) {
return true;
}
if (count($type->getObjectClassReflections()) !== 1) {
return true;
}
return $type->getObjectClassReflections()[0]
->isAnonymous();
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Rules;
use PhpParser\Node;
use PhpParser\Node\Expr\ArrayDimFetch;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ObjectType;
use Rector\TypePerfect\Configuration;
/**
* @see \Rector\TypePerfect\Tests\Rules\NoArrayAccessOnObjectRule\NoArrayAccessOnObjectRuleTest
* @implements Rule<ArrayDimFetch>
*/
final readonly class NoArrayAccessOnObjectRule implements Rule
{
public const string ERROR_MESSAGE = 'Use explicit methods over array access on object';
/**
* @var string[]
*/
private const array ALLOWED_CLASSES = [
'SplFixedArray',
'SimpleXMLElement',
'Iterator',
'WeakMap',
'Aws\ResultInterface',
'Symfony\Component\DomCrawler\AbstractUriElement',
'Symfony\Component\Form\FormInterface',
'Symfony\Component\OptionsResolver\Options',
];
public function __construct(
private Configuration $configuration,
) {
}
/**
* @return class-string<Node>
*/
public function getNodeType(): string
{
return ArrayDimFetch::class;
}
/**
* @param ArrayDimFetch $node
* @return RuleError[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $this->configuration->isNoArrayAccessOnObjectEnabled()) {
return [];
}
$varType = $scope->getType($node->var);
if (! $varType instanceof ObjectType) {
return [];
}
if ($this->isAllowedObjectType($varType)) {
return [];
}
return [
RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier('typePerfect.noArrayAccessOnObject')
->build(),
];
}
private function isAllowedObjectType(ObjectType $objectType): bool
{
return array_any(self::ALLOWED_CLASSES, fn (string $allowedClass): bool => $objectType->isInstanceOf($allowedClass)->yes());
}
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Rules;
use PhpParser\Node;
use PhpParser\Node\Expr\Empty_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use Rector\TypePerfect\Configuration;
use Rector\TypePerfect\Guard\EmptyIssetGuard;
/**
* @implements Rule<Empty_>
*/
final readonly class NoEmptyOnObjectRule implements Rule
{
public const string ERROR_MESSAGE = 'Use instanceof instead of empty() on object';
public function __construct(
private EmptyIssetGuard $emptyIssetGuard,
private Configuration $configuration,
) {
}
public function getNodeType(): string
{
return Empty_::class;
}
/**
* @param Empty_ $node
* @return RuleError[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $this->configuration->isNoEmptyOnObjectEnabled()) {
return [];
}
if ($this->emptyIssetGuard->isLegal($node->expr, $scope)) {
return [];
}
return [
RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier('typePerfect.noEmptyOnObject')
->build(),
];
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Rules;
use PhpParser\Node;
use PhpParser\Node\Expr\Isset_;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use Rector\TypePerfect\Configuration;
use Rector\TypePerfect\Guard\EmptyIssetGuard;
/**
* @see \Rector\TypePerfect\Tests\Rules\NoIssetOnObjectRule\NoIssetOnObjectRuleTest
* @implements Rule<Isset_>
*/
final readonly class NoIssetOnObjectRule implements Rule
{
public const string ERROR_MESSAGE = 'Use instanceof instead of isset() on object';
public function __construct(
private EmptyIssetGuard $emptyIssetGuard,
private Configuration $configuration,
) {
}
public function getNodeType(): string
{
return Isset_::class;
}
/**
* @param Isset_ $node
*
* @return RuleError[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $this->configuration->isNoIssetOnObjectEnabled()) {
return [];
}
foreach ($node->vars as $var) {
if ($this->emptyIssetGuard->isLegal($var, $scope)) {
continue;
}
return [
RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier('typePerfect.noIssetOnObject')
->build(),
];
}
return [];
}
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Rules;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Node\Printer\Printer;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ErrorType;
use PHPStan\Type\MixedType;
use Rector\TypePerfect\Configuration;
/**
* @see \Rector\TypePerfect\Tests\Rules\NoMixedMethodCallerRule\NoMixedMethodCallerRuleTest
* @implements Rule<MethodCall>
*/
final readonly class NoMixedMethodCallerRule implements Rule
{
public const string ERROR_MESSAGE = 'Mixed variable in a `%s->...()` can skip important errors. Make sure the type is known';
public function __construct(
private Printer $printer,
private Configuration $configuration,
) {
}
/**
* @return class-string<Node>
*/
public function getNodeType(): string
{
return MethodCall::class;
}
/**
* @param MethodCall $node
* @return RuleError[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $this->configuration->isNoMixedCallerEnabled()) {
return [];
}
$callerType = $scope->getType($node->var);
if (! $callerType instanceof MixedType) {
return [];
}
if ($callerType instanceof ErrorType) {
return [];
}
// if error, skip as well for false positive
if ($this->isPreviousTypeErrorType($node, $scope)) {
return [];
}
$printedMethodCall = $this->printer->prettyPrintExpr($node->var);
return [
RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $printedMethodCall))
->identifier('typePerfect.noMixedMethodCaller')
->build(),
];
}
private function isPreviousTypeErrorType(MethodCall $methodCall, Scope $scope): bool
{
$currentMethodCall = $methodCall;
while ($currentMethodCall->var instanceof MethodCall) {
$previousType = $scope->getType($currentMethodCall->var);
if ($previousType instanceof ErrorType) {
return true;
}
$currentMethodCall = $currentMethodCall->var;
}
return false;
}
}

View File

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Rules;
use PhpParser\Node;
use PhpParser\Node\Expr\PropertyFetch;
use PHPStan\Analyser\Scope;
use PHPStan\Node\Printer\Printer;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\MixedType;
use Rector\TypePerfect\Configuration;
/**
* @see \Rector\TypePerfect\Tests\Rules\NoMixedPropertyFetcherRule\NoMixedPropertyFetcherRuleTest
* @implements Rule<PropertyFetch>
*/
final readonly class NoMixedPropertyFetcherRule implements Rule
{
public const string ERROR_MESSAGE = 'Mixed property fetch in a "%s->..." can skip important errors. Make sure the type is known';
public function __construct(
private Printer $printer,
private Configuration $configuration,
) {
}
/**
* @return class-string<Node>
*/
public function getNodeType(): string
{
return PropertyFetch::class;
}
/**
* @param PropertyFetch $node
* @return RuleError[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $this->configuration->isNoMixedPropertyEnabled()) {
return [];
}
$callerType = $scope->getType($node->var);
if (! $callerType instanceof MixedType) {
return [];
}
$printedVar = $this->printer->prettyPrintExpr($node->var);
return [
RuleErrorBuilder::message(sprintf(self::ERROR_MESSAGE, $printedVar))
->identifier('typePerfect.noMixedPropertyFetcher')
->build(),
];
}
}

View File

@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Rules;
use PhpParser\Node;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\Php\PhpMethodReflection;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use Rector\TypePerfect\Configuration;
use Rector\TypePerfect\Reflection\MethodNodeAnalyser;
/**
* @see \Rector\TypePerfect\Tests\Rules\ForbiddenParamTypeRemovalRule\ForbiddenParamTypeRemovalRuleTest
* @implements Rule<ClassMethod>
*/
final readonly class NoParamTypeRemovalRule implements Rule
{
public const string ERROR_MESSAGE = 'Removing parent param type is forbidden';
public function __construct(
private MethodNodeAnalyser $methodNodeAnalyser,
private Configuration $configuration,
) {
}
/**
* @return class-string<Node>
*/
public function getNodeType(): string
{
return ClassMethod::class;
}
/**
* @param ClassMethod $node
* @return RuleError[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $this->configuration->isNoParamTypeRemovalEnabled()) {
return [];
}
if ($node->params === []) {
return [];
}
$classMethodName = (string) $node->name;
if ($classMethodName === '__construct') {
return [];
}
$parentClassMethodReflection = $this->methodNodeAnalyser->matchFirstParentClassMethod($scope, $classMethodName);
if (! $parentClassMethodReflection instanceof PhpMethodReflection) {
return [];
}
foreach ($node->params as $paramPosition => $param) {
if ($param->type !== null) {
continue;
}
$parentParamType = $this->resolveParentParamType($parentClassMethodReflection, $paramPosition);
if ($parentParamType instanceof MixedType) {
continue;
}
// removed param type!
return [
RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier('typePerfect.noParamTypeRemoval')
->build(),
];
}
return [];
}
private function resolveParentParamType(PhpMethodReflection $phpMethodReflection, int $paramPosition): Type
{
foreach ($phpMethodReflection->getVariants() as $extendedParametersAcceptor) {
foreach ($extendedParametersAcceptor->getParameters() as $parentParamPosition => $parameterReflectionWithPhpDoc) {
if ($paramPosition !== $parentParamPosition) {
continue;
}
return $parameterReflectionWithPhpDoc->getNativeType();
}
}
return new MixedType();
}
}

View File

@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Rules;
use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Return_;
use PhpParser\NodeFinder;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;
use Rector\TypePerfect\Configuration;
/**
* @implements Rule<ClassMethod>
*/
final readonly class ReturnNullOverFalseRule implements Rule
{
/**
* @api
*/
public const string ERROR_MESSAGE = 'Returning false in non return bool class method. Use null with type|null instead or add bool return type';
private NodeFinder $nodeFinder;
public function __construct(
private Configuration $configuration,
) {
$this->nodeFinder = new NodeFinder();
}
/**
* @return class-string<Node>
*/
public function getNodeType(): string
{
return ClassMethod::class;
}
/**
* @param ClassMethod $node
* @return RuleError[]
*/
public function processNode(Node $node, Scope $scope): array
{
if (! $this->configuration->isNoFalsyReturnEnabled()) {
return [];
}
if ($node->stmts === null) {
return [];
}
if ($node->returnType instanceof Node) {
return [];
}
/** @var Return_[] $returns */
$returns = $this->nodeFinder->findInstanceOf($node->stmts, Return_::class);
$hasFalseType = false;
$hasTrueType = false;
foreach ($returns as $return) {
if (! $return->expr instanceof Expr) {
continue;
}
$exprType = $scope->getType($return->expr);
if (! $exprType instanceof ConstantBooleanType) {
if ($exprType->isBoolean()->yes()) {
return [];
}
continue;
}
if ($exprType->getValue()) {
$hasTrueType = true;
continue;
}
$hasFalseType = true;
}
if (! $hasTrueType && $hasFalseType) {
return [
RuleErrorBuilder::message(self::ERROR_MESSAGE)
->identifier('typePerfect.nullOverFalse')
->build(),
];
}
return [];
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\ValueObject;
final readonly class MethodCallReference
{
public function __construct(
private string $class,
private string $method
) {
}
public function getClass(): string
{
return $this->class;
}
public function getMethod(): string
{
return $this->method;
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Source\AnotherClassWithRun;
use Rector\TypePerfect\Tests\Rules\PreferredClassRule\Fixture\SomeStaticCall;
class DifferentClassSameMethodCallName
{
/**
* @param SomeStaticCall|MethodCall $node
*/
public function process(AnotherClassWithRun $anotherClassWithRun)
{
$anotherClassWithRun->run($anotherClassWithRun);
}
/**
* @param SomeStaticCall|MethodCall $node
*/
private function run(Node $node)
{
if ($node->name instanceof MethodCall) {
$this->run($node->name);
}
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
class DoubleShot
{
public function run(Node\Arg $arg, Node\Param $param)
{
$this->isCheck($arg, $param);
}
private function isCheck(\PhpParser\Node $arg, \PhpParser\Node $param)
{
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
class Fixture
{
public function run(Node $node)
{
if ($node instanceof MethodCall) {
$this->isCheck($node);
}
}
private function isCheck(Node $node)
{
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace SkipAbstractBase;
use Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Source\ConceptBase;
use Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Source\ConceptImpl1;
class MyService {
public function run(ConceptImpl1 $arg)
{
$this->isCheck($arg);
}
private function isCheck(ConceptBase $arg)
{
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
class SkipAlreadyCorrectType
{
public function run(Node $node)
{
if ($node instanceof MethodCall) {
$this->isCheck($node);
}
}
private function isCheck(MethodCall $node)
{
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Stmt\Return_;
final class SkipCorrectUnionType
{
public function run(\PhpParser\Node $node)
{
if (! $node instanceof Assign && ! $node instanceof Return_) {
return [];
}
if (! $this->isIncludeOnceOrRequireOnce($node)) {
return [];
}
return [];
}
/**
* @param Assign|Return_ $node
*/
private function isIncludeOnceOrRequireOnce(\PhpParser\Node $node)
{
return true;
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PHPStan\Node\FileNode;
use Rector\Core\PhpParser\Node\CustomNode\FileWithoutNamespace;
final class SkipDuplicatedCallOfSameMethodWithComment2
{
public function firstMethod()
{
$this->printFile(new FileWithoutNamespace([]));
}
private function printFile(\PhpParser\Node $node)
{
}
public function secondMethod()
{
// some comment
$this->printFile(new FileNode([]));
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
/**
* @template T of MethodCall
*/
class SkipGenericType
{
/**
* @param T $node
* @return void
*/
public function run(Node $node)
{
$this->getsAGeneric($node);
}
/**
* @param T $node
* @return void
*/
private function getsAGeneric(Node $node)
{
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
class SkipMayOverrideArg
{
public function run(Node $node)
{
if ($node instanceof MethodCall) {
// on this part, $node may be overriden
$node = $this->mayOverrideNode();
$this->isCheck($node);
}
}
private function isCheck(Node $node)
{
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
class SkipMixed
{
public function run($arg)
{
$this->isCheck($arg);
}
private function isCheck(\PhpParser\Node $arg)
{
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\PropertyFetch;
class SkipMultipleUsed
{
public function run(Node $node)
{
if ($node instanceof MethodCall) {
$this->isCheck($node);
}
if ($node instanceof PropertyFetch) {
$this->isCheck($node);
}
}
private function isCheck(Node $node)
{
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
final class SkipNoArgs
{
public function run()
{
$this->execute();
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
final class SkipNotFromThis
{
public function run()
{
$obj = new \DateTime('now');
$obj->format('Y-m-d');
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
class SkipNotPrivate
{
public function run(Node $node)
{
if ($node instanceof MethodCall) {
$this->isCheckNotPrivate($node);
}
}
public function isCheckNotPrivate(Node $node)
{
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PHPStan\Type\MixedType;
class SkipOptedOut
{
public function run(\PHPStan\Type\Type $type)
{
if ($type instanceof MixedType) {
return;
}
$this->isCheck($type);
}
private function isCheck(\PHPStan\Type\Type $type)
{
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
final class SkipParentNotIf
{
public function run(Node $node)
{
$this->execute($node);
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use Rector\TypePerfect\Tests\Rules\PreferredClassRule\Fixture\SomeStaticCall;
class SkipRecursive
{
/**
* @param SomeStaticCall|MethodCall $node
*/
public function processNode(Node $node): void
{
$this->run($node);
}
/**
* @param SomeStaticCall|MethodCall $node
*/
private function run(Node $node): void
{
if ($node->name instanceof MethodCall) {
$this->run($node->name);
}
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Source\MyIterator;
use Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Source\MyPreise;
final class SkipSameGeneric
{
protected function getPriceAndCalculateTableVM(): void
{
$prices = $this->getPricesForArticle();
$this->getAmounts($prices);
}
/**
* @param MyIterator<MyPreise> $prices
*/
private function getAmounts(MyIterator $prices): void
{
}
/**
* @return MyIterator<MyPreise>
*/
private function getPricesForArticle(): MyIterator
{
/** @var MyIterator<MyPreise> */
return new MyIterator();
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
class SkipVariadics
{
public function run(Node $node, Node $node2, Node $node3)
{
$arr = [$node, $node2, $node3];
$this->takesVariadic(...$arr);
}
private function takesVariadic(Node ...$node)
{
}
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule;
use Iterator;
use Override;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Param;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\TypePerfect\Rules\NarrowPrivateClassMethodParamTypeRule;
final class NarrowPrivateClassMethodParamTypeRuleTest extends RuleTestCase
{
/**
* @param list<array{0: string, 1: int, 2?: string|null}> $expectedErrorsWithLines
*/
#[DataProvider('provideData')]
public function testRule(string $filePath, array $expectedErrorsWithLines): void
{
$this->analyse([$filePath], $expectedErrorsWithLines);
}
/**
* @return Iterator<array<array<int, mixed>, mixed>>
*/
public static function provideData(): Iterator
{
yield [__DIR__ . '/Fixture/SkipDuplicatedCallOfSameMethodWithComment.php', []];
yield [__DIR__ . '/Fixture/SkipCorrectUnionType.php', []];
yield [__DIR__ . '/Fixture/SkipRecursive.php', []];
yield [__DIR__ . '/Fixture/SkipMixed.php', []];
yield [__DIR__ . '/Fixture/SkipOptedOut.php', []];
yield [__DIR__ . '/Fixture/SkipNotFromThis.php', []];
yield [__DIR__ . '/Fixture/SkipParentNotIf.php', []];
yield [__DIR__ . '/Fixture/SkipNoArgs.php', []];
yield [__DIR__ . '/Fixture/SkipAlreadyCorrectType.php', []];
yield [__DIR__ . '/Fixture/SkipMayOverrideArg.php', []];
yield [__DIR__ . '/Fixture/SkipMultipleUsed.php', []];
yield [__DIR__ . '/Fixture/SkipNotPrivate.php', []];
$errorMessage = sprintf(NarrowPrivateClassMethodParamTypeRule::ERROR_MESSAGE, 1, MethodCall::class);
yield [__DIR__ . '/Fixture/Fixture.php', [[$errorMessage, 19]]];
yield [__DIR__ . '/Fixture/DifferentClassSameMethodCallName.php', [[$errorMessage, 25]]];
$argErrorMessage = sprintf(NarrowPrivateClassMethodParamTypeRule::ERROR_MESSAGE, 1, Arg::class);
$paramErrorMessage = sprintf(NarrowPrivateClassMethodParamTypeRule::ERROR_MESSAGE, 2, Param::class);
yield [__DIR__ . '/Fixture/DoubleShot.php', [[$argErrorMessage, 15], [$paramErrorMessage, 15]]];
yield [__DIR__ . '/Fixture/SkipGenericType.php', []];
yield [__DIR__ . '/Fixture/SkipAbstractBase.php', []];
yield [__DIR__ . '/Fixture/SkipVariadics.php', []];
yield [__DIR__ . '/Fixture/SkipSameGeneric.php', []];
}
/**
* @return string[]
*/
#[Override]
public static function getAdditionalConfigFiles(): array
{
return [__DIR__ . '/../../../config/extension.neon'];
}
protected function getRule(): Rule
{
return self::getContainer()->getByType(NarrowPrivateClassMethodParamTypeRule::class);
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Source;
use PhpParser\Node;
final class AnotherClassWithRun
{
public function run(Node $node)
{
return $node;
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Source;
abstract class ConceptBase
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Source;
class ConceptImpl1 extends ConceptBase
{
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Source;
/**
* @template T
*/
final class MyIterator
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Source;
class MyPreise
{
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
final class ExplicitlyNullableParams
{
public function test(
?bool $bool = true,
): bool
{
return $bool;
}
static public function run(): void
{
$skipDateTimeMix = new ExplicitlyNullableParams();
$skipDateTimeMix->test(false);
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\FirstClassCallables;
final class CallVariadics
{
public function callMe(SomeCalledMethod $someCalledMethod)
{
$closure = $someCalledMethod->callMe(...);
$someCalledMethod->callMe(1000);
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\FirstClassCallables;
final class SomeCalledMethod
{
public function callMe($number)
{
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\Generics;
/**
* @template T
*/
class GenericA
{
}
class User
{
/** @param GenericA<string> $g */
public function doFoo(GenericA $g):void
{
}
/** @param GenericA<string> $g */
function doBar($g):void {
$this->doFoo($g);
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\Generics;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\SkipSameGeneric\MyGroup;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\SkipSameGeneric\MyIterator;
final class SkipSameGeneric
{
/**
* @param MyIterator<MyGroup> $_werbemasse
*/
public function setWerbemasse($_werbemasse): void
{
$this->_werbemasse = $_werbemasse;
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
class HandleDefaultValue {
public function takesUnionWithDifferentDefaultValue(
int|string $value = "hallo"
): bool
{
return true;
}
public function takesUnionWithSameTypeDefaultValue(
int|string $value = 123
): bool
{
return true;
}
static public function run(): void
{
$o = new HandleDefaultValue();
$o->takesUnionWithDifferentDefaultValue(1);
$o = new HandleDefaultValue();
$o->takesUnionWithSameTypeDefaultValue(1);
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
final class PublicDoubleShot
{
public function callMeTwice($number)
{
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
final class SkipApiMarked
{
/**
* @api
*/
public function callNode(Node $node)
{
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
final class SkipCallable
{
public function map(callable $mapper): array
{
return [];
}
public function run()
{
$this->map(function () {});
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
final class SkipClassStringPassed
{
public function resolve(string $className)
{
}
}

View File

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
final class SkipClosure
{
public function map(\Closure $mapper): array
{
return [];
}
public function run()
{
$this->map(function () {});
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
final class SkipDateTimeMix
{
public function markDate(
\DateTimeInterface $date
): bool
{
return true;
}
static public function run(): void
{
$skipDateTimeMix = new SkipDateTimeMix();
$skipDateTimeMix->markDate(
new \DateTimeImmutable('15:30')
);
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
final class SkipDefault
{
public function dontSetParamValue(
string $Id = ''
): bool
{
return true;
}
static public function run(): void
{
$skipDefault = new SkipDefault();
$skipDefault->dontSetParamValue();
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\PickReasonEnum;
class SkipEnum
{
public function pick(PickReasonEnum $pickReasonEnum): void
{
}
public function run()
{
$this->pick(PickReasonEnum::BarcodeIssue);
}
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
final class SkipEqualUnionType
{
public function run(MethodCall|StaticCall $obj)
{
}
public function runTernary(StaticCall|MethodCall $obj)
{
return $obj instanceof StaticCall
? $obj->class
: $obj->var;
}
public function runTernaryFlipped(StaticCall|MethodCall $obj)
{
return $obj instanceof MethodCall
? $obj->var
: $obj->class;
}
/**
* @param Node[]|Node $node
*/
public function runArrayTyped(array | Node $node)
{
}
/**
* @param array<Node>|Node $node
*/
public function runArrayTyped2(array | Node $node)
{
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\PassMeAsType;
final class SkipExpectedClassType
{
public function callMeWithClassType(PassMeAsType $passMeAsType)
{
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
final class SkipMixedAndString
{
public function resolve($value)
{
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
final class SkipNonPublicClassMethod
{
public function personInTree()
{
$this->callMe(1000);
}
private function callMe($number)
{
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
use PhpParser\Node;
final class SkipNullableCompare
{
public function callNode(?Node $node)
{
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
final class SkipProperlyFilledParamType
{
public function callMeTwice(int $number)
{
}
}

View File

@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
final class SkipResource
{
/**
* @param resource $resource
*/
public function map($resource, string $name): void
{
}
public function run()
{
$resource = @fsockopen('100', 100);
$this->map($resource, 'name');
$resource = $this->getFileResource();
$this->map($resource, 'surname');
}
/**
* @return resource
*/
private function getFileResource()
{
return fopen('file.txt', 'r');
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
class SkipSelf {
public function takesSelf(self $code): bool
{
return true;
}
static public function run(SkipSelf $self): void
{
$self->takesSelf(new SkipSelf());
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedThisType\CallByThis;
final class SkipThisPassedExactType
{
public function run(CallByThis $obj)
{
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
use DateTime;
use stdClass;
final class SkipUsedInternallyForSecondType
{
public function run(stdClass|DateTime $obj)
{
}
public function execute()
{
$this->run(new DateTime('now'));
}
}

View File

@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\SomeInterface;
final class ThisPassedFromInterface
{
public function run(SomeInterface $obj)
{
}
}

View File

@ -0,0 +1,202 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule;
use Iterator;
use Override;
use PHPStan\Collectors\Collector;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\TypePerfect\Rules\NarrowPublicClassMethodParamTypeRule;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedThisType\CallByThisFromInterface;
final class NarrowPublicClassMethodParamTypeRuleTest extends RuleTestCase
{
/**
* @param string[] $filePaths
* @param list<array{0: string, 1: int, 2?: string|null}> $expectedErrorsWithLines
*/
#[DataProvider('provideData')]
public function testRule(array $filePaths, array $expectedErrorsWithLines): void
{
$this->analyse($filePaths, $expectedErrorsWithLines);
}
/**
* @return Iterator<array<array<int, array<int, mixed>>, mixed>>
*/
public static function provideData(): Iterator
{
yield [[__DIR__ . '/Fixture/Generics/SkipPassedGenerics.php'], []];
yield [[__DIR__ . '/Fixture/SkipDefault.php'], []];
yield [[__DIR__ . '/Fixture/SkipResource.php'], []];
yield [[__DIR__ . '/Fixture/SkipDateTimeMix.php'], []];
yield [[__DIR__ . '/Fixture/SkipNonPublicClassMethod.php'], []];
// skip first class callables as anything can be passed there
yield [[
__DIR__ . '/Fixture/FirstClassCallables/CallVariadics.php',
__DIR__ . '/Fixture/FirstClassCallables/SomeCalledMethod.php',
], []];
// skip expected scalar type
yield [[
__DIR__ . '/Fixture/SkipProperlyFilledParamType.php',
__DIR__ . '/Source/ExpectedType/FirstTypedCaller.php',
__DIR__ . '/Source/ExpectedType/SecondTypedCaller.php',
], []];
// skip expected object type
yield [[
__DIR__ . '/Fixture/SkipExpectedClassType.php',
__DIR__ . '/Source/ExpectedClassType/FirstClassTypedCaller.php',
__DIR__ . '/Source/ExpectedClassType/SecondClassTypedCaller.php',
], []];
// skip class-string
yield [[
__DIR__ . '/Fixture/SkipClassStringPassed.php',
__DIR__ . '/Source/ExpectedClassString/FirstTypedCaller.php',
__DIR__ . '/Source/ExpectedClassString/SecondTypedCaller.php',
], []];
// skip everything in case of values is mixed
yield [[
__DIR__ . '/Fixture/SkipMixedAndString.php',
__DIR__ . '/Source/MixedAndString/FirstCaller.php',
__DIR__ . '/Source/MixedAndString/SecondCaller.php',
], []];
// skip int + string values
yield [[
__DIR__ . '/Fixture/SkipMixedAndString.php',
__DIR__ . '/Source/MixedAndString/FirstCaller.php',
__DIR__ . '/Source/MixedAndString/ThirdCaller.php',
], []];
// skip nullable compare
yield [[
__DIR__ . '/Fixture/SkipNullableCompare.php',
__DIR__ . '/Source/NullableParam/FirstNullable.php',
__DIR__ . '/Source/NullableParam/SecondNullable.php',
], []];
// skip api
yield [[
__DIR__ . '/Fixture/SkipApiMarked.php',
__DIR__ . '/Source/ExpectedNodeApi/CallWithProperty.php',
], []];
// skip equal union type
yield [[
__DIR__ . '/Fixture/SkipEqualUnionType.php',
__DIR__ . '/Source/ExpectedUnion/CallUnionType.php',
], []];
// skip equal union type flipped
yield [[
__DIR__ . '/Fixture/SkipEqualUnionType.php',
__DIR__ . '/Source/ExpectedUnion/CallUnionTypeFlipped.php',
], []];
// skip equal union type ternary if else
yield [[
__DIR__ . '/Fixture/SkipEqualUnionType.php',
__DIR__ . '/Source/ExpectedUnion/CallUnionTypeTernaryIfElse.php',
], []];
// skip equal union type ternary if else flipped
yield [[
__DIR__ . '/Fixture/SkipEqualUnionType.php',
__DIR__ . '/Source/ExpectedUnion/CallUnionTypeTernaryIfElseFlipped.php',
], []];
// skip equal union type array typed
yield [[
__DIR__ . '/Fixture/SkipEqualUnionType.php',
__DIR__ . '/Source/ExpectedUnion/CallUnionArrayType.php',
], []];
// skip this passed exact type
yield [[
__DIR__ . '/Fixture/SkipThisPassedExactType.php',
__DIR__ . '/Source/ExpectedThisType/CallByThis.php',
], []];
// skip used internally for second type
yield [[
__DIR__ . '/Fixture/SkipUsedInternallyForSecondType.php',
__DIR__ . '/Source/ExpectedType/OnlyFirstTypeCalledOutside.php',
], []];
$argErrorMessage = sprintf(NarrowPublicClassMethodParamTypeRule::ERROR_MESSAGE, 'int');
yield [[
__DIR__ . '/Fixture/PublicDoubleShot.php',
__DIR__ . '/Source/FirstCaller.php',
__DIR__ . '/Source/SecondCaller.php',
], [[$argErrorMessage, 9]]];
// this passed from interface
$argErrorMessage = sprintf(
NarrowPublicClassMethodParamTypeRule::ERROR_MESSAGE,
CallByThisFromInterface::class
);
yield [[
__DIR__ . '/Fixture/ThisPassedFromInterface.php',
__DIR__ . '/Source/ExpectedThisType/CallByThisFromInterface.php',
], [[$argErrorMessage, 11]]];
yield [[__DIR__ . '/Fixture/SkipSelf.php'], []];
yield [[__DIR__ . '/Fixture/SkipClosure.php'], []];
yield [[__DIR__ . '/Fixture/SkipCallable.php'], []];
yield [[__DIR__ . '/Fixture/SkipEnum.php'], []];
$argErrorMessage = sprintf(NarrowPublicClassMethodParamTypeRule::ERROR_MESSAGE, 'int');
yield [[__DIR__ . '/Fixture/HandleDefaultValue.php'], [[$argErrorMessage, 15]]];
$argErrorMessage = sprintf(NarrowPublicClassMethodParamTypeRule::ERROR_MESSAGE, 'bool');
yield [[__DIR__ . '/Fixture/ExplicitlyNullableParams.php'], [[$argErrorMessage, 9]]];
// skip generics
yield [[
__DIR__ . '/Fixture/Generics/SkipSameGeneric.php',
__DIR__ . '/Source/SkipSameGeneric/MyGroup.php',
__DIR__ . '/Source/SkipSameGeneric/MyIterator.php',
__DIR__ . '/Source/SkipSameGeneric/MyService.php',
], []];
}
/**
* @return string[]
*/
#[Override]
public static function getAdditionalConfigFiles(): array
{
return [__DIR__ . '/../../../config/extension.neon'];
}
protected function getRule(): Rule
{
return self::getContainer()->getByType(NarrowPublicClassMethodParamTypeRule::class);
}
/**
* Warning, just spent hour looking for why the test does not run :D This should be implicit part of the parent
* class.
*
* @return Collector[]
*/
#[Override]
protected function getCollectors(): array
{
return self::getContainer()->getServicesByTag('phpstan.collector');
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedClassString;
use PHPStan\Reflection\ClassReflection;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\SkipClassStringPassed;
final class FirstTypedCaller
{
public function run(ClassReflection $classReflection)
{
$skipClassStringPassed = new SkipClassStringPassed();
$skipClassStringPassed->resolve($classReflection->getName());
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedClassString;
use PHPStan\Reflection\ClassReflection;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\SkipClassStringPassed;
final class SecondTypedCaller
{
public function run(ClassReflection $classReflection)
{
$skipClassStringPassed = new SkipClassStringPassed();
$skipClassStringPassed->resolve($classReflection->getName());
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedClassType;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\SkipExpectedClassType;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\PassMeAsType;
final class FirstClassTypedCaller
{
public function goForIt(SkipExpectedClassType $skipExpectedClassType)
{
$knownType = new PassMeAsType();
$skipExpectedClassType->callMeWithClassType($knownType);
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedClassType;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\SkipExpectedClassType;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\PassMeAsType;
final class SecondClassTypedCaller
{
public function goForIt(SkipExpectedClassType $skipExpectedClassType)
{
$knownType = new PassMeAsType();
$skipExpectedClassType->callMeWithClassType($knownType);
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedNodeApi;
use PhpParser\Node\Stmt\Property;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\SkipApiMarked;
final class CallWithProperty
{
public function run(SkipApiMarked $skipApiMarked, Property $property): void
{
$skipApiMarked->callNode($property);
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedThisType;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\SkipThisPassedExactType;
final class CallByThis
{
public function run(SkipThisPassedExactType $skipThisPassedExactType): void
{
$skipThisPassedExactType->run($this);
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedThisType;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\ThisPassedFromInterface;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\SomeInterface;
final class CallByThisFromInterface implements SomeInterface
{
public function run(ThisPassedFromInterface $thisPassedFromInterface): void
{
$thisPassedFromInterface->run($this);
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedType;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\SkipProperlyFilledParamType;
final class FirstTypedCaller
{
public function goForIt(SkipProperlyFilledParamType $skipProperlyFilledParamType)
{
$skipProperlyFilledParamType->callMeTwice(100);
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedType;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\SkipUsedInternallyForSecondType;
use stdClass;
final class OnlyFirstTypeCalledOutside
{
public function run(SkipUsedInternallyForSecondType $skipUsedInternallyForSecondType)
{
$skipUsedInternallyForSecondType->run(new stdClass());
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedType;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\SkipProperlyFilledParamType;
final class SecondTypedCaller
{
public function goForIt(SkipProperlyFilledParamType $skipProperlyFilledParamType)
{
$skipProperlyFilledParamType->callMeTwice(100);
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedUnion;
use PhpParser\Node;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\SkipEqualUnionType;
final class CallUnionTypeArrayType
{
public function run(SkipEqualUnionType $skipEqualUnionType, Node $node): void
{
/** @var Node[]|Node $node */
$node = rand(0, 1)
? $node
: [$node];
$skipEqualUnionType->runArrayTyped($node);
}
public function run2(SkipEqualUnionType $skipEqualUnionType, Node $node): void
{
/** @var Node[]|Node $node */
$node = rand(0, 1)
? $node
: [$node];
$skipEqualUnionType->runArrayTyped2($node);
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Source\ExpectedUnion;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use Rector\TypePerfect\Tests\Rules\NarrowPublicClassMethodParamTypeRule\Fixture\SkipEqualUnionType;
final class CallUnionType
{
/**
* @param MethodCall[]|StaticCall[] $data
*/
public function run(SkipEqualUnionType $skipEqualUnionType, array $data): void
{
foreach ($data as $value) {
$skipEqualUnionType->run($value);
}
}
}

Some files were not shown because too many files have changed in this diff Show More