Compare commits
No commits in common. "main" and "8.1.1" have entirely different histories.
|
|
@ -11,7 +11,7 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPOSER_ROOT_VERSION: 8.2.x-dev
|
||||
COMPOSER_ROOT_VERSION: 8.1.x-dev
|
||||
PHP_VERSION: 8.4
|
||||
|
||||
permissions:
|
||||
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
|
@ -66,7 +66,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
|
@ -117,7 +117,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
|
@ -176,7 +176,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
|
|
|||
|
|
@ -2,14 +2,6 @@
|
|||
|
||||
All notable changes are documented in this file using the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
|
||||
|
||||
## [8.2.0] - 2026-08-07
|
||||
|
||||
### Added
|
||||
|
||||
* `ObjectExporter` interface, `ObjectExporterChain`, and `ExportContext` for customizing how objects are exported (an object exporter can be passed to `Exporter`'s constructor and is consulted before the default representation of an object is used)
|
||||
* `Exporter::shortenedExport()` and `Exporter::shortenedRecursiveExport()` use the representation provided by an object exporter as well; it is collapsed to a single line and shortened when it is longer than the configured maximum length
|
||||
* `Exporter::hasCustomRepresentationFor()` for checking whether an object exporter provides the representation for an object
|
||||
|
||||
## [8.1.1] - 2026-07-13
|
||||
|
||||
### Fixed
|
||||
|
|
@ -69,7 +61,6 @@ All notable changes are documented in this file using the [Keep a CHANGELOG](htt
|
|||
|
||||
* This component is no longer supported on PHP 8.2
|
||||
|
||||
[8.2.0]: https://github.com/sebastianbergmann/exporter/compare/8.1.1...main
|
||||
[8.1.1]: https://github.com/sebastianbergmann/exporter/compare/8.1.0...8.1.1
|
||||
[8.1.0]: https://github.com/sebastianbergmann/exporter/compare/8.0.3...8.1.0
|
||||
[8.0.3]: https://github.com/sebastianbergmann/exporter/compare/8.0.2...8.0.3
|
||||
|
|
|
|||
276
README.md
276
README.md
|
|
@ -25,19 +25,19 @@ composer require --dev sebastian/exporter
|
|||
Exporting:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter;
|
||||
|
||||
/*
|
||||
Exception Object #4 (
|
||||
'message' => '',
|
||||
'string' => '',
|
||||
'code' => 0,
|
||||
'file' => '/home/sebastianbergmann/test.php',
|
||||
'line' => 34,
|
||||
'previous' => null,
|
||||
Exception Object &0000000078de0f0d000000002003a261 (
|
||||
'message' => ''
|
||||
'string' => ''
|
||||
'code' => 0
|
||||
'file' => '/home/sebastianbergmann/test.php'
|
||||
'line' => 34
|
||||
'previous' => null
|
||||
)
|
||||
*/
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ print $exporter->export(new Exception);
|
|||
Exporting simple types:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter;
|
||||
|
|
@ -85,66 +85,64 @@ print $exporter->export(chr(0) . chr(1) . chr(2) . chr(3) . chr(4) . chr(5));
|
|||
Exporting complex types:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter;
|
||||
|
||||
/*
|
||||
Array &0 [
|
||||
0 => Array &1 [
|
||||
0 => 1,
|
||||
1 => 2,
|
||||
2 => 3,
|
||||
],
|
||||
1 => Array &2 [
|
||||
0 => '',
|
||||
1 => 0,
|
||||
2 => false,
|
||||
],
|
||||
]
|
||||
*/
|
||||
|
||||
print $exporter->export([[1, 2, 3], ['', 0, false]]);
|
||||
|
||||
/*
|
||||
Array &0 [
|
||||
'self' => Array &1 [
|
||||
'self' => Array &1,
|
||||
],
|
||||
]
|
||||
*/
|
||||
|
||||
$array = [];
|
||||
$array['self'] = &$array;
|
||||
|
||||
print $exporter->export($array);
|
||||
|
||||
/*
|
||||
stdClass Object #4 (
|
||||
'self' => stdClass Object #4,
|
||||
Array &0 (
|
||||
0 => Array &1 (
|
||||
0 => 1
|
||||
1 => 2
|
||||
2 => 3
|
||||
)
|
||||
1 => Array &2 (
|
||||
0 => ''
|
||||
1 => 0
|
||||
2 => false
|
||||
)
|
||||
)
|
||||
*/
|
||||
|
||||
$obj = new stdClass;
|
||||
$obj->self = $obj;
|
||||
print $exporter->export(array(array(1,2,3), array("",0,FALSE)));
|
||||
|
||||
/*
|
||||
Array &0 (
|
||||
'self' => Array &1 (
|
||||
'self' => Array &1
|
||||
)
|
||||
)
|
||||
*/
|
||||
|
||||
$array = array();
|
||||
$array['self'] = &$array;
|
||||
print $exporter->export($array);
|
||||
|
||||
/*
|
||||
stdClass Object &0000000003a66dcc0000000025e723e2 (
|
||||
'self' => stdClass Object &0000000003a66dcc0000000025e723e2
|
||||
)
|
||||
*/
|
||||
|
||||
$obj = new stdClass();
|
||||
$obj->self = $obj;
|
||||
print $exporter->export($obj);
|
||||
```
|
||||
|
||||
Compact exports:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
<?php
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter;
|
||||
|
||||
// []
|
||||
print $exporter->shortenedExport([]);
|
||||
// Array ()
|
||||
print $exporter->shortenedExport(array());
|
||||
|
||||
// [...]
|
||||
print $exporter->shortenedExport([1, 2, 3, 4, 5]);
|
||||
// Array (...)
|
||||
print $exporter->shortenedExport(array(1,2,3,4,5));
|
||||
|
||||
// stdClass Object ()
|
||||
print $exporter->shortenedExport(new stdClass);
|
||||
|
|
@ -152,7 +150,7 @@ print $exporter->shortenedExport(new stdClass);
|
|||
// Exception Object (...)
|
||||
print $exporter->shortenedExport(new Exception);
|
||||
|
||||
// 'this\nis\na\nsuper\nlong\nstr...nspace'
|
||||
// this\nis\na\nsuper\nlong\nstring\nt...\nspace
|
||||
print $exporter->shortenedExport(
|
||||
<<<LONG_STRING
|
||||
this
|
||||
|
|
@ -175,181 +173,3 @@ space
|
|||
LONG_STRING
|
||||
);
|
||||
```
|
||||
|
||||
## Customizing How Objects Are Exported
|
||||
|
||||
By default, an object is exported property by property. Implement the `ObjectExporter` interface to control how objects of a type are represented:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
use SebastianBergmann\Exporter\ExportContext;
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
use SebastianBergmann\Exporter\ObjectExporter;
|
||||
|
||||
final class MoneyExporter implements ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return $object instanceof Money;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
assert($object instanceof Money);
|
||||
|
||||
return sprintf('Money (%d %s)', $object->amount(), $object->currency());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
An object exporter is registered by passing it to `Exporter`'s constructor. It is consulted before the default representation of an object is used:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter(objectExporter: new MoneyExporter);
|
||||
|
||||
// Money (1999 EUR)
|
||||
print $exporter->export(new Money(1999, 'EUR'));
|
||||
|
||||
/*
|
||||
stdClass Object #6 (
|
||||
'net' => Money (1999 EUR),
|
||||
'gross' => Money (2379 EUR),
|
||||
)
|
||||
*/
|
||||
|
||||
$price = new stdClass;
|
||||
$price->net = new Money(1999, 'EUR');
|
||||
$price->gross = new Money(2379, 'EUR');
|
||||
|
||||
print $exporter->export($price);
|
||||
```
|
||||
|
||||
`ObjectExporterChain` composes multiple object exporters into a single one. They are consulted in the order in which they are composed into the chain and the first one whose `handles()` method returns `true` is asked for the representation:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
use SebastianBergmann\Exporter\ObjectExporterChain;
|
||||
|
||||
$exporter = new Exporter(
|
||||
objectExporter: new ObjectExporterChain(
|
||||
[
|
||||
new BasketExporter,
|
||||
new MoneyExporter,
|
||||
],
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
Enums are objects and are therefore passed to the object exporters as well. When no object exporter handles an object, the default representation is used for it.
|
||||
|
||||
### Repeated Occurrences
|
||||
|
||||
When the same object occurs more than once, the default representation is only used for the first occurrence; subsequent occurrences are replaced with a reference to the object:
|
||||
|
||||
```php
|
||||
/*
|
||||
Array &0 [
|
||||
0 => Money Object #8 (
|
||||
'amount' => 1999,
|
||||
'currency' => 'EUR',
|
||||
),
|
||||
1 => Money Object #8,
|
||||
]
|
||||
*/
|
||||
|
||||
$money = new Money(1999, 'EUR');
|
||||
|
||||
print (new Exporter)->export([$money, $money]);
|
||||
```
|
||||
|
||||
An object exporter is responsible for the entire representation of the object it handles. Every occurrence of such an object is therefore exported by it:
|
||||
|
||||
```php
|
||||
/*
|
||||
Array &0 [
|
||||
0 => Money (1999 EUR),
|
||||
1 => Money (1999 EUR),
|
||||
]
|
||||
*/
|
||||
|
||||
print $exporter->export([$money, $money]);
|
||||
```
|
||||
|
||||
The exception is an object that is (indirectly) nested in itself: it cannot be exported this way without recursing infinitely and is replaced with a reference to the object.
|
||||
|
||||
### Exporting Nested Values
|
||||
|
||||
An object exporter may use the `Exporter` it is given to export values that are nested in the object it handles. The `ExportContext` it is given must be passed on when it does. Otherwise, the export of these nested values starts over with an empty context and, for instance, assigns references to arrays that are already in use elsewhere in the same export:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
use SebastianBergmann\Exporter\ExportContext;
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
use SebastianBergmann\Exporter\ObjectExporter;
|
||||
|
||||
final class BasketExporter implements ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return $object instanceof Basket;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
assert($object instanceof Basket);
|
||||
|
||||
return 'Basket ' . $exporter->export($object->items(), $indentation, $context);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```php
|
||||
/*
|
||||
Array &0 [
|
||||
'basket' => Basket Array &1 [
|
||||
0 => Money (1999 EUR),
|
||||
],
|
||||
]
|
||||
*/
|
||||
|
||||
print $exporter->export(['basket' => new Basket([new Money(1999, 'EUR')])]);
|
||||
```
|
||||
|
||||
### Compact Exports
|
||||
|
||||
`Exporter::shortenedExport()` and `Exporter::shortenedRecursiveExport()` consult object exporters as well. The representation an object exporter provides is collapsed to a single line and shortened when it is longer than the configured maximum length:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter(objectExporter: new MoneyExporter);
|
||||
|
||||
// Money (1999 EUR)
|
||||
print $exporter->shortenedExport(new Money(1999, 'EUR'));
|
||||
```
|
||||
|
||||
Bear in mind that these representations are used where a short, single-line, and stable string is required. The representation of a data set is built using `Exporter::shortenedRecursiveExport()`, for instance, and is part of the name of a test that uses a data provider. An object exporter should therefore provide a representation that is compact and that does not change from one export to the next.
|
||||
|
||||
### Custom Representations
|
||||
|
||||
`Exporter::hasCustomRepresentationFor()` tells whether an object exporter provides the representation for an object. This is meant for code that renders objects itself and wants to use the representation an object exporter provides when there is one:
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
use SebastianBergmann\Exporter\Exporter;
|
||||
|
||||
$exporter = new Exporter(objectExporter: new MoneyExporter);
|
||||
|
||||
// true
|
||||
var_dump($exporter->hasCustomRepresentationFor(new Money(1999, 'EUR')));
|
||||
|
||||
// false
|
||||
var_dump($exporter->hasCustomRepresentationFor(new stdClass));
|
||||
```
|
||||
|
||||
Bear in mind that an object that is (indirectly) nested in itself is replaced with a reference to the object instead of being exported by an object exporter again.
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@
|
|||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.4",
|
||||
|
|
@ -45,7 +44,7 @@
|
|||
"sebastian/recursion-context": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^13.3"
|
||||
"phpunit/phpunit": "^13.2.4"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
|
|
@ -59,7 +58,7 @@
|
|||
},
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "8.2-dev"
|
||||
"dev-main": "8.1-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use SebastianBergmann\RecursionContext\Context as RecursionContext;
|
||||
use SplObjectStorage;
|
||||
|
||||
/**
|
||||
* Carries the state that is shared by all steps of a single export operation.
|
||||
*
|
||||
* An implementation of ObjectExporter that exports values nested in the object
|
||||
* it handles must pass the instance of this class it is given to the Exporter
|
||||
* it delegates to. Otherwise, the export of these nested values starts over
|
||||
* with an empty context and, for instance, assigns references to arrays that
|
||||
* are already in use elsewhere in the same export.
|
||||
*/
|
||||
final class ExportContext
|
||||
{
|
||||
private RecursionContext $recursionContext;
|
||||
|
||||
/**
|
||||
* @var SplObjectStorage<object, null>
|
||||
*/
|
||||
private SplObjectStorage $exportedByObjectExporter;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->recursionContext = new RecursionContext;
|
||||
$this->exportedByObjectExporter = new SplObjectStorage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of array|object
|
||||
*
|
||||
* @param T $value
|
||||
*
|
||||
* @param-out T $value
|
||||
*/
|
||||
public function add(array|object &$value): int
|
||||
{
|
||||
return $this->recursionContext->add($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T of array|object
|
||||
*
|
||||
* @param T $value
|
||||
*
|
||||
* @param-out T $value
|
||||
*/
|
||||
public function contains(array|object &$value): false|int
|
||||
{
|
||||
return $this->recursionContext->contains($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the export of an object by an ObjectExporter is in progress.
|
||||
*
|
||||
* This is the case when the object is (indirectly) nested in itself.
|
||||
*/
|
||||
public function isBeingExportedByObjectExporter(object $object): bool
|
||||
{
|
||||
return $this->exportedByObjectExporter->offsetExists($object);
|
||||
}
|
||||
|
||||
public function beginExportByObjectExporter(object $object): void
|
||||
{
|
||||
$this->exportedByObjectExporter->offsetSet($object);
|
||||
}
|
||||
|
||||
public function endExportByObjectExporter(object $object): void
|
||||
{
|
||||
$this->exportedByObjectExporter->offsetUnset($object);
|
||||
}
|
||||
}
|
||||
181
src/Exporter.php
181
src/Exporter.php
|
|
@ -61,17 +61,15 @@ final readonly class Exporter
|
|||
* @var positive-int
|
||||
*/
|
||||
private int $maxLengthForStrings;
|
||||
private ?ObjectExporter $objectExporter;
|
||||
|
||||
/**
|
||||
* @param non-negative-int $shortenArraysLongerThan
|
||||
* @param positive-int $maxLengthForStrings
|
||||
*/
|
||||
public function __construct(int $shortenArraysLongerThan = 0, int $maxLengthForStrings = 40, ?ObjectExporter $objectExporter = null)
|
||||
public function __construct(int $shortenArraysLongerThan = 0, int $maxLengthForStrings = 40)
|
||||
{
|
||||
$this->shortenArraysLongerThan = $shortenArraysLongerThan;
|
||||
$this->maxLengthForStrings = $maxLengthForStrings;
|
||||
$this->objectExporter = $objectExporter;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -86,23 +84,15 @@ final readonly class Exporter
|
|||
* - Strings are always quoted with single quotes
|
||||
* - Carriage returns and newlines are normalized to \n
|
||||
* - Recursion and repeated rendering is treated properly
|
||||
*
|
||||
* An implementation of ObjectExporter must pass the ExportContext it is
|
||||
* given to this method when it exports values that are nested in the
|
||||
* object it handles.
|
||||
*
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
public function export(mixed $value, int $indentation = 0, ?ExportContext $context = null): string
|
||||
public function export(mixed $value, int $indentation = 0): string
|
||||
{
|
||||
return $this->recursiveExport($value, $indentation, $context);
|
||||
return $this->recursiveExport($value, $indentation);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
* @param positive-int $maxLengthForStrings
|
||||
*
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
public function shortenedRecursiveExport(array &$data, int $maxLengthForStrings = 40, ?RecursionContext $processed = null): string
|
||||
{
|
||||
|
|
@ -136,13 +126,7 @@ final readonly class Exporter
|
|||
* Newlines are replaced by the visible string '\n'.
|
||||
* Contents of arrays and objects (if any) are replaced by '...'.
|
||||
*
|
||||
* The representation a custom object exporter provides for an object is
|
||||
* used, but it is collapsed to a single line and shortened when it is
|
||||
* longer than $maxLengthForStrings.
|
||||
*
|
||||
* @param positive-int $maxLengthForStrings
|
||||
*
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
public function shortenedExport(mixed $value, int $maxLengthForStrings = 40): string
|
||||
{
|
||||
|
|
@ -151,20 +135,13 @@ final readonly class Exporter
|
|||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
return $this->shorten($this->exportString($value), $maxLengthForStrings);
|
||||
}
|
||||
$string = str_replace("\n", '', $this->exportString($value));
|
||||
|
||||
if ($this->objectExporter !== null &&
|
||||
is_object($value) &&
|
||||
$this->objectExporter->handles($value)) {
|
||||
$context = new ExportContext;
|
||||
if (mb_strlen($string) > $maxLengthForStrings) {
|
||||
return mb_substr($string, 0, $maxLengthForStrings - 10) . '...' . mb_substr($string, -7);
|
||||
}
|
||||
|
||||
$context->beginExportByObjectExporter($value);
|
||||
|
||||
return $this->shorten(
|
||||
$this->objectExporter->export($value, $this, 0, $context),
|
||||
$maxLengthForStrings,
|
||||
);
|
||||
return $string;
|
||||
}
|
||||
|
||||
if ($value instanceof BackedEnum) {
|
||||
|
|
@ -202,26 +179,6 @@ final readonly class Exporter
|
|||
return $this->export($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a custom object exporter provides the representation
|
||||
* for an object.
|
||||
*
|
||||
* This is intended for code that renders objects itself and wants to use
|
||||
* the representation a custom object exporter provides when there is one.
|
||||
*
|
||||
* Bear in mind that an object that is (indirectly) nested in itself is
|
||||
* replaced with a reference to the object instead of being exported by a
|
||||
* custom object exporter again.
|
||||
*/
|
||||
public function hasCustomRepresentationFor(object $object): bool
|
||||
{
|
||||
if ($this->objectExporter === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->objectExporter->handles($object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an object to an array containing all of its private, protected
|
||||
* and public properties.
|
||||
|
|
@ -355,8 +312,6 @@ final readonly class Exporter
|
|||
/**
|
||||
* @param array<mixed> $data
|
||||
* @param positive-int $maxLengthForStrings
|
||||
*
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
private function shortenedCountedRecursiveExport(array &$data, RecursionContext $processed, int &$counter, int $maxLengthForStrings): string
|
||||
{
|
||||
|
|
@ -393,10 +348,7 @@ final readonly class Exporter
|
|||
return implode(', ', $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
private function recursiveExport(mixed &$value, int $indentation = 0, ?ExportContext $context = null): string
|
||||
private function recursiveExport(mixed &$value, int $indentation = 0, ?RecursionContext $processed = null): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return 'null';
|
||||
|
|
@ -423,42 +375,44 @@ final readonly class Exporter
|
|||
);
|
||||
}
|
||||
|
||||
if ($value instanceof BackedEnum) {
|
||||
return sprintf(
|
||||
'%s Enum #%d (%s, %s)',
|
||||
$value::class,
|
||||
spl_object_id($value),
|
||||
$value->name,
|
||||
$this->export($value->value),
|
||||
);
|
||||
}
|
||||
|
||||
if ($value instanceof UnitEnum) {
|
||||
return sprintf(
|
||||
'%s Enum #%d (%s)',
|
||||
$value::class,
|
||||
spl_object_id($value),
|
||||
$value->name,
|
||||
);
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
return $this->exportString($value);
|
||||
}
|
||||
|
||||
if ($context === null) {
|
||||
$context = new ExportContext;
|
||||
if ($processed === null) {
|
||||
$processed = new RecursionContext;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return $this->exportArray($value, $context, $indentation);
|
||||
return $this->exportArray($value, $processed, $indentation);
|
||||
}
|
||||
|
||||
if (is_object($value)) {
|
||||
return $this->exportObject($value, $context, $indentation);
|
||||
return $this->exportObject($value, $processed, $indentation);
|
||||
}
|
||||
|
||||
return var_export($value, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapses a representation to a single line and shortens it when it is
|
||||
* longer than $maxLengthForStrings.
|
||||
*
|
||||
* @param positive-int $maxLengthForStrings
|
||||
*/
|
||||
private function shorten(string $string, int $maxLengthForStrings): string
|
||||
{
|
||||
$string = str_replace(["\r", "\n"], '', $string);
|
||||
|
||||
if (mb_strlen($string) > $maxLengthForStrings) {
|
||||
return mb_substr($string, 0, $maxLengthForStrings - 10) . '...' . mb_substr($string, -7);
|
||||
}
|
||||
|
||||
return $string;
|
||||
}
|
||||
|
||||
private function exportFloat(float $value): string
|
||||
{
|
||||
if (is_nan($value)) {
|
||||
|
|
@ -529,17 +483,15 @@ final readonly class Exporter
|
|||
|
||||
/**
|
||||
* @param array<mixed> $value
|
||||
*
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
private function exportArray(array &$value, ExportContext $context, int $indentation): string
|
||||
private function exportArray(array &$value, RecursionContext $processed, int $indentation): string
|
||||
{
|
||||
if (($key = $context->contains($value)) !== false) {
|
||||
if (($key = $processed->contains($value)) !== false) {
|
||||
return 'Array &' . $key;
|
||||
}
|
||||
|
||||
$array = $value;
|
||||
$key = $context->add($value);
|
||||
$key = $processed->add($value);
|
||||
$values = '';
|
||||
|
||||
if (count($array) > 0) {
|
||||
|
|
@ -552,7 +504,7 @@ final readonly class Exporter
|
|||
$this->recursiveExport($k, $indentation)
|
||||
. ' => ' .
|
||||
/** @phpstan-ignore offsetAccess.invalidOffset */
|
||||
$this->recursiveExport($value[$k], $indentation + 1, $context)
|
||||
$this->recursiveExport($value[$k], $indentation + 1, $processed)
|
||||
. ",\n";
|
||||
}
|
||||
|
||||
|
|
@ -562,68 +514,15 @@ final readonly class Exporter
|
|||
return 'Array &' . (string) $key . ' [' . $values . ']';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
private function exportObject(object $value, ExportContext $context, int $indentation): string
|
||||
private function exportObject(object $value, RecursionContext $processed, int $indentation): string
|
||||
{
|
||||
$class = $value::class;
|
||||
|
||||
if ($this->objectExporter !== null) {
|
||||
// An object that is (indirectly) nested in itself cannot be
|
||||
// exported by a custom object exporter without recursing
|
||||
// infinitely and is therefore replaced with a reference to the
|
||||
// object.
|
||||
if ($context->isBeingExportedByObjectExporter($value)) {
|
||||
return $class . ' Object #' . spl_object_id($value);
|
||||
}
|
||||
|
||||
// A custom object exporter is responsible for the entire
|
||||
// representation of the object it handles. Therefore, it is asked
|
||||
// for that representation before the recursion context is
|
||||
// consulted: every occurrence of such an object is exported the
|
||||
// same way instead of repeated occurrences being replaced with a
|
||||
// reference to the object.
|
||||
if ($this->objectExporter->handles($value)) {
|
||||
$context->beginExportByObjectExporter($value);
|
||||
|
||||
try {
|
||||
return $this->objectExporter->export($value, $this, $indentation, $context);
|
||||
} finally {
|
||||
$context->endExportByObjectExporter($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enums are handled after a custom object exporter has been consulted
|
||||
// so that the representation of an enum can be customized, but before
|
||||
// the recursion context is consulted because an enum case is a
|
||||
// singleton for which a reference to a previous occurrence would be
|
||||
// less informative than the representation itself.
|
||||
if ($value instanceof BackedEnum) {
|
||||
return sprintf(
|
||||
'%s Enum #%d (%s, %s)',
|
||||
$class,
|
||||
spl_object_id($value),
|
||||
$value->name,
|
||||
$this->export($value->value),
|
||||
);
|
||||
}
|
||||
|
||||
if ($value instanceof UnitEnum) {
|
||||
return sprintf(
|
||||
'%s Enum #%d (%s)',
|
||||
$class,
|
||||
spl_object_id($value),
|
||||
$value->name,
|
||||
);
|
||||
}
|
||||
|
||||
if ($context->contains($value) !== false) {
|
||||
if ($processed->contains($value) !== false) {
|
||||
return $class . ' Object #' . spl_object_id($value);
|
||||
}
|
||||
|
||||
$context->add($value);
|
||||
$processed->add($value);
|
||||
|
||||
$array = $this->toArray($value);
|
||||
$buffer = '';
|
||||
|
|
@ -637,7 +536,7 @@ final readonly class Exporter
|
|||
. ' ' .
|
||||
$this->recursiveExport($k, $indentation)
|
||||
. ' => ' .
|
||||
$this->recursiveExport($v, $indentation + 1, $context)
|
||||
$this->recursiveExport($v, $indentation + 1, $processed)
|
||||
. ",\n";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
interface ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool;
|
||||
|
||||
/**
|
||||
* Exports an object this object exporter handles.
|
||||
*
|
||||
* The ExportContext must be passed on to Exporter::export() when values
|
||||
* that are nested in the object are exported using $exporter.
|
||||
*
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string;
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use function array_any;
|
||||
|
||||
final readonly class ObjectExporterChain implements ObjectExporter
|
||||
{
|
||||
/**
|
||||
* @var non-empty-list<ObjectExporter>
|
||||
*/
|
||||
private array $exporters;
|
||||
|
||||
/**
|
||||
* @param non-empty-list<ObjectExporter> $exporters
|
||||
*/
|
||||
public function __construct(array $exporters)
|
||||
{
|
||||
$this->exporters = $exporters;
|
||||
}
|
||||
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return array_any(
|
||||
$this->exporters,
|
||||
static fn (ObjectExporter $exporter) => $exporter->handles($object),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ObjectNotSupportedException
|
||||
*/
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
foreach ($this->exporters as $objectExporter) {
|
||||
if ($objectExporter->handles($object)) {
|
||||
return $objectExporter->export($object, $exporter, $indentation, $context);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ObjectNotSupportedException;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use Throwable;
|
||||
|
||||
interface Exception extends Throwable
|
||||
{
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class ObjectNotSupportedException extends RuntimeException implements Exception
|
||||
{
|
||||
}
|
||||
|
|
@ -30,7 +30,6 @@ use PHPUnit\Framework\Attributes\CoversClass;
|
|||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
|
||||
use PHPUnit\Framework\Attributes\Small;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
use SebastianBergmann\RecursionContext\Context;
|
||||
|
|
@ -38,8 +37,6 @@ use SplObjectStorage;
|
|||
use stdClass;
|
||||
|
||||
#[CoversClass(Exporter::class)]
|
||||
#[UsesClass(ExportContext::class)]
|
||||
#[UsesClass(ObjectExporterChain::class)]
|
||||
#[Small]
|
||||
final class ExporterTest extends TestCase
|
||||
{
|
||||
|
|
@ -652,428 +649,6 @@ EOF;
|
|||
$this->assertTrue($reflector->isUninitializedLazyObject($object));
|
||||
}
|
||||
|
||||
public function testObjectCanBeExportedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'stdClass (indentation: 0)',
|
||||
$exporter->export(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
public function testObjectCanBeExportedByCustomObjectExporterThatIsNotComposedIntoChain(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'stdClass (indentation: 0)',
|
||||
$exporter->export(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
public function testObjectNestedInArrayCanBeExportedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
<<<'EOT'
|
||||
Array &0 [
|
||||
0 => stdClass (indentation: 1),
|
||||
]
|
||||
EOT,
|
||||
$exporter->export([new stdClass]),
|
||||
);
|
||||
}
|
||||
|
||||
public function testObjectNestedInObjectCanBeExportedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesObjectsOfSpecificType(ExampleClass::class),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$object = new stdClass;
|
||||
$object->nested = new ExampleClass('bar');
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
<<<'EOT'
|
||||
stdClass Object #%d (
|
||||
'nested' => SebastianBergmann\Exporter\ExampleClass handled by custom exporter,
|
||||
)
|
||||
EOT,
|
||||
$exporter->export($object),
|
||||
);
|
||||
}
|
||||
|
||||
public function testEveryOccurrenceOfObjectHandledByCustomObjectExporterIsExportedByThatObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$object = new stdClass;
|
||||
|
||||
$this->assertSame(
|
||||
<<<'EOT'
|
||||
Array &0 [
|
||||
0 => stdClass (indentation: 1),
|
||||
1 => stdClass (indentation: 1),
|
||||
]
|
||||
EOT,
|
||||
$exporter->export([$object, $object]),
|
||||
);
|
||||
}
|
||||
|
||||
public function testRepeatedOccurrenceOfObjectNotHandledByCustomObjectExporterIsReplacedWithReferenceToObject(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$object = new stdClass;
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
<<<'EOT'
|
||||
Array &0 [
|
||||
0 => stdClass Object #%d (),
|
||||
1 => stdClass Object #%d,
|
||||
]
|
||||
EOT,
|
||||
$exporter->export([$object, $object]),
|
||||
);
|
||||
}
|
||||
|
||||
public function testDefaultExportIsUsedWhenNoCustomObjectExporterHandlesObject(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
'stdClass Object #%d ()',
|
||||
$exporter->export(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
public function testEnumCanBeExportedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'SebastianBergmann\Exporter\ExampleEnum (indentation: 0)',
|
||||
$exporter->export(ExampleEnum::Value),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'SebastianBergmann\Exporter\ExampleStringBackedEnum (indentation: 0)',
|
||||
$exporter->export(ExampleStringBackedEnum::Value),
|
||||
);
|
||||
}
|
||||
|
||||
public function testDefaultExportIsUsedForEnumWhenNoCustomObjectExporterHandlesIt(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
'SebastianBergmann\Exporter\ExampleEnum Enum #%d (Value)',
|
||||
$exporter->export(ExampleEnum::Value),
|
||||
);
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
'SebastianBergmann\Exporter\ExampleStringBackedEnum Enum #%d (Value, \'value\')',
|
||||
$exporter->export(ExampleStringBackedEnum::Value),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShortenedExportUsesRepresentationProvidedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'stdClass (indentation: 0)',
|
||||
$exporter->shortenedExport(new stdClass),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'SebastianBergmann\Exporter\ExampleEnum (indentation: 0)',
|
||||
$exporter->shortenedExport(ExampleEnum::Value, 80),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShortenedExportCollapsesRepresentationProvidedByCustomObjectExporterToSingleLine(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatReturnsGivenRepresentation("first\nsecond"),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'firstsecond',
|
||||
$exporter->shortenedExport(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShortenedExportShortensRepresentationProvidedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatReturnsGivenRepresentation(str_repeat('a', 41)),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
str_repeat('a', 30) . '...' . str_repeat('a', 7),
|
||||
$exporter->shortenedExport(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShortenedRecursiveExportUsesRepresentationProvidedByCustomObjectExporter(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
);
|
||||
|
||||
$data = [new stdClass, 'foo'];
|
||||
|
||||
$this->assertSame(
|
||||
"stdClass (indentation: 0), 'foo'",
|
||||
$exporter->shortenedRecursiveExport($data),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShortenedExportDoesNotRecurseInfinitelyForObjectNestedInItself(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatExportsNestedValues,
|
||||
);
|
||||
|
||||
$node = new Node;
|
||||
$node->children = [$node];
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
'Node(Array &0 [ 0 => SebastianBergmann\Exporter\Node Object #%d,])',
|
||||
$exporter->shortenedExport($node, 200),
|
||||
);
|
||||
}
|
||||
|
||||
public function testShortenedExportUsesDefaultRepresentationWhenNoCustomObjectExporterHandlesObject(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'stdClass Object ()',
|
||||
$exporter->shortenedExport(new stdClass),
|
||||
);
|
||||
}
|
||||
|
||||
public function testKnowsThatCustomObjectExporterProvidesRepresentationForObjectItHandles(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesObjectsOfSpecificType(ExampleClass::class),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertTrue($exporter->hasCustomRepresentationFor(new ExampleClass('bar')));
|
||||
}
|
||||
|
||||
public function testKnowsThatCustomObjectExporterDoesNotProvideRepresentationForObjectItDoesNotHandle(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesObjectsOfSpecificType(ExampleClass::class),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertFalse($exporter->hasCustomRepresentationFor(new stdClass));
|
||||
}
|
||||
|
||||
public function testKnowsThatNoCustomObjectExporterProvidesRepresentationForObjectWhenNoneIsConfigured(): void
|
||||
{
|
||||
$this->assertFalse((new Exporter)->hasCustomRepresentationFor(new stdClass));
|
||||
}
|
||||
|
||||
public function testCustomObjectExporterCanExportValuesNestedInObjectItHandles(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatExportsNestedValues,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
<<<'EOT'
|
||||
Node(Array &0 [
|
||||
'key' => 'value',
|
||||
])
|
||||
EOT,
|
||||
$exporter->export(new Node(['key' => 'value'])),
|
||||
);
|
||||
}
|
||||
|
||||
public function testArrayExportedByCustomObjectExporterDoesNotReuseReferenceToOtherArray(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatExportsNestedValues,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
<<<'EOT'
|
||||
Array &0 [
|
||||
0 => Node(Array &1 [
|
||||
'key' => 'value',
|
||||
]),
|
||||
]
|
||||
EOT,
|
||||
$exporter->export([new Node(['key' => 'value'])]),
|
||||
);
|
||||
}
|
||||
|
||||
public function testObjectNestedInItselfIsNotExportedByCustomObjectExporterAgain(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatExportsNestedValues,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$node = new Node;
|
||||
$node->children = [$node];
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
<<<'EOT'
|
||||
Node(Array &0 [
|
||||
0 => SebastianBergmann\Exporter\Node Object #%d,
|
||||
])
|
||||
EOT,
|
||||
$exporter->export($node),
|
||||
);
|
||||
}
|
||||
|
||||
public function testObjectsNestedInEachOtherAreNotExportedByCustomObjectExporterAgain(): void
|
||||
{
|
||||
$exporter = new Exporter(
|
||||
0,
|
||||
40,
|
||||
new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatExportsNestedValues,
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
$first = new Node;
|
||||
$second = new Node;
|
||||
$first->children = [$second];
|
||||
$second->children = [$first];
|
||||
|
||||
$this->assertStringMatchesFormat(
|
||||
<<<'EOT'
|
||||
Node(Array &0 [
|
||||
0 => Node(Array &1 [
|
||||
0 => SebastianBergmann\Exporter\Node Object #%d,
|
||||
]),
|
||||
])
|
||||
EOT,
|
||||
$exporter->export($first),
|
||||
);
|
||||
}
|
||||
|
||||
private function trimNewline(string $string): string
|
||||
{
|
||||
return (string) preg_replace('/[ ]*\n/', "\n", $string);
|
||||
|
|
|
|||
|
|
@ -1,80 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Small;
|
||||
use PHPUnit\Framework\Attributes\UsesClass;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use stdClass;
|
||||
|
||||
#[CoversClass(ObjectExporterChain::class)]
|
||||
#[UsesClass(Exporter::class)]
|
||||
#[UsesClass(ExportContext::class)]
|
||||
#[Small]
|
||||
final class ObjectExporterChainTest extends TestCase
|
||||
{
|
||||
public function testHandlesObjectWhenAtLeastOneComposedObjectExporterHandlesIt(): void
|
||||
{
|
||||
$chain = new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertTrue($chain->handles(new stdClass));
|
||||
}
|
||||
|
||||
public function testDoesNotHandleObjectWhenNoComposedObjectExporterHandlesIt(): void
|
||||
{
|
||||
$chain = new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertFalse($chain->handles(new stdClass));
|
||||
}
|
||||
|
||||
public function testDelegatesExportingToFirstComposedObjectExporterThatHandlesObject(): void
|
||||
{
|
||||
$chain = new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
new ObjectExporterThatHandlesObjectsOfSpecificType(stdClass::class),
|
||||
new ObjectExporterThatHandlesEveryObject,
|
||||
],
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
'stdClass handled by custom exporter',
|
||||
$chain->export(new stdClass, new Exporter, 0, new ExportContext),
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
ExampleClass::class . ' (indentation: 0)',
|
||||
$chain->export(new ExampleClass('foo'), new Exporter, 0, new ExportContext),
|
||||
);
|
||||
}
|
||||
|
||||
public function testCannotExportObjectWhenNoComposedObjectExporterHandlesIt(): void
|
||||
{
|
||||
$chain = new ObjectExporterChain(
|
||||
[
|
||||
new ObjectExporterThatHandlesNoObject,
|
||||
],
|
||||
);
|
||||
|
||||
$this->expectException(ObjectNotSupportedException::class);
|
||||
|
||||
$chain->export(new stdClass, new Exporter, 0, new ExportContext);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
/*
|
||||
* Helper to test export of objects that are nested in objects.
|
||||
*/
|
||||
final class Node
|
||||
{
|
||||
/**
|
||||
* @var array<mixed>
|
||||
*/
|
||||
public array $children;
|
||||
|
||||
/**
|
||||
* @param array<mixed> $children
|
||||
*/
|
||||
public function __construct(array $children = [])
|
||||
{
|
||||
$this->children = $children;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use function assert;
|
||||
use function sprintf;
|
||||
|
||||
final readonly class ObjectExporterThatExportsNestedValues implements ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return $object instanceof Node;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
assert($object instanceof Node);
|
||||
|
||||
return sprintf(
|
||||
'Node(%s)',
|
||||
$exporter->export($object->children, $indentation, $context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final readonly class ObjectExporterThatHandlesEveryObject implements ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s (indentation: %d)',
|
||||
$object::class,
|
||||
$indentation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
final readonly class ObjectExporterThatHandlesNoObject implements ObjectExporter
|
||||
{
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
throw new ObjectNotSupportedException;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final readonly class ObjectExporterThatHandlesObjectsOfSpecificType implements ObjectExporter
|
||||
{
|
||||
/**
|
||||
* @var class-string
|
||||
*/
|
||||
private string $type;
|
||||
|
||||
/**
|
||||
* @param class-string $type
|
||||
*/
|
||||
public function __construct(string $type)
|
||||
{
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return $object instanceof $this->type;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s handled by custom exporter',
|
||||
$object::class,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<?php declare(strict_types=1);
|
||||
/*
|
||||
* This file is part of sebastian/exporter.
|
||||
*
|
||||
* (c) Sebastian Bergmann <sebastian@phpunit.de>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
namespace SebastianBergmann\Exporter;
|
||||
|
||||
final readonly class ObjectExporterThatReturnsGivenRepresentation implements ObjectExporter
|
||||
{
|
||||
private string $representation;
|
||||
|
||||
public function __construct(string $representation)
|
||||
{
|
||||
$this->representation = $representation;
|
||||
}
|
||||
|
||||
public function handles(object $object): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function export(object $object, Exporter $exporter, int $indentation, ExportContext $context): string
|
||||
{
|
||||
return $this->representation;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.2.7",
|
||||
"phpstan/phpstan": "^2.2.5",
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.12",
|
||||
"tomasvotruba/type-coverage": "^2.3.0",
|
||||
"phpstan/phpstan-strict-rules": "^2.0.11",
|
||||
"tomasvotruba/type-coverage": "^2.2.2",
|
||||
"ergebnis/phpstan-rules": "^2.13.1"
|
||||
},
|
||||
"config": {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "3579343022b2a28c7195fe30b1752bfb",
|
||||
"content-hash": "f2f0502dde6131231f7156bb150bc1fa",
|
||||
"packages": [],
|
||||
"packages-dev": [
|
||||
{
|
||||
|
|
@ -130,11 +130,11 @@
|
|||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.2.7",
|
||||
"version": "2.2.5",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/692db47b9dddb0487934e5236e77d48594aef921",
|
||||
"reference": "692db47b9dddb0487934e5236e77d48594aef921",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0",
|
||||
"reference": "909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -190,32 +190,31 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-07-29T17:39:32+00:00"
|
||||
"time": "2026-07-05T06:31:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-strict-rules",
|
||||
"version": "2.0.12",
|
||||
"version": "2.0.11",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
|
||||
"reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b"
|
||||
"reference": "9b000a578b85b32945b358b172c7b20e91189024"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/2bc5ae19ae965663b62ac907ee6342c3903ec93b",
|
||||
"reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/9b000a578b85b32945b358b172c7b20e91189024",
|
||||
"reference": "9b000a578b85b32945b358b172c7b20e91189024",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpstan/phpstan": "^2.1.52"
|
||||
"phpstan/phpstan": "^2.1.39"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"shipmonk/name-collision-detector": "^2.1"
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"type": "phpstan-extension",
|
||||
"extra": {
|
||||
|
|
@ -240,37 +239,35 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.12"
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.11"
|
||||
},
|
||||
"time": "2026-07-19T07:24:06+00:00"
|
||||
"time": "2026-05-02T06:54:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tomasvotruba/type-coverage",
|
||||
"version": "2.3.0",
|
||||
"version": "2.2.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/TomasVotruba/type-coverage.git",
|
||||
"reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644"
|
||||
"reference": "25f298265c823e8fb0505169135855e1e6a91021"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
|
||||
"reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
|
||||
"url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/25f298265c823e8fb0505169135855e1e6a91021",
|
||||
"reference": "25f298265c823e8fb0505169135855e1e6a91021",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.4",
|
||||
"phpstan/phpstan": "^2.2",
|
||||
"webmozart/assert": "^1.11 || ^2.1"
|
||||
"phpstan/phpstan": "^2.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpunit/phpunit": "^13.2",
|
||||
"phpunit/phpunit": "^12.5",
|
||||
"rector/jack": "^1.0",
|
||||
"rector/rector": "^2.5",
|
||||
"rector/rector": "^2.4",
|
||||
"shipmonk/composer-dependency-analyser": "^1.8",
|
||||
"symfony/dom-crawler": "^8.1",
|
||||
"symplify/easy-coding-standard": "^13.2",
|
||||
"symplify/easy-coding-standard": "^13.1",
|
||||
"tomasvotruba/unused-public": "^2.2",
|
||||
"tracy/tracy": "^2.12"
|
||||
},
|
||||
|
|
@ -278,14 +275,12 @@
|
|||
"extra": {
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"config/extension.neon",
|
||||
"packages/type-perfect/config/extension.neon"
|
||||
"config/extension.neon"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Rector\\TypePerfect\\": "packages/type-perfect/src",
|
||||
"TomasVotruba\\TypeCoverage\\": "src"
|
||||
}
|
||||
},
|
||||
|
|
@ -300,7 +295,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/TomasVotruba/type-coverage/issues",
|
||||
"source": "https://github.com/TomasVotruba/type-coverage/tree/2.3.0"
|
||||
"source": "https://github.com/TomasVotruba/type-coverage/tree/2.2.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -312,73 +307,7 @@
|
|||
"type": "github"
|
||||
}
|
||||
],
|
||||
"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"
|
||||
"time": "2026-06-07T12:35:00+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
|
|
|
|||
|
|
@ -129,12 +129,12 @@
|
|||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.2.7",
|
||||
"version_normalized": "2.2.7.0",
|
||||
"version": "2.2.5",
|
||||
"version_normalized": "2.2.5.0",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/692db47b9dddb0487934e5236e77d48594aef921",
|
||||
"reference": "692db47b9dddb0487934e5236e77d48594aef921",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0",
|
||||
"reference": "909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
|
@ -143,7 +143,7 @@
|
|||
"conflict": {
|
||||
"phpstan/phpstan-shim": "*"
|
||||
},
|
||||
"time": "2026-07-29T17:39:32+00:00",
|
||||
"time": "2026-07-05T06:31:06+00:00",
|
||||
"bin": [
|
||||
"phpstan",
|
||||
"phpstan.phar"
|
||||
|
|
@ -196,31 +196,30 @@
|
|||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-strict-rules",
|
||||
"version": "2.0.12",
|
||||
"version_normalized": "2.0.12.0",
|
||||
"version": "2.0.11",
|
||||
"version_normalized": "2.0.11.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpstan-strict-rules.git",
|
||||
"reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b"
|
||||
"reference": "9b000a578b85b32945b358b172c7b20e91189024"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/2bc5ae19ae965663b62ac907ee6342c3903ec93b",
|
||||
"reference": "2bc5ae19ae965663b62ac907ee6342c3903ec93b",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan-strict-rules/zipball/9b000a578b85b32945b358b172c7b20e91189024",
|
||||
"reference": "9b000a578b85b32945b358b172c7b20e91189024",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpstan/phpstan": "^2.1.52"
|
||||
"phpstan/phpstan": "^2.1.39"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"shipmonk/name-collision-detector": "^2.1"
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"time": "2026-07-19T07:24:06+00:00",
|
||||
"time": "2026-05-02T06:54:10+00:00",
|
||||
"type": "phpstan-extension",
|
||||
"extra": {
|
||||
"phpstan": {
|
||||
|
|
@ -245,55 +244,51 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpstan-strict-rules/issues",
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.12"
|
||||
"source": "https://github.com/phpstan/phpstan-strict-rules/tree/2.0.11"
|
||||
},
|
||||
"install-path": "../phpstan/phpstan-strict-rules"
|
||||
},
|
||||
{
|
||||
"name": "tomasvotruba/type-coverage",
|
||||
"version": "2.3.0",
|
||||
"version_normalized": "2.3.0.0",
|
||||
"version": "2.2.2",
|
||||
"version_normalized": "2.2.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/TomasVotruba/type-coverage.git",
|
||||
"reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644"
|
||||
"reference": "25f298265c823e8fb0505169135855e1e6a91021"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
|
||||
"reference": "7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644",
|
||||
"url": "https://api.github.com/repos/TomasVotruba/type-coverage/zipball/25f298265c823e8fb0505169135855e1e6a91021",
|
||||
"reference": "25f298265c823e8fb0505169135855e1e6a91021",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.4",
|
||||
"phpstan/phpstan": "^2.2",
|
||||
"webmozart/assert": "^1.11 || ^2.1"
|
||||
"phpstan/phpstan": "^2.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpunit/phpunit": "^13.2",
|
||||
"phpunit/phpunit": "^12.5",
|
||||
"rector/jack": "^1.0",
|
||||
"rector/rector": "^2.5",
|
||||
"rector/rector": "^2.4",
|
||||
"shipmonk/composer-dependency-analyser": "^1.8",
|
||||
"symfony/dom-crawler": "^8.1",
|
||||
"symplify/easy-coding-standard": "^13.2",
|
||||
"symplify/easy-coding-standard": "^13.1",
|
||||
"tomasvotruba/unused-public": "^2.2",
|
||||
"tracy/tracy": "^2.12"
|
||||
},
|
||||
"time": "2026-07-29T20:32:37+00:00",
|
||||
"time": "2026-06-07T12:35:00+00:00",
|
||||
"type": "phpstan-extension",
|
||||
"extra": {
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"config/extension.neon",
|
||||
"packages/type-perfect/config/extension.neon"
|
||||
"config/extension.neon"
|
||||
]
|
||||
}
|
||||
},
|
||||
"installation-source": "dist",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Rector\\TypePerfect\\": "packages/type-perfect/src",
|
||||
"TomasVotruba\\TypeCoverage\\": "src"
|
||||
}
|
||||
},
|
||||
|
|
@ -308,7 +303,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/TomasVotruba/type-coverage/issues",
|
||||
"source": "https://github.com/TomasVotruba/type-coverage/tree/2.3.0"
|
||||
"source": "https://github.com/TomasVotruba/type-coverage/tree/2.2.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
|
@ -321,75 +316,6 @@
|
|||
}
|
||||
],
|
||||
"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,
|
||||
|
|
@ -398,7 +324,6 @@
|
|||
"phpstan/extension-installer",
|
||||
"phpstan/phpstan",
|
||||
"phpstan/phpstan-strict-rules",
|
||||
"tomasvotruba/type-coverage",
|
||||
"webmozart/assert"
|
||||
"tomasvotruba/type-coverage"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
'name' => '__root__',
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '4f08958e05d5bfc330b2f0b05252a7b7ddbe0558',
|
||||
'reference' => '33eb9b136b2312642002f4287bbdb796233c54b5',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
'__root__' => array(
|
||||
'pretty_version' => 'dev-main',
|
||||
'version' => 'dev-main',
|
||||
'reference' => '4f08958e05d5bfc330b2f0b05252a7b7ddbe0558',
|
||||
'reference' => '33eb9b136b2312642002f4287bbdb796233c54b5',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../../',
|
||||
'aliases' => array(),
|
||||
|
|
@ -38,40 +38,31 @@
|
|||
'dev_requirement' => true,
|
||||
),
|
||||
'phpstan/phpstan' => array(
|
||||
'pretty_version' => '2.2.7',
|
||||
'version' => '2.2.7.0',
|
||||
'reference' => '692db47b9dddb0487934e5236e77d48594aef921',
|
||||
'pretty_version' => '2.2.5',
|
||||
'version' => '2.2.5.0',
|
||||
'reference' => '909c1e5fef7989ac0d0c1c5c42e32a5c4f6198a0',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../phpstan/phpstan',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'phpstan/phpstan-strict-rules' => array(
|
||||
'pretty_version' => '2.0.12',
|
||||
'version' => '2.0.12.0',
|
||||
'reference' => '2bc5ae19ae965663b62ac907ee6342c3903ec93b',
|
||||
'pretty_version' => '2.0.11',
|
||||
'version' => '2.0.11.0',
|
||||
'reference' => '9b000a578b85b32945b358b172c7b20e91189024',
|
||||
'type' => 'phpstan-extension',
|
||||
'install_path' => __DIR__ . '/../phpstan/phpstan-strict-rules',
|
||||
'aliases' => array(),
|
||||
'dev_requirement' => true,
|
||||
),
|
||||
'tomasvotruba/type-coverage' => array(
|
||||
'pretty_version' => '2.3.0',
|
||||
'version' => '2.3.0.0',
|
||||
'reference' => '7ba48c1bf3bbb7ffe99caacc7017bae5ee72d644',
|
||||
'pretty_version' => '2.2.2',
|
||||
'version' => '2.2.2.0',
|
||||
'reference' => '25f298265c823e8fb0505169135855e1e6a91021',
|
||||
'type' => 'phpstan-extension',
|
||||
'install_path' => __DIR__ . '/../tomasvotruba/type-coverage',
|
||||
'aliases' => array(),
|
||||
'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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,14 +8,13 @@
|
|||
"keywords": ["static analysis"],
|
||||
"require": {
|
||||
"php": "^7.4 || ^8.0",
|
||||
"phpstan/phpstan": "^2.1.52"
|
||||
"phpstan/phpstan": "^2.1.39"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"phpstan/phpstan-deprecation-rules": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"shipmonk/name-collision-detector": "^2.1"
|
||||
"phpunit/phpunit": "^9.6"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
namespace PHPStan\Rules\Operators;
|
||||
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Scalar\Int_;
|
||||
use PHPStan\Analyser\Scope;
|
||||
use PHPStan\Rules\RuleLevelHelper;
|
||||
use PHPStan\Type\Accessory\AccessoryNumericStringType;
|
||||
|
|
@ -39,20 +38,7 @@ class OperatorRuleHelper
|
|||
return true;
|
||||
}
|
||||
|
||||
if ($this->isSubtypeOfNumber($scope, $expr)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the type supports arithmetic via operator overloading extensions
|
||||
// (only applies to object types like GMP, BCMath\Number)
|
||||
if ($type->isObject()->yes()) {
|
||||
$resultType = $scope->getType(new Expr\BinaryOp\Plus($expr, new Int_(1)));
|
||||
if (!$resultType instanceof ErrorType) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->isSubtypeOfNumber($scope, $expr);
|
||||
}
|
||||
|
||||
public function isValidForIncrement(Scope $scope, Expr $expr): bool
|
||||
|
|
@ -67,18 +53,7 @@ class OperatorRuleHelper
|
|||
return true;
|
||||
}
|
||||
|
||||
if ($this->isSubtypeOfNumber($scope, $expr)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($type->isObject()->yes()) {
|
||||
$resultType = $scope->getType(new Expr\PreInc($expr));
|
||||
if (!$resultType instanceof ErrorType) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->isSubtypeOfNumber($scope, $expr);
|
||||
}
|
||||
|
||||
public function isValidForDecrement(Scope $scope, Expr $expr): bool
|
||||
|
|
@ -88,18 +63,7 @@ class OperatorRuleHelper
|
|||
return true;
|
||||
}
|
||||
|
||||
if ($this->isSubtypeOfNumber($scope, $expr)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($type->isObject()->yes()) {
|
||||
$resultType = $scope->getType(new Expr\PreDec($expr));
|
||||
if (!$resultType instanceof ErrorType) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return $this->isSubtypeOfNumber($scope, $expr);
|
||||
}
|
||||
|
||||
private function isSubtypeOfNumber(Scope $scope, Expr $expr): bool
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,16 +1,16 @@
|
|||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAABCgAdFiEEynwsejDI6OEnSoR2UcZzBf/C5cAFAmpqOpYACgkQUcZzBf/C
|
||||
5cCEABAAiNhRb1h8uQWaC3yY2aDdrISyH/ONSWanM56iFhsOP7n34Yyjwr/d3ywv
|
||||
pFvy8CERGuIQj/3/Y4c8S84bpRZtZWOJ0+ti/dGsPG9nG4IMBpyOjaoHfqTuIqnI
|
||||
qpD+2xMNG6iVhoYrpPcTi85A1RqJyh+T+7OjMml63EjhApY7A/zM+yVvktTmRBGe
|
||||
d15vdOErXegl0i/2dSFgqoWmFtTNqy9twQutupjQeehrcTX7bjnqVvHL6E65dlLt
|
||||
wPFjAU9CnQCkT4mmyt7tHuHuUL/uuBnJDRipB2KCO7K5rG2SbX2r6YJTt81AcvQ7
|
||||
n0o6AMWf9YaTuzp73i2H1QMMsP/845CCHih8lJsg6a1VcdmEsdf+yvj4XJrjZgR1
|
||||
4dKoA58zjX1SJ0k+PHl04eshWlJhXWXioUNyD2JXrlZP0l6GuXZnO7X3QYPtFcL0
|
||||
C5mADqt5X1SIAiQYOyW/KjRmbt4EYjQYCBo+AQQR2yjm3FUjrfEI0rjF96/MIksp
|
||||
R73S9JKS+Fo/igbI86iJoNse4+ijwyGgT3702fGqUyAPVLDZptEUMXuAnJ276RO1
|
||||
URSHUXWTbPDCjoMOeOO8IcaKhCVnEElCssPsZCLZdoYWM6wM/4bdN/iOIHbIJNWL
|
||||
nuw97z7kkY2rvPCHNmdSvNQrH4Xe++9TtcDParXOoelFKwnkkU0=
|
||||
=YVL9
|
||||
iQIzBAABCgAdFiEEynwsejDI6OEnSoR2UcZzBf/C5cAFAmpJ+fMACgkQUcZzBf/C
|
||||
5cCr2w/9EwgQ9tw1wifi7D6DXBZe5wZiscozNE72vEdbvA5CrbSe8iwUULCBnkey
|
||||
Jx8bpz6g3FY1cUtIboEhwdOObQXLcctYYeolvGZFk6pvtvxCs4POmj0t3bjduhCh
|
||||
pbygjlaT6FZ7U5DmfioAd27t7JXJ0d6WwkBm3dV4OWMiGKidi3TU43YkO2zecrN5
|
||||
dV0hmOm2ewoXW5lGKp2eSo27BRXfb95IjNuqEiSuFUB/OA68Wbe0QIi/ZS7sUbhD
|
||||
4mBpl7Fo2ZhLAb6DyY3GKdJ3lCuzn/nbH26uPPVHvO/0tG2B5CIz2lIZlsSFsTmI
|
||||
HTBH8em3M9+aFsPAXi5qDl1qA5v+6jewmSnnQaAfdXfVBr+K0lNxQGFaONA6QZHa
|
||||
PEYWadTfDUNVlK7q5Kz71q9C5mDYSCBU6Q42KsZZ6NGnqYP/KSCyIPz5LfXLRyG5
|
||||
JUbZTaSFZZGf6YQfFTi26QpQSGlVOk2rHp3Pia/XVD4+xbQSjBaomQnrRFiEohKm
|
||||
NESTEdIJgszpBChNzPtNwx3J4RUo9jYn3TmAL2v0LuuUHckY7byuL8ec0bgxD1Qb
|
||||
XmtzLstiO9MUAslbjS50rX9okUUOXawaCu2QSkCbpdkwXaG/+DGoOH7PU70Bzi/B
|
||||
JXFDezIvA8VQV3cC2y2MlZgSqEdac/o6o/hQ42nDqvIFpDQ5Vrg=
|
||||
=YaiL
|
||||
-----END PGP SIGNATURE-----
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
86312b7
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -138,406 +138,3 @@ vendor/bin/phpstan
|
|||
<br>
|
||||
|
||||
Happy coding!
|
||||
|
||||
<br>
|
||||
|
||||
---
|
||||
|
||||
# Type Perfect
|
||||
|
||||
[](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!
|
||||
|
|
|
|||
|
|
@ -7,9 +7,4 @@ use ShipMonk\ComposerDependencyAnalyser\Config\ErrorType;
|
|||
|
||||
return new Configuration()
|
||||
// PhpParser classes are provided by phpstan/phpstan, no need to require nikic/php-parser directly
|
||||
->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]
|
||||
);
|
||||
->ignoreErrorsOnPackage('nikic/php-parser', [ErrorType::SHADOW_DEPENDENCY]);
|
||||
|
|
|
|||
|
|
@ -6,30 +6,26 @@
|
|||
"keywords": ["static analysis", "phpstan-extension"],
|
||||
"require": {
|
||||
"php": "^8.4",
|
||||
"phpstan/phpstan": "^2.2",
|
||||
"webmozart/assert": "^1.11 || ^2.1"
|
||||
"phpstan/phpstan": "^2.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpunit/phpunit": "^13.2",
|
||||
"symplify/easy-coding-standard": "^13.2",
|
||||
"rector/rector": "^2.5",
|
||||
"phpunit/phpunit": "^12.5",
|
||||
"symplify/easy-coding-standard": "^13.1",
|
||||
"rector/rector": "^2.4",
|
||||
"tracy/tracy": "^2.12",
|
||||
"tomasvotruba/unused-public": "^2.2",
|
||||
"rector/jack": "^1.0",
|
||||
"shipmonk/composer-dependency-analyser": "^1.8",
|
||||
"symfony/dom-crawler": "^8.1"
|
||||
"shipmonk/composer-dependency-analyser": "^1.8"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"TomasVotruba\\TypeCoverage\\": "src",
|
||||
"Rector\\TypePerfect\\": "packages/type-perfect/src"
|
||||
"TomasVotruba\\TypeCoverage\\": "src"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"TomasVotruba\\TypeCoverage\\Tests\\": "tests",
|
||||
"Rector\\TypePerfect\\Tests\\": "packages/type-perfect/tests"
|
||||
"TomasVotruba\\TypeCoverage\\Tests\\": "tests"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
|
@ -47,8 +43,7 @@
|
|||
"extra": {
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"config/extension.neon",
|
||||
"packages/type-perfect/config/extension.neon"
|
||||
"config/extension.neon"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
<?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()];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\TypePerfect\Enum\Types;
|
||||
|
||||
final class ResolvedTypes
|
||||
{
|
||||
public const string UNKNOWN_TYPES = 'unknown_types';
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?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() === [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
<?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');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?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');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?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]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
<?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());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
<?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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
<?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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
<?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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
<?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 [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<?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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<?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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<?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([]));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Rector\TypePerfect\Tests\Rules\NarrowPrivateClassMethodParamTypeRule\Fixture;
|
||||
|
||||
use PhpParser\Node;
|
||||
|
||||
final class SkipNoArgs
|
||||
{
|
||||
public function run()
|
||||
{
|
||||
$this->execute();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue