diff --git a/CHANGELOG.md b/CHANGELOG.md index aa24e48..99d3fd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ### Upcoming release +- add PHPStan inference for literal `Arrayy::get()` dot-notation paths on typed subclasses while keeping custom path separators sound +- improve callable generic inference and preserve transformed value types across `each()` and `map()` +- fix nested dot-notation removal so removing a deep key preserves the root array and sibling values +- preserve `Traversable` entries when mapping JSON data and harden array/object path traversal around scalar intermediates - fix `average()` so non-numeric values no longer error on modern PHP versions - make `changeKeyCase()` Unicode case conversion deterministic across PHP 8.0–8.5 - strengthen native property type checks, array-shape contracts, and regression coverage across Json mapper and collection helpers diff --git a/README.md b/README.md index 681125e..d132fd3 100644 --- a/README.md +++ b/README.md @@ -120,12 +120,26 @@ $arrayy->Lars->lastname; // 'Müller' The library offers type checking for phpdoc array-shape annotations, legacy `@property` phpdoc-class-comments, and native declared properties. Prefer the array-shape form because it can reuse the `Arrayy` template for IDE autocompletion and static-analysis support. `meta()` is also understood by PHPStan, so `meta()`-derived keys such as `$userMeta->city` and `$cityMeta->name` keep precise literal-string information during static analysis. When you want PHPStan to check reads precisely, prefer array-like access with literal keys (for example `$user['lastName']`) or narrowed `meta()` keys on these array-shape-based models. Do not combine array-shape annotations and `@property` tags on the same model. -If you use PHPStan and call `YourArrayySubclass::meta()`, you can register the custom return-type extension from `src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php`. Use it when you want PHPStan to understand that `meta()` returns an object shape whose properties are the exact array keys collected from your array-shape annotations, `@property` tags, or native declared properties. That keeps expressions such as `$userMeta->id` typed as the literal string `'id'`, helps nested lookups like `$user[$userMeta->city][$cityMeta->name]`, and lets PHPStan report invalid meta-property access such as `$userMeta->ghost`. +If you use PHPStan, register the return-type extensions from `src/PHPStan`. `GetDynamicMethodReturnTypeExtension` resolves literal dot-notation paths against a typed subclass's `TData` array shape, including fallback values. `MetaDynamicStaticMethodReturnTypeExtension` understands that `meta()` returns an object shape whose properties are the exact keys collected from array-shape annotations, `@property` tags, or native declared properties. + +All three access styles can therefore participate in static analysis: + +```php +$name = $user->get('profile.name', 'Guest'); // dot path resolved from TData +$name = $user['profile']['name']; // ArrayAccess / array-shape inference +$name = $user->profile->name; // @property-read declarations +``` + +Dot-notation inference intentionally applies to literal dotted paths on typed `Arrayy` subclasses with a stable array-shape `TData` that implement `Arrayy\PHPStan\DefaultDotNotationTypeInterface`. Implementing this marker is a promise that the subclass keeps Arrayy's default `.` separator and does not switch it with `changeSeparator()`; without that promise, splitting the path statically would be unsound. Dynamic strings, wildcard paths, custom separators, and plain `Arrayy` instances retain the method's safe general return type. Object-property access should be declared with `@property` or `@property-read`; `meta()` keeps generated key access precise. The repository's own `phpstan.neon` registers the extension like this; copy the same service definition into your project's PHPStan config because this repository file is not shipped in the Composer package: ```neon services: + - + class: Arrayy\PHPStan\GetDynamicMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension - class: Arrayy\PHPStan\MetaDynamicStaticMethodReturnTypeExtension tags: @@ -137,7 +151,7 @@ services: * @template T of array{id: int, firstName: int|string, lastName: string, city?: City|null} * @extends \Arrayy\Arrayy,value-of,T> */ -class User extends \Arrayy\Arrayy +class User extends \Arrayy\Arrayy implements \Arrayy\PHPStan\DefaultDotNotationTypeInterface { protected $checkPropertyTypes = true; @@ -148,7 +162,7 @@ class User extends \Arrayy\Arrayy * @template T of array{plz: string|null, name: string, infos: string[]} * @extends \Arrayy\Arrayy,value-of,T> */ -class City extends \Arrayy\Arrayy +class City extends \Arrayy\Arrayy implements \Arrayy\PHPStan\DefaultDotNotationTypeInterface { protected $checkPropertyTypes = true; diff --git a/build/docs/base.md b/build/docs/base.md index ec23151..2766f77 100644 --- a/build/docs/base.md +++ b/build/docs/base.md @@ -119,12 +119,26 @@ $arrayy->Lars->lastname; // 'Müller' The library offers type checking for phpdoc array-shape annotations, legacy `@property` phpdoc-class-comments, and native declared properties. Prefer the array-shape form because it can reuse the `Arrayy` template for IDE autocompletion and static-analysis support. `meta()` is also understood by PHPStan, so `meta()`-derived keys such as `$userMeta->city` and `$cityMeta->name` keep precise literal-string information during static analysis. When you want PHPStan to check reads precisely, prefer array-like access with literal keys (for example `$user['lastName']`) or narrowed `meta()` keys on these array-shape-based models. Do not combine array-shape annotations and `@property` tags on the same model. -If you use PHPStan and call `YourArrayySubclass::meta()`, you can register the custom return-type extension from `src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php`. Use it when you want PHPStan to understand that `meta()` returns an object shape whose properties are the exact array keys collected from your array-shape annotations, `@property` tags, or native declared properties. That keeps expressions such as `$userMeta->id` typed as the literal string `'id'`, helps nested lookups like `$user[$userMeta->city][$cityMeta->name]`, and lets PHPStan report invalid meta-property access such as `$userMeta->ghost`. +If you use PHPStan, register the return-type extensions from `src/PHPStan`. `GetDynamicMethodReturnTypeExtension` resolves literal dot-notation paths against a typed subclass's `TData` array shape, including fallback values. `MetaDynamicStaticMethodReturnTypeExtension` understands that `meta()` returns an object shape whose properties are the exact keys collected from array-shape annotations, `@property` tags, or native declared properties. + +All three access styles can therefore participate in static analysis: + +```php +$name = $user->get('profile.name', 'Guest'); // dot path resolved from TData +$name = $user['profile']['name']; // ArrayAccess / array-shape inference +$name = $user->profile->name; // @property-read declarations +``` + +Dot-notation inference intentionally applies to literal dotted paths on typed `Arrayy` subclasses with a stable array-shape `TData` that implement `Arrayy\PHPStan\DefaultDotNotationTypeInterface`. Implementing this marker is a promise that the subclass keeps Arrayy's default `.` separator and does not switch it with `changeSeparator()`; without that promise, splitting the path statically would be unsound. Dynamic strings, wildcard paths, custom separators, and plain `Arrayy` instances retain the method's safe general return type. Object-property access should be declared with `@property` or `@property-read`; `meta()` keeps generated key access precise. The repository's own `phpstan.neon` registers the extension like this; copy the same service definition into your project's PHPStan config because this repository file is not shipped in the Composer package: ```neon services: + - + class: Arrayy\PHPStan\GetDynamicMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension - class: Arrayy\PHPStan\MetaDynamicStaticMethodReturnTypeExtension tags: @@ -136,7 +150,7 @@ services: * @template T of array{id: int, firstName: int|string, lastName: string, city?: City|null} * @extends \Arrayy\Arrayy,value-of,T> */ -class User extends \Arrayy\Arrayy +class User extends \Arrayy\Arrayy implements \Arrayy\PHPStan\DefaultDotNotationTypeInterface { protected $checkPropertyTypes = true; @@ -147,7 +161,7 @@ class User extends \Arrayy\Arrayy * @template T of array{plz: string|null, name: string, infos: string[]} * @extends \Arrayy\Arrayy,value-of,T> */ -class City extends \Arrayy\Arrayy +class City extends \Arrayy\Arrayy implements \Arrayy\PHPStan\DefaultDotNotationTypeInterface { protected $checkPropertyTypes = true; diff --git a/phpstan.neon b/phpstan.neon index ff963b3..2b32038 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,11 +1,19 @@ parameters: level: 8 reportUnmatchedIgnoredErrors: true + excludePaths: + analyse: + - %currentWorkingDirectory%/tests/PHPStan/ArrayShapeInvalidUsage.php + - %currentWorkingDirectory%/tests/PHPStan/MetaInvalidUsage.php paths: - %currentWorkingDirectory%/src/ - %currentWorkingDirectory%/tests/ services: + - + class: Arrayy\PHPStan\GetDynamicMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension - class: Arrayy\PHPStan\MetaDynamicStaticMethodReturnTypeExtension tags: diff --git a/src/Arrayy.php b/src/Arrayy.php index 55f51ad..2d92739 100644 --- a/src/Arrayy.php +++ b/src/Arrayy.php @@ -247,7 +247,7 @@ public function &__get($key) if (\is_array($return) === true) { $return = static::create( - [], + [], // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false )->createByReference($return); @@ -282,7 +282,7 @@ public function add($value, $key = null) ); } - $this->internalSet($key, $value); + $this->internalSet($key, $value); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) return $this; } @@ -761,7 +761,7 @@ public function offsetExists($offset): bool $this->callAtPath( $containerPath, static function ($container) use ($lastOffset, &$offsetExists) { - $offsetExists = \array_key_exists($lastOffset, $container); + $offsetExists = \is_array($container) && \array_key_exists($lastOffset, $container); } ); } @@ -788,10 +788,10 @@ public function &offsetGet($offset) $value = null; if ($this->offsetExists($offset)) { - $value = &$this->__get($offset); + $value = &$this->__get($offset); // @phpstan-ignore-line argument.templateType, argument.type (the dynamic value is intentionally forwarded through an invariant generic boundary) } - return $value; + return $value; // @phpstan-ignore return.type (offsetGet() intentionally returns the referenced value selected at runtime) } /** @@ -1067,7 +1067,7 @@ public function appendArrayValues(array $values, $key = null) \is_array($this->array[$key]) ) { foreach ($values as $value) { - $this->array[$key][] = $value; + $this->array[$key][] = $value; // @phpstan-ignore-line assign.propertyType (runtime normalization intentionally rebuilds the generic backing array) } } else { foreach ($values as $value) { @@ -1103,7 +1103,7 @@ public function appendToEachKey($prefix): self if ($item instanceof self) { $result[$prefix . $key] = $item->appendToEachKey($prefix); } elseif (\is_array($item)) { - $result[$prefix . $key] = self::create($item, $this->iteratorClass, false) + $result[$prefix . $key] = self::create($item, $this->iteratorClass, false) // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) ->appendToEachKey($prefix) ->toArray(); } else { @@ -1112,7 +1112,7 @@ public function appendToEachKey($prefix): self } return self::create( - $result, + $result, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -1138,7 +1138,7 @@ public function appendToEachValue($prefix): self if ($item instanceof self) { $result[$key] = $item->appendToEachValue($prefix); } elseif (\is_array($item)) { - $result[$key] = self::create($item, $this->iteratorClass, false)->appendToEachValue($prefix)->toArray(); + $result[$key] = self::create($item, $this->iteratorClass, false)->appendToEachValue($prefix)->toArray(); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } elseif (\is_object($item) === true) { $result[$key] = $item; } else { @@ -1146,7 +1146,7 @@ public function appendToEachValue($prefix): self } } - return self::create($result, $this->iteratorClass, false); + return self::create($result, $this->iteratorClass, false); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** @@ -1215,7 +1215,7 @@ public function at(\Closure $closure): self } return static::create( - $that->toArray(), + $that->toArray(), // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -1307,7 +1307,7 @@ public function changeKeyCase(int $case = \CASE_LOWER): self } return static::create( - $return, + $return, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -1345,7 +1345,7 @@ public function changeSeparator($separator): self * @return static|static[] *

(Immutable) A new array of chunks from the original array.

* - * @phpstan-return static + * @phpstan-return self>,array>>> * @psalm-mutation-free */ public function chunk($size, $preserveKeys = false): self @@ -1738,7 +1738,7 @@ public function containsValues(array $needles): bool public function countValues(): self { /** @phpstan-var static $return - help for phpstan */ - $return = self::create(\array_count_values($this->toArray()), $this->iteratorClass); + $return = self::create(\array_count_values($this->toArray()), $this->iteratorClass); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) return $return; } @@ -1868,7 +1868,7 @@ public static function createFromGeneratorFunction(callable $generatorFunction): */ public static function createFromGeneratorImmutable(\Generator $generator): self { - return self::create(\iterator_to_array($generator, true)); + return self::create(\iterator_to_array($generator, true)); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** @@ -1901,7 +1901,7 @@ public static function createFromJson(string $json): self */ public static function createFromArray(array $array): self { - return static::create($array); + return static::create($array); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** @@ -1998,7 +1998,7 @@ static function (&$val) { ); /** @var static $return - help for phpstan */ - $return = static::create($array); + $return = static::create($array); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) return $return; } @@ -2020,7 +2020,7 @@ static function (&$val) { */ public static function createFromTraversableImmutable(\Traversable $traversable, bool $use_keys = true): self { - return self::create(\iterator_to_array($traversable, $use_keys)); + return self::create(\iterator_to_array($traversable, $use_keys)); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** @@ -2039,7 +2039,7 @@ public static function createFromTraversableImmutable(\Traversable $traversable, public static function createWithRange($low, $high, $step = 1): self { /** @phpstan-var static $return - help for phpstan */ - $return = static::create(\range($low, $high, $step)); + $return = static::create(\range($low, $high, $step)); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) return $return; } @@ -2365,7 +2365,7 @@ public function diffRecursive(array $array = [], $helperVariableForRecursion = n } return static::create( - $result, + $result, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -2390,7 +2390,7 @@ public function diffRecursive(array $array = [], $helperVariableForRecursion = n public function diffReverse(array $array = []): self { return static::create( - \array_diff($array, $this->toArray()), + \array_diff($array, $this->toArray()), // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -2412,7 +2412,7 @@ public function diffReverse(array $array = []): self public function divide(): self { return static::create( - [ + [ // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->keys(), $this->values(), ], @@ -2437,8 +2437,11 @@ public function divide(): self * @return static *

(Immutable)

* - * @phpstan-param \Closure(T,?TKey):T $closure - * @phpstan-return static + * @template TEach + *

The output value type.

+ * + * @phpstan-param \Closure(T,?TKey):TEach $closure + * @phpstan-return static> * @psalm-mutation-free */ public function each(\Closure $closure): self @@ -2450,8 +2453,8 @@ public function each(\Closure $closure): self $array[$key] = $closure($value, $key); } - return static::create( - $array, + return static::create( // @phpstan-ignore return.type (create() is intentionally re-parameterized with TEach) + $array, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -2552,7 +2555,7 @@ public function fillWithDefaults(int $num, $default = null): self } return static::create( - $tmpArray, + $tmpArray, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -2729,7 +2732,7 @@ static function ($item) use ( $comparisonOp ) { $item = (array) $item; - $itemArrayy = static::create($item); + $itemArrayy = static::create($item); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $item[$property] = $itemArrayy->get($property, []); return $ops[$comparisonOp]($item, $property, $value); @@ -2738,7 +2741,7 @@ static function ($item) use ( ); return static::create( - $result, + $result, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -2901,7 +2904,7 @@ public function firstsImmutable(?int $number = null): self } return static::create( - $array, + $array, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -2930,7 +2933,7 @@ public function firstsKeys(?int $number = null): self } return static::create( - $array, + $array, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -3057,7 +3060,7 @@ public function get( if ($key === null) { return static::create( - [], + [], // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false )->createByReference($usedArray); @@ -3072,7 +3075,7 @@ public function get( if (\array_key_exists($key, $usedArray) === true) { if (\is_array($usedArray[$key])) { return static::create( - [], + [], // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false )->createByReference($usedArray[$key]); @@ -3124,7 +3127,7 @@ public function get( unset($segmentsTmp[0]); $keyTmp = \implode('.', $segmentsTmp); $returnTmp = static::create( - [], + [], // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -3170,7 +3173,7 @@ public function get( if (\is_array($usedArrayTmp)) { return static::create( - [], + [], // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false )->createByReference($usedArrayTmp); @@ -3184,7 +3187,7 @@ public function get( } return static::create( - [], + [], // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false )->createByReference($usedArray); @@ -3561,7 +3564,7 @@ public function getValues() $this->generatorToArray(false); return static::create( - \array_values($this->array), + \array_values($this->array), // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -3630,7 +3633,7 @@ public function group($grouper, bool $saveKeys = false): self } return static::create( - $result, + $result, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -3745,7 +3748,7 @@ public function indexBy($key): self } return static::create( - $results, + $results, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -3814,7 +3817,7 @@ public function intersection(array $search, bool $keepKeys = false): self * @psalm-suppress MissingClosureParamType */ return static::create( - \array_uintersect( + \array_uintersect( // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->toArray(), $search, static function ($a, $b) { @@ -3827,7 +3830,7 @@ static function ($a, $b) { } return static::create( - \array_values(\array_intersect($this->toArray(), $search)), + \array_values(\array_intersect($this->toArray(), $search)), // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -3848,7 +3851,7 @@ static function ($a, $b) { public function intersectionMulti(...$array): self { return static::create( - \array_values(\array_intersect($this->toArray(), ...$array)), + \array_values(\array_intersect($this->toArray(), ...$array)), // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -3904,7 +3907,7 @@ public function invoke($callable, $arguments = []): self } return static::create( - $array, + $array, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -4049,7 +4052,7 @@ public function isSequential(bool $recursive = false): bool && (\is_array($value) || $value instanceof \Traversable) && - self::create($value)->isSequential() === false + self::create($value)->isSequential() === false // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) ) { return false; } @@ -4157,7 +4160,7 @@ public function keys( ); return static::create( - $array, + $array, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -4324,7 +4327,7 @@ public function lastsImmutable(?int $number = null): self { if ($this->isEmpty()) { return static::create( - [], + [], // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -4340,7 +4343,7 @@ public function lastsImmutable(?int $number = null): self } $arrayy = static::create( - $poppedValue, + $poppedValue, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -4412,7 +4415,7 @@ public function length(int $mode = \COUNT_NORMAL): int *

The output value type.

* * @phpstan-param callable(T,TKey=,mixed=):T2 $callable - * @phpstan-return static + * @phpstan-return static> * @psalm-mutation-free */ public function map( @@ -4581,7 +4584,7 @@ public function mergeAppendKeepIndex(array $array = [], bool $recursive = false) } return static::create( - $result, + $result, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -4623,7 +4626,7 @@ public function mergeAppendNewIndex(array $array = [], bool $recursive = false): } return static::create( - $result, + $result, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -4664,7 +4667,7 @@ public function mergePrependKeepIndex(array $array = [], bool $recursive = false } return static::create( - $result, + $result, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -4706,7 +4709,7 @@ public function mergePrependNewIndex(array $array = [], bool $recursive = false) } return static::create( - $result, + $result, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -4767,8 +4770,7 @@ public function min() */ public function mostUsedValue() { - /* @phpstan-ignore return.type */ - return $this->countValues()->arsortImmutable()->firstKey(); + return $this->countValues()->arsortImmutable()->firstKey(); // @phpstan-ignore return.type (countValues() changes the intermediate value type, while firstKey() restores the original value) } /** @@ -4832,7 +4834,7 @@ public function moveElement($from, $to): self } return static::create( - $output, + $output, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -4863,7 +4865,7 @@ public function moveElementToFirstPlace($key): self } return static::create( - $array, + $array, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -4894,7 +4896,7 @@ public function moveElementToLastPlace($key): self } return static::create( - $array, + $array, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -4997,7 +4999,7 @@ public function only(array $keys): self public function pad(int $size, $value): self { return static::create( - \array_pad($this->toArray(), $size, $value), + \array_pad($this->toArray(), $size, $value), // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5032,7 +5034,7 @@ public function partition(\Closure $closure): array } } - return [self::create($matches), self::create($noMatches)]; + return [self::create($matches), self::create($noMatches)]; // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** @@ -5148,7 +5150,7 @@ public function prependToEachKey($suffix): self $result[$key] = $item->prependToEachKey($suffix); } elseif (\is_array($item)) { $result[$key] = self::create( - $item, + $item, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false )->prependToEachKey($suffix) @@ -5159,7 +5161,7 @@ public function prependToEachKey($suffix): self } return self::create( - $result, + $result, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5186,7 +5188,7 @@ public function prependToEachValue($suffix): self $result[$key] = $item->prependToEachValue($suffix); } elseif (\is_array($item)) { $result[$key] = self::create( - $item, + $item, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false )->prependToEachValue($suffix) @@ -5199,7 +5201,7 @@ public function prependToEachValue($suffix): self } return self::create( - $result, + $result, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5294,7 +5296,7 @@ public function randomImmutable(?int $number = null): self if ($this->count() === 0) { return static::create( - [], + [], // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5304,7 +5306,7 @@ public function randomImmutable(?int $number = null): self $arrayRandValue = [$this->array[\array_rand($this->array)]]; return static::create( - $arrayRandValue, + $arrayRandValue, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5314,7 +5316,7 @@ public function randomImmutable(?int $number = null): self \shuffle($arrayTmp); return static::create( - $arrayTmp, + $arrayTmp, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false )->firstsImmutable($number); @@ -5385,7 +5387,7 @@ public function randomKeys(int $number): self $result = (array) \array_rand($this->array, $number); return static::create( - $result, + $result, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5411,7 +5413,7 @@ public function randomMutable(?int $number = null): self if ($this->count() === 0) { return static::create( - [], + [], // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5562,7 +5564,7 @@ public function reduce_dimension(bool $unique = true): self foreach ($this->getGenerator() as $val) { if (\is_array($val)) { - $result[] = static::create($val)->reduce_dimension($unique)->toArray(); + $result[] = static::create($val)->reduce_dimension($unique)->toArray(); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } else { $result[] = [$val]; } @@ -5570,7 +5572,7 @@ public function reduce_dimension(bool $unique = true): self $result = $result === [] ? [] : \array_merge(...$result); - $resultArrayy = static::create($result); + $resultArrayy = static::create($result); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) /** * @psalm-suppress ImpureMethodCall - object is already re-created @@ -5631,7 +5633,7 @@ public function reject(\Closure $closure): self } return static::create( - $filtered, + $filtered, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5661,7 +5663,7 @@ public function remove($key) } return static::create( - $this->toArray(), + $this->toArray(), // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5670,7 +5672,7 @@ public function remove($key) $this->internalRemove($key); return static::create( - $this->toArray(), + $this->toArray(), // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5713,7 +5715,7 @@ public function removeFirst(): self \array_shift($tmpArray); return static::create( - $tmpArray, + $tmpArray, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5739,7 +5741,7 @@ public function removeLast(): self \array_pop($tmpArray); return static::create( - $tmpArray, + $tmpArray, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5779,7 +5781,7 @@ public function removeValue($value): self } return static::create( - $this->array, + $this->array, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5799,11 +5801,11 @@ public function removeValue($value): self public function repeat($times): self { if ($times === 0) { - return static::create([], $this->iteratorClass); + return static::create([], $this->iteratorClass); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } return static::create( - \array_fill(0, (int) $times, $this->toArray()), + \array_fill(0, (int) $times, $this->toArray()), // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5872,14 +5874,11 @@ public function replace($oldKey, $newKey, $newValue): self */ public function replaceAllKeys(array $keys): self { - $data = \array_combine($keys, $this->toArray()); - /* @phpstan-ignore identical.alwaysFalse */ - if ($data === false) { - $data = []; - } + $values = $this->toArray(); + $data = \count($keys) === \count($values) ? \array_combine($keys, $values) : []; return static::create( - $data, + $data, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5916,14 +5915,11 @@ public function replaceAllKeys(array $keys): self */ public function replaceAllValues(array $array): self { - $data = \array_combine($this->toArray(), $array); - /* @phpstan-ignore identical.alwaysFalse */ - if ($data === false) { - $data = []; - } + $keys = $this->toArray(); + $data = \count($keys) === \count($array) ? \array_combine($keys, $array) : []; return static::create( - $data, + $data, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5948,14 +5944,10 @@ public function replaceAllValues(array $array): self public function replaceKeys(array $keys): self { $values = \array_values($this->toArray()); - $result = \array_combine($keys, $values); - /* @phpstan-ignore identical.alwaysFalse */ - if ($result === false) { - $result = []; - } + $result = \count($keys) === \count($values) ? \array_combine($keys, $values) : []; return static::create( - $result, + $result, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -5990,7 +5982,7 @@ public function replaceOneValue($search, $replacement = ''): self } return static::create( - $array, + $array, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -6019,8 +6011,7 @@ public function replaceValues($search, $replacement = ''): self return \str_replace($search, $replacement, $value); }; - /* @phpstan-ignore argument.type */ - return $this->each($callable); + return $this->each($callable); // @phpstan-ignore return.type (the replacement callback intentionally changes values while preserving the collection class) } /** @@ -6043,7 +6034,7 @@ public function rest(int $from = 1): self $tmpArray = $this->toArray(); return static::create( - \array_splice($tmpArray, $from), + \array_splice($tmpArray, $from), // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -6194,7 +6185,7 @@ public function searchValue($index): self if ($this->array === []) { return static::create( - [], + [], // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -6211,7 +6202,7 @@ public function searchValue($index): self } return static::create( - $return, + $return, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -6343,7 +6334,7 @@ public function shuffle(bool $secure = false, ?array $array = null): self } return static::create( - $array, + $array, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -6514,7 +6505,7 @@ public function sizeRecursive(): int public function slice(int $offset, ?int $length = null, bool $preserveKeys = false) { return static::create( - \array_slice( + \array_slice( // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->toArray(), $offset, $length, @@ -6730,7 +6721,7 @@ public function sorter($sorter = null, $direction = \SORT_ASC, int $strategy = \ // Transform all values into their results. if ($sorter) { $arrayy = static::create( - $array, + $array, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -6758,7 +6749,7 @@ static function ($value) use ($sorter) { \array_multisort($results, $direction, $strategy, $array); return static::create( - $array, + $array, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -6788,7 +6779,7 @@ public function splice(int $offset, ?int $length = null, $replacement = []): sel ); return static::create( - $tmpArray, + $tmpArray, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -6924,7 +6915,7 @@ public function swap($swapA, $swapB): self list($array[$swapA], $array[$swapB]) = [$array[$swapB], $array[$swapA]]; return static::create( - $array, + $array, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -6970,8 +6961,7 @@ public function toArray( } } - /* @phpstan-ignore return.type */ - return $array; + return $array; // @phpstan-ignore return.type (recursive Arrayy conversion produces the documented runtime array shape) } return \iterator_to_array($this->getGenerator(), $preserveKeys); @@ -7061,7 +7051,7 @@ public function toPermutation(?array $items = null, array $helper = []): self /** @var static $return - help for phpstan */ $return = static::create( - $return, + $return, // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $this->iteratorClass, false ); @@ -7321,7 +7311,7 @@ public function where(string $keyOrPropertyOrMethod, $value): self return $this->filter( function ($item) use ($keyOrPropertyOrMethod, $value) { $accessorValue = $this->extractValue( - $item, + $item, // @phpstan-ignore-line argument.type (filter() does not retain the item type in this callback) $keyOrPropertyOrMethod ); @@ -7451,7 +7441,7 @@ protected function array_keys_recursive( * * @return void * - * @phpstan-param array|null $currentOffset + * @phpstan-param array|null $currentOffset * @psalm-mutation-free */ protected function callAtPath($path, $callable, &$currentOffset = null) @@ -7463,10 +7453,6 @@ protected function callAtPath($path, $callable, &$currentOffset = null) } $explodedPath = \explode($this->pathSeparator, $path); - /* @phpstan-ignore identical.alwaysFalse */ - if ($explodedPath === false) { - return; - } $nextPath = \array_shift($explodedPath); if (!isset($currentOffset[$nextPath])) { @@ -7474,10 +7460,15 @@ protected function callAtPath($path, $callable, &$currentOffset = null) } if ($explodedPath !== []) { + if (!\is_array($currentOffset[$nextPath])) { + return; + } + + $nestedOffset = &$currentOffset[$nextPath]; $this->callAtPath( \implode($this->pathSeparator, $explodedPath), $callable, - $currentOffset[$nextPath] + $nestedOffset ); } else { $callable($currentOffset[$nextPath]); @@ -7487,7 +7478,7 @@ protected function callAtPath($path, $callable, &$currentOffset = null) /** * Extracts the value of the given property or method from the object. * - * @param static $object + * @param array|object $object *

The object to extract the value from.

* @param string $keyOrPropertyOrMethod *

The property or method for which the @@ -7498,11 +7489,15 @@ protected function callAtPath($path, $callable, &$currentOffset = null) * @return mixed *

The value extracted from the specified property or method.

* - * @phpstan-param self $object + * @phpstan-param array|object $object */ - final protected function extractValue(self $object, string $keyOrPropertyOrMethod) + final protected function extractValue($object, string $keyOrPropertyOrMethod) { - if (isset($object[$keyOrPropertyOrMethod])) { + if (\is_array($object)) { + if (\array_key_exists($keyOrPropertyOrMethod, $object)) { + return $object[$keyOrPropertyOrMethod]; + } + } elseif ($object instanceof self && isset($object[$keyOrPropertyOrMethod])) { $return = $object->get($keyOrPropertyOrMethod); if ($return instanceof self) { @@ -7512,11 +7507,11 @@ final protected function extractValue(self $object, string $keyOrPropertyOrMetho return $return; } - if (\property_exists($object, $keyOrPropertyOrMethod)) { + if (\is_object($object) && \property_exists($object, $keyOrPropertyOrMethod)) { return $object->{$keyOrPropertyOrMethod}; } - if (\method_exists($object, $keyOrPropertyOrMethod)) { + if (\is_object($object) && \method_exists($object, $keyOrPropertyOrMethod)) { return $object->{$keyOrPropertyOrMethod}(); } @@ -8027,6 +8022,10 @@ protected function internalRemove($key): bool { $this->generatorToArray(); + if (\is_float($key)) { + $key = (int) $key; + } + if ( $this->pathSeparator && @@ -8035,18 +8034,28 @@ protected function internalRemove($key): bool \strpos($key, $this->pathSeparator) !== false ) { $path = \explode($this->pathSeparator, (string) $key); + $array = &$this->array; + // crawl though the keys while (\count($path, \COUNT_NORMAL) > 1) { $key = \array_shift($path); - if (!$this->has($key)) { + if (!\is_array($array) || !\array_key_exists($key, $array)) { return false; } - $this->array = &$this->array[$key]; + $array = &$array[$key]; } $key = \array_shift($path); + + if (!\is_array($array)) { + return false; + } + + unset($array[$key]); + + return true; } unset($this->array[$key]); diff --git a/src/Collection/AbstractCollection.php b/src/Collection/AbstractCollection.php index 58fbcd1..a5b7148 100644 --- a/src/Collection/AbstractCollection.php +++ b/src/Collection/AbstractCollection.php @@ -317,11 +317,11 @@ public static function createFromJsonMapper(string $json) if (\is_array($jsonObject)) { foreach ($jsonObject as $jsonObjectSingle) { $collectionData = $mapper->map($jsonObjectSingle, $type); - $return->add($collectionData); + $return->add($collectionData); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } } else { $collectionData = $mapper->map($jsonObject, $type); - $return->add($collectionData); + $return->add($collectionData); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } } else { foreach ($jsonObject as $key => $jsonValue) { diff --git a/src/Create.php b/src/Create.php index 9241591..5f5cc91 100644 --- a/src/Create.php +++ b/src/Create.php @@ -17,7 +17,7 @@ */ function create($data): Arrayy { - return new Arrayy($data); + return new Arrayy($data); // @phpstan-ignore return.type (the convenience factory deliberately accepts mixed input) } } diff --git a/src/Mapper/Json.php b/src/Mapper/Json.php index a5724e0..59de56b 100644 --- a/src/Mapper/Json.php +++ b/src/Mapper/Json.php @@ -19,7 +19,7 @@ final class Json * Override class names that JsonMapper uses to create objects. * Useful when your setter methods accept abstract classes or interfaces. * - * @var array + * @var array */ public $classMap = []; @@ -33,7 +33,7 @@ final class Json * 2. Name of the unknown JSON property * 3. JSON value of the property * - * @var callable + * @var callable|null */ public $undefinedPropertyHandler; @@ -41,14 +41,14 @@ final class Json * Runtime cache for inspected classes. This is particularly effective if * mapArray() is called with a large number of objects * - * @var array property inspection result cache + * @var array> property inspection result cache */ private $arInspectedClasses = []; /** * Map data all data in $json into the given $object instance. * - * @param object|iterable $json + * @param object|iterable $json *

JSON object structure from json_decode()

* @param object|string $object *

Object to map $json data into

@@ -58,7 +58,7 @@ final class Json * * @see mapArray() * - * @template TObject + * @template TObject of object * @phpstan-param TObject|class-string $object *

Object to map $json data into.

* @phpstan-return TObject @@ -79,7 +79,10 @@ public function map($json, $object) $strClassName = \get_class($object); $rc = new \ReflectionClass($object); $strNs = $rc->getNamespaceName(); - foreach ($json as $key => $jsonValue) { + $jsonValues = $json instanceof \Traversable + ? $json + : (\is_object($json) ? \get_object_vars($json) : $json); + foreach ($jsonValues as $key => $jsonValue) { $key = $this->getSafeName($key); // Store the property inspection results, so we don't have to do it @@ -247,7 +250,7 @@ public function map($json, $object) /** * Map an array * - * @param array $json JSON array structure from json_decode() + * @param array $json JSON array structure from json_decode() * @param mixed $array Array or ArrayObject that gets filled with * data from $json * @param string|null $class Class name for children objects. @@ -302,7 +305,7 @@ public function mapArray($json, $array, $class = null, $parent_key = '') && \count($typesTmp->getTypes()) === 1 ) { - $array[$key] = $this->map($jsonValue, $typesTmp->getTypes()[0]); + $array[$key] = $this->map($jsonValue, $typesTmp->getTypes()[0]); // @phpstan-ignore-line argument.templateType, argument.type (runtime PHPDoc type strings are validated by map()) $foundArrayy = true; break; @@ -404,7 +407,7 @@ private function getFullNamespace($type, $strNs) * @param \ReflectionClass $rc Reflection class to check * @param string $name Property name * - * @return array First value: if the property exists + * @return array{bool,string|\ReflectionMethod|\ReflectionProperty|null,string|null} First value: if the property exists * Second value: the accessor to use ( * Array-Key-String or ReflectionMethod or ReflectionProperty, or null) * Third value: type of the property @@ -485,7 +488,7 @@ private function inspectProperty(\ReflectionClass $rc, $name): array * * @param string $docblock Full method docblock * - * @return array + * @return array> */ private static function parseAnnotations($docblock): array { @@ -692,13 +695,13 @@ private function isArrayOfType($strType): bool /** * Checks if the given type is nullable * - * @param string $type type name from the phpdoc param + * @param string|null $type type name from the phpdoc param * * @return bool True if it is nullable */ private function isNullable($type): bool { - return \stripos('|' . $type . '|', '|null|') !== false; + return $type !== null && \stripos('|' . $type . '|', '|null|') !== false; } /** @@ -739,7 +742,7 @@ private function removeNullable($type) * * @internal * - * @template TClass + * @template TClass of object * @phpstan-param TClass|class-string $class * @phpstan-return TClass */ diff --git a/src/PHPStan/DefaultDotNotationTypeInterface.php b/src/PHPStan/DefaultDotNotationTypeInterface.php new file mode 100644 index 0000000..53adfc5 --- /dev/null +++ b/src/PHPStan/DefaultDotNotationTypeInterface.php @@ -0,0 +1,15 @@ +getName() === 'get'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type + { + if (!isset($methodCall->args[0]) || !$methodCall->args[0] instanceof Arg) { + return null; + } + + $pathType = $scope->getType($methodCall->args[0]->value); + $paths = $pathType->getConstantStrings(); + if ( + \count($paths) !== 1 + || !\str_contains($paths[0]->getValue(), '.') + || \str_contains($paths[0]->getValue(), '*') + ) { + return null; + } + + $dataType = $this->getArrayyDataType($scope->getType($methodCall->var)); + if ($dataType === null || !$dataType->isConstantArray()->yes()) { + return null; + } + + $valueType = $dataType; + foreach (\explode('.', $paths[0]->getValue()) as $segment) { + $valueType = TypeCombinator::removeNull($valueType); + $nestedDataType = $this->getArrayyDataType($valueType); + if ($nestedDataType !== null) { + $valueType = $nestedDataType; + } + + $offsetType = new ConstantStringType($segment); + if (!$valueType->isOffsetAccessible()->yes() || $valueType->hasOffsetValueType($offsetType)->no()) { + return $this->getFallbackType($methodCall, $scope); + } + + $valueType = $valueType->getOffsetValueType($offsetType); + } + + if (!$valueType->isArray()->yes() && $valueType->getArrays() !== []) { + // Arrayy wraps array results in an Arrayy instance. If the shape is a + // union of arrays and scalars, the native method type is safer than + // pretending the runtime wrapper is a scalar. + return null; + } + + if ($valueType->isArray()->yes()) { + $valueType = new GenericObjectType( + Arrayy::class, + [$valueType->getIterableKeyType(), $valueType->getIterableValueType(), $valueType] + ); + } + + return TypeCombinator::union($valueType, $this->getFallbackType($methodCall, $scope)); + } + + private function getArrayyDataType(Type $type): ?Type + { + foreach ($type->getObjectClassReflections() as $classReflection) { + if ($classReflection->getName() === Arrayy::class) { + // Plain Arrayy instances freely change shape at runtime. Typed + // subclasses provide the stable TData contract needed here. + continue; + } + + if ($classReflection->getAncestorWithClassName(DefaultDotNotationTypeInterface::class) === null) { + // The separator is mutable runtime state. Only an explicit + // default-separator contract makes splitting on "." sound. + continue; + } + + $arrayyReflection = $this->getArrayyReflection($classReflection); + if ($arrayyReflection === null) { + continue; + } + + $dataType = $arrayyReflection->getActiveTemplateTypeMap()->getType('TData'); + if ($dataType !== null) { + return $dataType; + } + } + + return null; + } + + private function getArrayyReflection(ClassReflection $classReflection): ?ClassReflection + { + if ($classReflection->getName() === Arrayy::class) { + return $classReflection; + } + + return $classReflection->getAncestorWithClassName(Arrayy::class); + } + + private function getFallbackType(MethodCall $methodCall, Scope $scope): Type + { + if (!isset($methodCall->args[1]) || !$methodCall->args[1] instanceof Arg) { + return new NullType(); + } + + $fallbackType = $scope->getType($methodCall->args[1]->value); + + return $fallbackType instanceof NeverType ? new NullType() : $fallbackType; + } +} diff --git a/src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php b/src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php index feb0c21..8ccfe92 100644 --- a/src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php +++ b/src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php @@ -38,7 +38,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, } $className = $scope->resolveName($methodCall->class); - if (!\is_a($className, Arrayy::class, true)) { + if (!\is_a($className, Arrayy::class, true)) { // @phpstan-ignore-line phpstanApi.runtimeReflection (the extension requires runtime reflection because PHPStan does not expose this check as a stable API) return null; } diff --git a/src/Type/DetectFirstValueTypeCollection.php b/src/Type/DetectFirstValueTypeCollection.php index b0693a1..a6bf175 100644 --- a/src/Type/DetectFirstValueTypeCollection.php +++ b/src/Type/DetectFirstValueTypeCollection.php @@ -40,7 +40,7 @@ public function __construct( */ if ($data instanceof Arrayy) { $firstValue = $data->first(); - } elseif (\is_array($data)) { + } elseif (\is_array($data)) { // @phpstan-ignore-line function.alreadyNarrowedType (the runtime guard is retained for inputs from supported PHP versions and data providers) $firstValue = array_first($data); } else { $firstValue = $data; diff --git a/src/TypeCheck/TypeCheckCallback.php b/src/TypeCheck/TypeCheckCallback.php index 8600770..6cc8b37 100644 --- a/src/TypeCheck/TypeCheckCallback.php +++ b/src/TypeCheck/TypeCheckCallback.php @@ -51,7 +51,7 @@ public function checkType(&$value): bool } /** - * @return array + * @return array */ public function getTypes(): array { diff --git a/src/TypeCheck/TypeCheckPhpDoc.php b/src/TypeCheck/TypeCheckPhpDoc.php index 43fbbe7..d58bc6e 100644 --- a/src/TypeCheck/TypeCheckPhpDoc.php +++ b/src/TypeCheck/TypeCheckPhpDoc.php @@ -14,11 +14,6 @@ */ final class TypeCheckPhpDoc extends AbstractTypeCheck implements TypeCheckInterface { - /** - * @var bool - */ - private $hasTypeDeclaration = false; - /** * @var string */ @@ -68,8 +63,6 @@ public static function fromDocTypeObject(string $property, $type) $tmpReflection = new self($property); if ($type) { - $tmpReflection->hasTypeDeclaration = true; - $docTypes = self::parseDocTypeObject($type); if (\is_array($docTypes) === true) { foreach ($docTypes as $docType) { @@ -94,8 +87,6 @@ public static function fromReflectionProperty(\ReflectionProperty $reflectionPro $docTypes = self::getTypesFromReflectionPropertyDocBlock($reflectionProperty); if ($docTypes !== null) { - $tmpReflection->hasTypeDeclaration = true; - if (\is_array($docTypes) === true) { foreach ($docTypes as $docType) { $tmpReflection->types[] = $docType; @@ -109,8 +100,6 @@ public static function fromReflectionProperty(\ReflectionProperty $reflectionPro return $tmpReflection; } else { - $tmpReflection->hasTypeDeclaration = true; - $docTypes = self::parseReflectionTypeObject($type); if (\is_array($docTypes) === true) { foreach ($docTypes as $docType) { @@ -243,15 +232,12 @@ private static function getScalarPseudoTypeClasses(): array { $classes = []; - foreach ( - [ - '\phpDocumentor\Reflection\PseudoTypes\Scalar', - '\phpDocumentor\Reflection\Types\Scalar', - ] as $className - ) { - if (\class_exists($className)) { - $classes[] = $className; - } + if (\class_exists(\phpDocumentor\Reflection\PseudoTypes\Scalar::class)) { + $classes[] = \phpDocumentor\Reflection\PseudoTypes\Scalar::class; + } + + if (\class_exists(\phpDocumentor\Reflection\Types\Scalar::class)) { + $classes[] = \phpDocumentor\Reflection\Types\Scalar::class; } return $classes; diff --git a/tests/Account.php b/tests/Account.php index 88ae8c8..33d2917 100644 --- a/tests/Account.php +++ b/tests/Account.php @@ -7,7 +7,7 @@ */ class Account { - public function __construct($accountName) + public function __construct(string $accountName) { $this->accountName = $accountName; } diff --git a/tests/ArrayyTest.php b/tests/ArrayyTest.php index 6eea6f7..0508296 100644 --- a/tests/ArrayyTest.php +++ b/tests/ArrayyTest.php @@ -19,7 +19,7 @@ final class ArrayyTest extends \PHPUnit\Framework\TestCase const TYPE_NUMERIC = 'numeric'; /** - * @return array + * @return array */ public function appendProvider(): array { @@ -45,7 +45,7 @@ public function appendProvider(): array } /** - * @return array + * @return array */ public function appendToEachKeyProvider(): array { @@ -114,7 +114,7 @@ public function appendToEachKeyProvider(): array } /** - * @return array + * @return array */ public function appendToEachValueProvider(): array { @@ -194,7 +194,7 @@ public static function assertArrayy($actual): void } /** - * @return array + * @return array */ public function averageProvider(): array { @@ -213,37 +213,23 @@ public function averageProvider(): array } /** - * @return array + * @return array */ public function cleanProvider(): array { - // breaking-change from PHP8 - // -> Implement the negative_array_index RFC: https://github.com/php/php-src/commit/6732028273b109cb342387ab5580c367f629d0ac - if (\PHP_VERSION_ID >= 80000) { - return [ - [[], []], - [[null, false], []], - [[0 => true], [0 => true]], - [[0 => -9, 0], [0 => -9]], - [[-8 => -9, 1, 2 => false], [-8 => -9, -7 => 1]], - [[0 => 1.18, 1 => false], [0 => 1.18]], - [['foo' => false, 'foo', 'lall'], ['foo', 'lall']], - ]; - } - return [ [[], []], [[null, false], []], [[0 => true], [0 => true]], [[0 => -9, 0], [0 => -9]], - [[-8 => -9, 1, 2 => false], [-8 => -9, 0 => 1]], + [[-8 => -9, 1, 2 => false], [-8 => -9, -7 => 1]], [[0 => 1.18, 1 => false], [0 => 1.18]], [['foo' => false, 'foo', 'lall'], ['foo', 'lall']], ]; } /** - * @return array + * @return array */ public function containsCaseInsensitiveProvider(): array { @@ -265,7 +251,7 @@ public function containsCaseInsensitiveProvider(): array } /** - * @return array + * @return array */ public function containsCaseInsensitiveProviderRecursive(): array { @@ -287,7 +273,7 @@ public function containsCaseInsensitiveProviderRecursive(): array } /** - * @return array + * @return array */ public function containsOnlyProvider(): array { @@ -306,7 +292,7 @@ public function containsOnlyProvider(): array } /** - * @return array + * @return array */ public function containsProvider(): array { @@ -324,7 +310,7 @@ public function containsProvider(): array } /** - * @return array + * @return array */ public function containsProviderRecursive(): array { @@ -342,7 +328,7 @@ public function containsProviderRecursive(): array } /** - * @return array + * @return array */ public function countProvider(): array { @@ -361,7 +347,7 @@ public function countProvider(): array } /** - * @return array + * @return array */ public function countProviderRecursive(): array { @@ -380,7 +366,7 @@ public function countProviderRecursive(): array } /** - * @return array + * @return array */ public function diffProvider(): array { @@ -454,7 +440,7 @@ public function diffProvider(): array } /** - * @return array + * @return array */ public function diffKeyProvider(): array { @@ -527,7 +513,7 @@ public function diffKeyProvider(): array } /** - * @return array + * @return array */ public function diffKeyAndValueProvider(): array { @@ -604,7 +590,7 @@ public function diffKeyAndValueProvider(): array } /** - * @return array + * @return array */ public function diffReverseProvider(): array { @@ -679,7 +665,7 @@ public function diffReverseProvider(): array } /** - * @return array + * @return array */ public function fillWithDefaultsProvider(): array { @@ -695,7 +681,7 @@ public function fillWithDefaultsProvider(): array } /** - * @return array + * @return array */ public function findProvider(): array { @@ -711,7 +697,7 @@ public function findProvider(): array } /** - * @return array + * @return array */ public function firstProvider(): array { @@ -731,7 +717,7 @@ public function firstProvider(): array } /** - * @return array + * @return array */ public function firstsProvider(): array { @@ -751,7 +737,7 @@ public function firstsProvider(): array } /** - * @return array + * @return array */ public function getProvider(): array { @@ -769,7 +755,7 @@ public function getProvider(): array } /** - * @return array + * @return array */ public function hasProvider(): array { @@ -789,7 +775,7 @@ public function hasProvider(): array } /** - * @return array + * @return array */ public function implodeKeysProvider(): array { @@ -813,7 +799,7 @@ public function implodeKeysProvider(): array } /** - * @return array + * @return array */ public function implodeProvider(): array { @@ -837,7 +823,7 @@ public function implodeProvider(): array } /** - * @return array + * @return array */ public function initialProvider(): array { @@ -858,7 +844,7 @@ public function initialProvider(): array } /** - * @return array + * @return array */ public function isAssocProvider(): array { @@ -877,7 +863,7 @@ public function isAssocProvider(): array } /** - * @return array + * @return array */ public function isMultiArrayProvider(): array { @@ -899,7 +885,7 @@ public function isMultiArrayProvider(): array } /** - * @return array + * @return array */ public function lastProvider(): array { @@ -921,7 +907,7 @@ public function lastProvider(): array } /** - * @return array + * @return array */ public function matchesAnyProvider(): array { @@ -943,7 +929,7 @@ public function matchesAnyProvider(): array } /** - * @return array + * @return array */ public function matchesProvider(): array { @@ -966,7 +952,7 @@ public function matchesProvider(): array } /** - * @return array + * @return array */ public function maxProvider(): array { @@ -985,7 +971,7 @@ public function maxProvider(): array } /** - * @return array + * @return array */ public function mergeAppendKeepIndexProvider(): array { @@ -1077,7 +1063,7 @@ public function mergeAppendKeepIndexProvider(): array } /** - * @return array + * @return array */ public function mergeAppendNewIndexProvider(): array { @@ -1175,7 +1161,7 @@ public function mergeAppendNewIndexProvider(): array } /** - * @return array + * @return array */ public function mergePrependKeepIndexProvider(): array { @@ -1267,7 +1253,7 @@ public function mergePrependKeepIndexProvider(): array } /** - * @return array + * @return array */ public function mergePrependNewIndexProvider(): array { @@ -1365,7 +1351,7 @@ public function mergePrependNewIndexProvider(): array } /** - * @return array + * @return array */ public function minProvider(): array { @@ -1384,7 +1370,7 @@ public function minProvider(): array } /** - * @return array + * @return array */ public function prependProvider(): array { @@ -1409,7 +1395,7 @@ public function prependProvider(): array } /** - * @return array + * @return array */ public function prependToEachKeyProvider(): array { @@ -1478,7 +1464,7 @@ public function prependToEachKeyProvider(): array } /** - * @return array + * @return array */ public function prependToEachValueProvider(): array { @@ -1547,7 +1533,7 @@ public function prependToEachValueProvider(): array } /** - * @return array + * @return array */ public function randomProvider(): array { @@ -1565,7 +1551,7 @@ public function randomProvider(): array } /** - * @return array + * @return array */ public function randomWeightedProvider(): array { @@ -1582,7 +1568,7 @@ public function randomWeightedProvider(): array } /** - * @return array + * @return array */ public function removeFirstProvider(): array { @@ -1598,7 +1584,7 @@ public function removeFirstProvider(): array } /** - * @return array + * @return array */ public function removeLastProvider(): array { @@ -1614,7 +1600,7 @@ public function removeLastProvider(): array } /** - * @return array + * @return array */ public function removeProvider(): array { @@ -1633,7 +1619,7 @@ public function removeProvider(): array } /** - * @return array + * @return array */ public function removeV2Provider(): array { @@ -1649,7 +1635,7 @@ public function removeV2Provider(): array } /** - * @return array + * @return array */ public function removeValueProvider(): array { @@ -1666,7 +1652,7 @@ public function removeValueProvider(): array } /** - * @return array + * @return array */ public function restProvider(): array { @@ -1686,7 +1672,7 @@ public function restProvider(): array } /** - * @return array + * @return array */ public function reverseProvider(): array { @@ -1710,7 +1696,7 @@ public function reverseProvider(): array } /** - * @return array + * @return array */ public function searchIndexProvider(): array { @@ -1726,7 +1712,7 @@ public function searchIndexProvider(): array } /** - * @return array + * @return array */ public function searchValueProvider(): array { @@ -1744,7 +1730,7 @@ public function searchValueProvider(): array } /** - * @return array + * @return array */ public function setAndGetProvider(): array { @@ -1763,7 +1749,7 @@ public function setAndGetProvider(): array } /** - * @return array + * @return array */ public function setProvider(): array { @@ -1782,7 +1768,7 @@ public function setProvider(): array } /** - * @return array + * @return array */ public function simpleArrayProvider(): array { @@ -1823,7 +1809,7 @@ public function simpleArrayProvider(): array } /** - * @return array + * @return array */ public function reduceDimensionProvider(): array { @@ -1846,7 +1832,7 @@ public function reduceDimensionProvider(): array } /** - * @return array + * @return array */ public function sortKeysProvider(): array { @@ -1864,7 +1850,7 @@ public function sortKeysProvider(): array } /** - * @return array + * @return array */ public function stringWithSeparatorProvider(): array { @@ -1895,7 +1881,7 @@ public function testAdd(): void $resultArrayy = $arrayy->add(3); $array[] = 3; - self::assertMutable($arrayy, $resultArrayy, $array); + self::assertMutable($arrayy, $resultArrayy, $array); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } public function testAppendImmutableYield(): void @@ -1906,7 +1892,7 @@ public function testAppendImmutableYield(): void $arrayResult = $array; $arrayResult[] = 3; - self::assertImmutable($arrayy, $resultArrayy, $array, $arrayResult); + self::assertImmutable($arrayy, $resultArrayy, $array, $arrayResult); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } public function testPrependImmutableYield(): void @@ -1916,14 +1902,14 @@ public function testPrependImmutableYield(): void $resultArrayy = $arrayy->prependImmutable(3); $arrayResult = [3, 3 => 1, 2]; - self::assertImmutable($arrayy, $resultArrayy, $array, $arrayResult); + self::assertImmutable($arrayy, $resultArrayy, $array, $arrayResult); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider appendProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result * @param mixed $value */ public function testAppend($array, $result, $value): void @@ -1936,8 +1922,8 @@ public function testAppend($array, $result, $value): void /** * @dataProvider appendToEachKeyProvider * - * @param array $array - * @param array $result + * @param array $array + * @param array $result */ public function testAppendToEachKey($array, $result): void { @@ -1949,8 +1935,8 @@ public function testAppendToEachKey($array, $result): void /** * @dataProvider appendToEachValueProvider * - * @param array $array - * @param array $result + * @param array $array + * @param array $result */ public function testAppendToEachValue($array, $result): void { @@ -1962,7 +1948,7 @@ public function testAppendToEachValue($array, $result): void /** * @dataProvider averageProvider() * - * @param array $array + * @param array $array * @param mixed $value * @param float|int $expected */ @@ -2156,7 +2142,7 @@ public function testChangeKeyCase(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testChunk(array $array): void { @@ -2164,7 +2150,7 @@ public function testChunk(array $array): void $resultArrayy = $arrayy->chunk(2); $resultArray = \array_chunk($array, 2); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) // --- @@ -2177,8 +2163,8 @@ public function testChunk(array $array): void /** * @dataProvider cleanProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result */ public function testClean($array, $result): void { @@ -2190,7 +2176,7 @@ public function testClean($array, $result): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testClear(array $array): void { @@ -2298,7 +2284,7 @@ public function testConstructWithArray(): void /** * @dataProvider containsOnlyProvider() * - * @param array $array + * @param array $array * @param mixed $value * @param bool $expected */ @@ -2312,7 +2298,7 @@ public function testContainsOnly($array, $value, $expected): void /** * @dataProvider containsProvider() * - * @param array $array + * @param array $array * @param mixed $value * @param bool $expected */ @@ -2328,7 +2314,7 @@ public function testContains($array, $value, $expected): void /** * @dataProvider containsCaseInsensitiveProvider() * - * @param array $array + * @param array $array * @param mixed $value * @param bool $expected */ @@ -2343,7 +2329,7 @@ public function testContainsCaseInsensitive($array, $value, $expected): void /** * @dataProvider containsCaseInsensitiveProviderRecursive() * - * @param array $array + * @param array $array * @param mixed $value * @param bool $expected */ @@ -2416,7 +2402,7 @@ public function testContainsKeysRecursive(): void /** * @dataProvider containsProviderRecursive() * - * @param array $array + * @param array $array * @param mixed $value * @param bool $expected */ @@ -2440,7 +2426,7 @@ public function testContainsValues(): void /** * @dataProvider countProvider() * - * @param array $array + * @param array $array * @param int $expected */ public function testCount($array, $expected): void @@ -2457,7 +2443,7 @@ public function testCount($array, $expected): void /** * @dataProvider countProviderRecursive() * - * @param array $array + * @param array $array * @param int $expected */ public function testCountRecursive($array, $expected): void @@ -2576,13 +2562,13 @@ public function testCreateFromString($string, $separator): void } else { $array = [$string]; } - \assert(\is_array($array)); + \assert(\is_array($array)); // @phpstan-ignore-line function.alreadyNarrowedType (the runtime guard is retained for inputs from supported PHP versions and data providers) $arrayy = new A($array); $resultArrayy = A::createFromString($string, $separator); - self::assertImmutable($arrayy, $resultArrayy, $array, $array); + self::assertImmutable($arrayy, $resultArrayy, $array, $array); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } public function testCreateFromTraversableImmutable(): void @@ -2593,7 +2579,7 @@ public function testCreateFromTraversableImmutable(): void $resultArrayy = A::createFromTraversableImmutable($iterator); - self::assertImmutable($arrayy, $resultArrayy, $array, $array); + self::assertImmutable($arrayy, $resultArrayy, $array, $array); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } public function testCreateFromStringRegEx(): void @@ -2650,7 +2636,7 @@ public function testCreateWithRange(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testCustomSort(array $array): void { @@ -2673,7 +2659,7 @@ public function testCustomSort(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testCustomSortImmutable(array $array): void { @@ -2690,13 +2676,13 @@ public function testCustomSortImmutable(array $array): void $resultArray = $array; \usort($resultArray, $callable); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testCustomSortKeys(array $array): void { @@ -2780,7 +2766,7 @@ public function testCustomSortKeysSimple(): void 'two' => 2, ]; static::assertSame($expected, $resultArrayy->getArray()); - self::assertImmutable($arrayy, $resultArrayy, $input, $resultArrayy->getArray()); + self::assertImmutable($arrayy, $resultArrayy, $input, $resultArrayy->getArray()); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } public function testCustomSortValuesByDateTimeObject(): void @@ -2808,8 +2794,8 @@ public function testCustomSortValuesByDateTimeObject(): void /** * sort by date - helper-function * - * @param array $a - * @param array $b + * @param array $a + * @param array $b * * @return int */ @@ -2829,10 +2815,10 @@ public function testCustomSortValuesByDateTimeObject(): void /** * reduce by date - helper-function * - * @param array $resultArray - * @param array $value + * @param array $resultArray + * @param array $value * - * @return array + * @return array */ $closureReduce = static function ($resultArray, $value) use ($currentDate) { /* @var $valueDate \DateTime */ @@ -2857,8 +2843,8 @@ public function testCustomSortValuesByDateTimeObject(): void /* @var $resultMatch Arrayy|Arrayy[] */ $resultMatch = $birthDatesAraayy->reduce($closureReduce); - $thisYear = $resultMatch['thisYear']->customSortValues($closureSort); - $nextYear = $resultMatch['nextYear']->customSortValues($closureSort); + $thisYear = $resultMatch['thisYear']->customSortValues($closureSort); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + $nextYear = $resultMatch['nextYear']->customSortValues($closureSort); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) $resultMatch = $nextYear->reverse()->mergePrependNewIndex($thisYear->reverse()->getArray()); @@ -2894,9 +2880,9 @@ public function testCustomSortValuesByDateTimeObject(): void /** * @dataProvider diffProvider() * - * @param array $array - * @param array $arrayNew - * @param array $result + * @param array $array + * @param array $arrayNew + * @param array $result */ public function testDiff($array, $arrayNew, $result): void { @@ -2908,9 +2894,9 @@ public function testDiff($array, $arrayNew, $result): void /** * @dataProvider diffKeyProvider() * - * @param array $array - * @param array $arrayNew - * @param array $result + * @param array $array + * @param array $arrayNew + * @param array $result */ public function testDiffKey($array, $arrayNew, $result): void { @@ -2922,9 +2908,9 @@ public function testDiffKey($array, $arrayNew, $result): void /** * @dataProvider diffKeyAndValueProvider() * - * @param array $array - * @param array $arrayNew - * @param array $result + * @param array $array + * @param array $arrayNew + * @param array $result */ public function testDiffKeyAndValue($array, $arrayNew, $result): void { @@ -3000,7 +2986,7 @@ public function testDiffRecursive(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testDiffWith(array $array): void { @@ -3014,7 +3000,7 @@ public function testDiffWith(array $array): void $resultArrayy = $arrayy->diff($secondArray); $resultArray = \array_diff($array, $secondArray); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } public function testDivide(): void @@ -3063,10 +3049,10 @@ public function testExchangeArray(): void /** * @dataProvider fillWithDefaultsProvider() * - * @param array $array + * @param array $array * @param int $num * @param mixed $default - * @param array $expected + * @param array $expected */ public function testFillWithDefaults($array, $num, $default, $expected): void { @@ -3139,7 +3125,7 @@ static function ($value) { $under = A::create([0 => 1, 1 => 2, 2 => 3, 3 => 4, 7 => 7])->filter( /* @phpstan-ignore argument.type */ static function ($key, $value): bool { - return ($value % 2 !== 0) && ($key & 2 !== 0); + return ($value % 2 !== 0) && ($key & 2 !== 0); // @phpstan-ignore-line notIdentical.alwaysTrue (the runtime assertion documents behavior for values broader than this statically narrowed fixture) }, \ARRAY_FILTER_USE_BOTH ); @@ -3172,17 +3158,17 @@ public function testFilterBy(): void $b = $arrayy->filterBy('name', 'baz'); static::assertCount(1, $b); /** @noinspection OffsetOperationsInspection */ - static::assertSame(2365, $b[0]['value']); + static::assertSame(2365, $b[0]['value']); // @phpstan-ignore-line offsetAccess.notFound (the test intentionally exercises dynamic ArrayAccess keys absent from the inferred fixture shape) $b = $arrayy->filterBy('name', ['baz']); static::assertCount(1, $b); /** @noinspection OffsetOperationsInspection */ - static::assertSame(2365, $b[0]['value']); + static::assertSame(2365, $b[0]['value']); // @phpstan-ignore-line offsetAccess.notFound (the test intentionally exercises dynamic ArrayAccess keys absent from the inferred fixture shape) $c = $arrayy->filterBy('value', 2468); static::assertCount(1, $c); /** @noinspection OffsetOperationsInspection */ - static::assertSame('primary', $c[0]['group']); + static::assertSame('primary', $c[0]['group']); // @phpstan-ignore-line offsetAccess.notFound (the test intentionally exercises dynamic ArrayAccess keys absent from the inferred fixture shape) $d = $arrayy->filterBy('group', 'primary'); static::assertCount(3, $d); @@ -3190,7 +3176,7 @@ public function testFilterBy(): void $e = $arrayy->filterBy('value', 2000, 'lt'); static::assertCount(1, $e); /** @noinspection OffsetOperationsInspection */ - static::assertSame(1468, $e[0]['value']); + static::assertSame(1468, $e[0]['value']); // @phpstan-ignore-line offsetAccess.notFound (the test intentionally exercises dynamic ArrayAccess keys absent from the inferred fixture shape) $e = $arrayy->filterBy('value', [2468, 2365], 'contains'); static::assertCount(2, $e); @@ -3207,7 +3193,7 @@ public function testFilterBy(): void /** * @dataProvider findProvider() * - * @param array $array + * @param array $array * @param mixed $search * @param false|mixed $result */ @@ -3224,7 +3210,7 @@ public function testFind($array, $search, $result): void } /** - * @return array + * @return array */ public function findKeyProvider(): array { @@ -3246,7 +3232,7 @@ public function findKeyProvider(): array /** * @dataProvider findKeyProvider() * - * @param array $array + * @param array $array * @param mixed $search * @param false|mixed $result */ @@ -3305,8 +3291,8 @@ public function testFindKeyForMinAndMaxValues(): void /** * @dataProvider firstProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result */ public function testFirst($array, $result): void { @@ -3318,8 +3304,8 @@ public function testFirst($array, $result): void /** * @dataProvider firstsProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result * @param null $take */ public function testFirsts($array, $result, $take = null): void @@ -3368,7 +3354,7 @@ public function testGet(): void * @dataProvider getProvider() * * @param mixed $expected - * @param array $array + * @param array $array * @param mixed $key */ public function testGetV2($expected, $array, $key): void @@ -3400,7 +3386,7 @@ public function testGetViaDotNotation(): void * @dataProvider hasProvider() * * @param mixed $expected - * @param array $array + * @param array $array * @param mixed $key */ public function testHas($expected, $array, $key): void @@ -3412,7 +3398,7 @@ public function testHas($expected, $array, $key): void /** * @dataProvider implodeProvider() * - * @param array $array + * @param array $array * @param string $result * @param string $with */ @@ -3426,7 +3412,7 @@ public function testImplode($array, $result, $with = ','): void /** * @dataProvider implodeKeysProvider() * - * @param array $array + * @param array $array * @param string $result * @param string $with */ @@ -3464,8 +3450,8 @@ public function testIndexByReturnSome(): void /** * @dataProvider initialProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result * @param int $to */ public function testInitial($array, $result, $to = 1): void @@ -3622,7 +3608,7 @@ public function testIsArrayMultidim(): void /** * @dataProvider isAssocProvider() * - * @param array $array + * @param array $array * @param bool $result */ public function testIsAssoc($array, $result): void @@ -3879,8 +3865,8 @@ public function testKeys(): void /** * @dataProvider lastProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result * @param null $take */ public function testLast($array, $result, $take = null): void @@ -3947,7 +3933,7 @@ public function testMagicSetViaDotNotation(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testMap(array $array): void { @@ -3957,7 +3943,7 @@ public function testMap(array $array): void $arrayy = new A($array); $resultArrayy = $arrayy->map($callable); $resultArray = \array_map($callable, $array); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) // --- @@ -3970,7 +3956,7 @@ public function testMap(array $array): void /* @phpstan-ignore argument.type */ $resultArrayy = $arrayy->map('str_repeat', false, 2); $resultArray = \array_map($callable, $array); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } public function testMapSimpleExample(): void @@ -3983,7 +3969,7 @@ public function testMapSimpleExample(): void /** * @dataProvider matchesProvider() * - * @param array $array + * @param array $array * @param mixed $search * @param bool $result */ @@ -4007,8 +3993,8 @@ public function testMatches($array, $search, $result): void /** * @dataProvider matchesAnyProvider() * - * @param array $array - * @param array $search + * @param array $array + * @param array $search * @param bool $result */ public function testMatchesAny($array, $search, $result): void @@ -4069,7 +4055,7 @@ public function testMatchesSimple(): void /** * @dataProvider maxProvider() * - * @param array $array + * @param array $array * @param mixed $expected */ public function testMax($array, $expected): void @@ -4127,9 +4113,9 @@ public function testMergeMethods(): void /** * @dataProvider mergeAppendKeepIndexProvider() * - * @param array $array - * @param array $arrayNew - * @param array $result + * @param array $array + * @param array $arrayNew + * @param array $result */ public function testMergeAppendKeepIndex($array, $arrayNew, $result): void { @@ -4141,9 +4127,9 @@ public function testMergeAppendKeepIndex($array, $arrayNew, $result): void /** * @dataProvider mergeAppendNewIndexProvider() * - * @param array $array - * @param array $arrayNew - * @param array $result + * @param array $array + * @param array $arrayNew + * @param array $result */ public function testMergeAppendNewIndex($array, $arrayNew, $result): void { @@ -4155,9 +4141,9 @@ public function testMergeAppendNewIndex($array, $arrayNew, $result): void /** * @dataProvider mergePrependKeepIndexProvider() * - * @param array $array - * @param array $arrayNew - * @param array $result + * @param array $array + * @param array $arrayNew + * @param array $result */ public function testMergePrependKeepIndex($array, $arrayNew, $result): void { @@ -4169,9 +4155,9 @@ public function testMergePrependKeepIndex($array, $arrayNew, $result): void /** * @dataProvider mergePrependNewIndexProvider() * - * @param array $array - * @param array $arrayNew - * @param array $result + * @param array $array + * @param array $arrayNew + * @param array $result */ public function testMergePrependNewIndex($array, $arrayNew, $result): void { @@ -4183,7 +4169,7 @@ public function testMergePrependNewIndex($array, $arrayNew, $result): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testMergePrependNewIndexV2(array $array): void { @@ -4197,13 +4183,13 @@ public function testMergePrependNewIndexV2(array $array): void $resultArrayy = $arrayy->mergePrependNewIndex($secondArray); $resultArray = \array_merge($secondArray, $array); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testMergeToRecursively(array $array): void { @@ -4217,13 +4203,13 @@ public function testMergeToRecursively(array $array): void $resultArrayy = $arrayy->mergePrependNewIndex($secondArray, true); $resultArray = \array_merge_recursive($secondArray, $array); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testMergeWith(array $array): void { @@ -4237,13 +4223,13 @@ public function testMergeWith(array $array): void $resultArrayy = $arrayy->mergeAppendNewIndex($secondArray); $resultArray = \array_merge($array, $secondArray); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testMergeWithRecursively(array $array): void { @@ -4257,13 +4243,13 @@ public function testMergeWithRecursively(array $array): void $resultArrayy = $arrayy->mergeAppendNewIndex($secondArray, true); $resultArray = \array_merge_recursive($array, $secondArray); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider minProvider() * - * @param array $array + * @param array $array * @param mixed $expected */ public function testMin($array, $expected): void @@ -4378,7 +4364,7 @@ public function testNested(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testOffsetNullSet(array $array): void { @@ -4395,7 +4381,7 @@ public function testOffsetNullSet(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testOffsetSet(array $array): void { @@ -4412,7 +4398,7 @@ public function testOffsetSet(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testOffsetUnset(array $array): void { @@ -4431,7 +4417,7 @@ public function testOffsetUnset(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testDeleteKey(array $array): void { @@ -4477,10 +4463,21 @@ public function testOffsetUnsetViaDotNotation(): void static::assertSame([0 => 'a', 'b' => []], $array); static::assertSame($array, $arrayy->toArray()); - static::assertFalse(isset($array[$offset])); + static::assertFalse(isset($array[$offset])); // @phpstan-ignore-line isset.offset, staticMethod.alreadyNarrowedType (the assertion verifies runtime removal even though the fixture shape already excludes the offset) static::assertFalse($arrayy->offsetExists($offset)); } + public function testDotNotationStopsAtScalarIntermediateValue(): void + { + $arrayy = new A(['user' => 'not-an-array']); + + static::assertFalse($arrayy->offsetExists('user.name')); + + $arrayy->offsetUnset('user.name'); + + static::assertSame(['user' => null], $arrayy->toArray()); + } + public function testOrderByKey(): void { $array = [ @@ -4579,7 +4576,7 @@ public function testOrderByValueNewIndex(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testPad(array $array): void { @@ -4587,13 +4584,13 @@ public function testPad(array $array): void $resultArrayy = $arrayy->pad(10, 5); $resultArray = \array_pad($array, 10, 5); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testPop(array $array): void { @@ -4609,8 +4606,8 @@ public function testPop(array $array): void /** * @dataProvider prependProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result * @param mixed $value */ public function testPrepend($array, $result, $value): void @@ -4656,8 +4653,8 @@ public function testPrependKey(): void /** * @dataProvider prependToEachKeyProvider * - * @param array $array - * @param array $result + * @param array $array + * @param array $result */ public function testPrependToEachKey($array, $result): void { @@ -4676,8 +4673,8 @@ public function testPrependToEachKey($array, $result): void /** * @dataProvider prependToEachValueProvider * - * @param array $array - * @param array $result + * @param array $array + * @param array $result */ public function testPrependToEachValue($array, $result): void { @@ -4689,7 +4686,7 @@ public function testPrependToEachValue($array, $result): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testPush(array $array): void { @@ -4707,7 +4704,7 @@ public function testPush(array $array): void /** * @dataProvider randomProvider() * - * @param array $array + * @param array $array * @param int|null $take */ public function testRandom($array, $take = null): void @@ -4760,7 +4757,7 @@ public function testRandomValues(): void /** * @dataProvider randomWeightedProvider() * - * @param array $array + * @param array $array * @param int|null $take */ public function testRandomWeighted($array, $take = null): void @@ -4794,10 +4791,10 @@ public function testReduceViaFunction(): void $testArray = ['foo', 2 => 'bar', 4 => 'lall']; /** - * @param array $resultArray + * @param array $resultArray * @param mixed $value * - * @return array + * @return array */ $myReducer = static function ($resultArray, $value): array { if ($value === 'foo') { @@ -4816,8 +4813,8 @@ public function testReduceViaFunction(): void /** * @dataProvider reduceDimensionProvider * - * @param array $array - * @param array $expected + * @param array $array + * @param array $expected * @param bool $unique */ public function testReduceDimension(array $array, array $expected, bool $unique = false): void @@ -4831,7 +4828,7 @@ public function testReduceDimension(array $array, array $expected, bool $unique /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testTestgetValues(array $array): void { @@ -4845,7 +4842,7 @@ public function testTestgetValues(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetValuesYield(array $array): void { @@ -4864,7 +4861,7 @@ public function testGetValuesYield(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetGetBackwardsGenerator(array $array): void { @@ -4883,7 +4880,7 @@ public function testGetGetBackwardsGenerator(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testReindex(array $array): void { @@ -4983,9 +4980,9 @@ static function ($value, $key) { /** * @dataProvider removeProvider() * - * @param array $array + * @param array $array * @param mixed $key - * @param array $result + * @param array $result */ public function testRemove($array, $key, $result): void { @@ -4994,11 +4991,72 @@ public function testRemove($array, $key, $result): void static::assertSame($result, $resultTmp); } + public function testRemoveWithDotNotationPreservesRootAndSiblingValues(): void + { + $arrayy = new A([ + 'user' => [ + 'profile' => [ + 'name' => 'Lars', + 'avatar' => 'avatar.png', + ], + 'active' => true, + ], + 'keep' => 'root value', + ]); + + $result = $arrayy->remove('user.profile.name'); + $expected = [ + 'user' => [ + 'profile' => ['avatar' => 'avatar.png'], + 'active' => true, + ], + 'keep' => 'root value', + ]; + + static::assertSame($expected, $arrayy->toArray()); + static::assertSame($expected, $result->toArray()); + } + + public function testUnsetWithDotNotationPreservesRootAndSiblingValues(): void + { + $arrayy = new A([ + 'user' => [ + 'profile' => [ + 'name' => 'Lars', + 'avatar' => 'avatar.png', + ], + ], + 'keep' => 'root value', + ]); + + unset($arrayy->{'user.profile.name'}); + + static::assertSame([ + 'user' => [ + 'profile' => ['avatar' => 'avatar.png'], + ], + 'keep' => 'root value', + ], $arrayy->toArray()); + } + + public function testRemoveWithDotNotationStopsAtScalarIntermediateValue(): void + { + $arrayy = new A([ + 'user' => 'not-an-array', + 'keep' => true, + ]); + + $result = $arrayy->remove('user.name'); + + static::assertSame(['user' => 'not-an-array', 'keep' => true], $arrayy->toArray()); + static::assertSame($arrayy->toArray(), $result->toArray()); + } + /** * @dataProvider removeFirstProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result */ public function testRemoveFirst($array, $result): void { @@ -5010,8 +5068,8 @@ public function testRemoveFirst($array, $result): void /** * @dataProvider removeLastProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result */ public function testRemoveLast($array, $result): void { @@ -5023,8 +5081,8 @@ public function testRemoveLast($array, $result): void /** * @dataProvider removeV2Provider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result * @param mixed $key */ public function testRemoveV2($array, $result, $key): void @@ -5037,8 +5095,8 @@ public function testRemoveV2($array, $result, $key): void /** * @dataProvider removeValueProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result * @param mixed $value */ public function testRemoveValue($array, $result, $value): void @@ -5158,7 +5216,7 @@ public function testReplaceAllValues(): void $resultArrayy = $arrayy->replaceAllValues($secondArray); $resultArray = (array) \array_combine($firstArray, $secondArray); - self::assertImmutable($arrayy, $resultArrayy, $firstArray, $resultArray); + self::assertImmutable($arrayy, $resultArrayy, $firstArray, $resultArray); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } public function testReplaceAllValuesV2(): void @@ -5189,7 +5247,7 @@ public function testReplaceAllValuesV2(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testReplaceIn(array $array): void { @@ -5209,7 +5267,7 @@ public function testReplaceIn(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testReplaceInRecursively(array $array): void { @@ -5239,6 +5297,15 @@ public function testReplaceKeys(): void static::assertSame('foo', $arrayy['replaced']); } + public function testReplacementMethodsReturnEmptyForMismatchedSizes(): void + { + $arrayy = A::create(['one', 'two']); + + static::assertSame([], $arrayy->replaceAllKeys(['only-one'])->toArray()); + static::assertSame([], $arrayy->replaceAllValues([1])->toArray()); + static::assertSame([], $arrayy->replaceKeys(['only-one'])->toArray()); + } + public function testReplaceOneValue(): void { $testArray = ['bar', 'foo' => 'foo', 'foobar' => 'foobar']; @@ -5275,7 +5342,7 @@ public function testReplaceValues(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testReplaceWith(array $array): void { @@ -5295,7 +5362,7 @@ public function testReplaceWith(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testReplaceWithRecursively(array $array): void { @@ -5315,8 +5382,8 @@ public function testReplaceWithRecursively(array $array): void /** * @dataProvider restProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result * @param int $from */ public function testRest($array, $result, $from = 1): void @@ -5329,8 +5396,8 @@ public function testRest($array, $result, $from = 1): void /** * @dataProvider reverseProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result */ public function testReverse($array, $result): void { @@ -5343,7 +5410,7 @@ public function testReverse($array, $result): void * @dataProvider searchIndexProvider() * * @param false|int|string $expected - * @param array $array + * @param array $array * @param mixed $value */ public function testSearchIndex($expected, $array, $value): void @@ -5356,8 +5423,8 @@ public function testSearchIndex($expected, $array, $value): void /** * @dataProvider searchValueProvider() * - * @param array $expected - * @param array $array + * @param array $expected + * @param array $array * @param mixed $value */ public function testSearchValue($expected, $array, $value): void @@ -5385,13 +5452,8 @@ public function testSerialize(): void static::assertSame($object->arrayy, $arrayy); // serialize + tests - if (\PHP_VERSION_ID < 70400) { - static::assertStringContainsString('O:8:"stdClass":1:{s:6:"arrayy";C:13:"Arrayy\Arrayy":', \serialize($object)); - static::assertNotSame($object, \unserialize(\serialize($object))); - } else { - static::assertStringContainsString('O:8:"stdClass":1:{s:6:"arrayy";O:13:"Arrayy\\Arrayy":', \serialize($object)); - static::assertNotSame($object, \unserialize(\serialize($object))); - } + static::assertStringContainsString('O:8:"stdClass":1:{s:6:"arrayy";O:13:"Arrayy\\Arrayy":', \serialize($object)); + static::assertNotSame($object, \unserialize(\serialize($object))); $arrayy = new A([1 => 1, 2 => 2, 3 => 3]); $serialized = $arrayy->serialize(); @@ -5412,17 +5474,10 @@ public function testSerialize(): void ); // serialize + tests - if (\PHP_VERSION_ID < 70400) { - static::assertInstanceOf(CityData::class, $model); - static::assertStringContainsString('C:21:"Arrayy\tests\CityData":', \serialize($model)); - static::assertNotSame($model, \unserialize(\serialize($model))); - static::assertInstanceOf(CityData::class, $model); - } else { - static::assertInstanceOf(CityData::class, $model); - static::assertStringContainsString('O:21:"Arrayy\tests\CityData":', \serialize($model)); - static::assertNotSame($model, \unserialize(\serialize($model))); - static::assertInstanceOf(CityData::class, $model); - } + static::assertInstanceOf(CityData::class, $model); + static::assertStringContainsString('O:21:"Arrayy\tests\CityData":', \serialize($model)); + static::assertNotSame($model, \unserialize(\serialize($model))); + static::assertInstanceOf(CityData::class, $model); } public function testSerializeSimple(): void @@ -5435,7 +5490,7 @@ public function testSerializeSimple(): void /** * @dataProvider setProvider() * - * @param array $array + * @param array $array * @param mixed $key * @param mixed $value */ @@ -5449,7 +5504,7 @@ public function testSet($array, $key, $value): void /** * @dataProvider setAndGetProvider() * - * @param array $array + * @param array $array * @param mixed $key * @param mixed $value */ @@ -5519,12 +5574,12 @@ public function testSetViaDotNotation(): void { $arrayy = new A(['Lars' => ['lastname' => 'Moelleken']]); - static::assertSame(['lastname' => 'Moelleken'], $arrayy['Lars']->getArray()); + static::assertSame(['lastname' => 'Moelleken'], $arrayy['Lars']->getArray()); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) $result = $arrayy->get('Lars.lastname'); static::assertSame('Moelleken', $result); - static::assertSame(['lastname' => 'Moelleken'], $arrayy['Lars']->getArray()); + static::assertSame(['lastname' => 'Moelleken'], $arrayy['Lars']->getArray()); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) /* @phpstan-ignore property.notFound */ static::assertSame(['lastname' => 'Moelleken'], $arrayy->Lars->getArray()); @@ -5532,7 +5587,7 @@ public function testSetViaDotNotation(): void /* @phpstan-ignore property.notFound */ static::assertSame('Moelleken', $arrayy->Lars->lastname); - static::assertSame('Moelleken', $arrayy['Lars']['lastname']); + static::assertSame('Moelleken', $arrayy['Lars']['lastname']); // @phpstan-ignore-line offsetAccess.notFound (the test intentionally exercises dynamic ArrayAccess keys absent from the inferred fixture shape) $tmp = $arrayy['Lars']; static::assertSame('Moelleken', $tmp['lastname']); @@ -5584,7 +5639,7 @@ public function testSetViaDotNotation(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testShift(array $array): void { @@ -5715,7 +5770,7 @@ public function testSimpleRandomWeighted(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSlice(array $array): void { @@ -5723,7 +5778,7 @@ public function testSlice(array $array): void $resultArrayy = $arrayy->slice(1, 1); $resultArray = \array_slice($array, 1, 1); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } public function testSort(): void @@ -5774,7 +5829,7 @@ static function ($value) { /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSortAscWithPreserveKeys(array $array): void { @@ -5801,13 +5856,13 @@ public function testSortAscWithPreserveKeys(array $array): void $arrayV2 = new A($array); $resultArrayV2 = $arrayV2->asortImmutable(); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSortAscWithoutPreserveKeys(array $array): void { @@ -5831,7 +5886,7 @@ public function testSortAscWithoutPreserveKeys(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSortDescWithPreserveKeys(array $array): void { @@ -5855,7 +5910,7 @@ public function testSortDescWithPreserveKeys(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSortImmutableDescWithPreserveKeys(array $array): void { @@ -5864,7 +5919,7 @@ public function testSortImmutableDescWithPreserveKeys(array $array): void $resultArray = $array; \arsort($resultArray, \SORT_REGULAR); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) // --- @@ -5873,13 +5928,13 @@ public function testSortImmutableDescWithPreserveKeys(array $array): void $arrayV2 = new A($array); $resultArrayV2 = $arrayV2->arsortImmutable(); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSortDescWithoutPreserveKeys(array $array): void { @@ -5906,14 +5961,14 @@ public function testSortDescWithoutPreserveKeys(array $array): void $arrayV2 = new A($array); $resultArrayV2 = $arrayV2->rsortImmutable(); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider sortKeysProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result * @param string $direction */ public function testSortKeys($array, $result, $direction = 'ASC'): void @@ -5926,7 +5981,7 @@ public function testSortKeys($array, $result, $direction = 'ASC'): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSortKeysAsc(array $array): void { @@ -5953,13 +6008,13 @@ public function testSortKeysAsc(array $array): void $arrayV2 = new A($array); $resultArrayV2 = $arrayV2->ksortImmutable(); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testNatcasesort(array $array): void { @@ -5975,7 +6030,7 @@ public function testNatcasesort(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testNatsortImmutable(array $array): void { @@ -5985,13 +6040,13 @@ public function testNatsortImmutable(array $array): void $arrayResult = $arrayyResult->getArray(); static::assertSame($array, $arrayResult); - self::assertImmutable($arrayy, $arrayyResult, $array, $arrayResult); + self::assertImmutable($arrayy, $arrayyResult, $array, $arrayResult); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testNatsort(array $array): void { @@ -6007,7 +6062,7 @@ public function testNatsort(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testNatcasesortImmutable(array $array): void { @@ -6017,13 +6072,13 @@ public function testNatcasesortImmutable(array $array): void $arrayResult = $arrayyResult->getArray(); static::assertSame($array, $arrayResult); - self::assertImmutable($arrayy, $arrayyResult, $array, $arrayResult); + self::assertImmutable($arrayy, $arrayyResult, $array, $arrayResult); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testUasort(array $array): void { @@ -6046,7 +6101,7 @@ public function testUasort(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testUasortImmutable(array $array): void { @@ -6063,13 +6118,13 @@ public function testUasortImmutable(array $array): void $arrayResult = $arrayyResult->getArray(); static::assertSame($array, $arrayResult); - self::assertImmutable($arrayy, $arrayyResult, $array, $arrayResult); + self::assertImmutable($arrayy, $arrayyResult, $array, $arrayResult); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSortKeysDesc(array $array): void { @@ -6096,7 +6151,7 @@ public function testSortKeysDesc(array $array): void $arrayV2 = new A($array); $resultArrayV2 = $arrayV2->krsortImmutable(); - self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); + self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } public function testSortV2(): void @@ -6172,20 +6227,20 @@ public function testSplit(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testStaticCreate(array $array): void { $arrayy = new A($array); $resultArrayy = A::create($array); - self::assertImmutable($arrayy, $resultArrayy, $array, $array); + self::assertImmutable($arrayy, $resultArrayy, $array, $array); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testStaticCreateFromGeneratorImmutableFromArray(array $array): void { @@ -6199,7 +6254,7 @@ public function testStaticCreateFromGeneratorImmutableFromArray(array $array): v /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array * @param int $count */ public function testStaticCreateFromGeneratorFunctionFromArray(array $array, int $count): void @@ -6219,7 +6274,7 @@ static function () use ($arrayy) { /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testStaticCreateFromJson(array $array): void { @@ -6234,7 +6289,7 @@ public function testStaticCreateFromJson(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testStaticCreateFromObject(array $array): void { @@ -6334,7 +6389,7 @@ public function testStaticCreateFromString($string, $separator): void } else { $array = [$string]; } - \assert(\is_array($array)); + \assert(\is_array($array)); // @phpstan-ignore-line function.alreadyNarrowedType (the runtime guard is retained for inputs from supported PHP versions and data providers) $arrayy = A::create($array); $resultArrayy = A::createFromString($string, $separator); @@ -6369,9 +6424,9 @@ public function testSwap(): void /** * @dataProvider diffReverseProvider() * - * @param array $array - * @param array $arrayNew - * @param array $result + * @param array $array + * @param array $arrayNew + * @param array $result */ public function testTestdiffReverse($array, $arrayNew, $result): void { @@ -6383,7 +6438,7 @@ public function testTestdiffReverse($array, $arrayNew, $result): void /** * @dataProvider isMultiArrayProvider() * - * @param array $array + * @param array $array * @param bool $result */ public function testTestisMultiArray($array, $result): void @@ -6397,7 +6452,7 @@ public function testTestisMultiArray($array, $result): void * @dataProvider toStringProvider() * * @param string $expected - * @param array $array + * @param array $array */ public function testToString($expected, $array): void { @@ -6407,8 +6462,8 @@ public function testToString($expected, $array): void /** * @dataProvider uniqueProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result */ public function testUnique($array, $result): void { @@ -6422,8 +6477,8 @@ public function testUnique($array, $result): void /** * @dataProvider uniqueProviderKeepIndex() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result */ public function testUniqueKeepIndex($array, $result): void { @@ -6456,7 +6511,7 @@ public function testUnsetSimple(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testUnshift(array $array): void { @@ -6480,10 +6535,30 @@ public function testValues(): void static::assertSame($matcher, $values->getArray()); } + public function testWhereSupportsArraysAndObjects(): void + { + $object = new \stdClass(); + $object->status = 'active'; + + $result = A::create([ + ['status' => 'active', 'name' => 'array'], + ['status' => 'inactive', 'name' => 'other'], + $object, + ])->where('status', 'active'); + + static::assertSame( + [ + ['status' => 'active', 'name' => 'array'], + 2 => $object, + ], + $result->toArray() + ); + } + /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testWalk(array $array): void { @@ -6502,7 +6577,7 @@ public function testWalk(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testWalkRecursively(array $array): void { @@ -6547,7 +6622,7 @@ public function testWalkSimpleRecursively(): void } /** - * @return array + * @return array */ public function toStringProvider(): array { @@ -6562,7 +6637,7 @@ public function toStringProvider(): array } /** - * @return array + * @return array */ public function uniqueProvider(): array { @@ -6619,7 +6694,7 @@ public function uniqueProvider(): array } /** - * @return array + * @return array */ public function uniqueProviderKeepIndex(): array { @@ -6678,8 +6753,8 @@ public function uniqueProviderKeepIndex(): array /** * @param A> $arrayzy * @param A>|A>|A> $resultArrayzy - * @param array $array - * @param array $resultArray + * @param array $array + * @param array $resultArray */ protected static function assertImmutable(A $arrayzy, A $resultArrayzy, array $array, array $resultArray): void { @@ -6691,7 +6766,7 @@ protected static function assertImmutable(A $arrayzy, A $resultArrayzy, array $a /** * @param A> $arrayzy * @param A> $resultArrayzy - * @param array $resultArray + * @param array $resultArray */ protected static function assertMutable(A $arrayzy, A $resultArrayzy, array $resultArray): void { diff --git a/tests/BasicArrayTest.php b/tests/BasicArrayTest.php index e671c94..3fe4909 100644 --- a/tests/BasicArrayTest.php +++ b/tests/BasicArrayTest.php @@ -29,7 +29,7 @@ final class BasicArrayTest extends \PHPUnit\Framework\TestCase protected $arrayyClassName = A::class; /** - * @return array + * @return array */ public function simpleArrayProvider(): array { @@ -67,7 +67,7 @@ public function simpleArrayProvider(): array } /** - * @return array + * @return array */ public function stringWithSeparatorProvider(): array { @@ -90,7 +90,7 @@ public function stringWithSeparatorProvider(): array /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testContains(array $array): void { @@ -105,7 +105,7 @@ public function testContains(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testContainsKey(array $array): void { @@ -120,7 +120,7 @@ public function testContainsKey(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testCount(array $array): void { @@ -133,7 +133,7 @@ public function testCount(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testCurrent(array $array): void { @@ -147,7 +147,7 @@ public function testCurrent(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testDebugReturn(array $array): void { @@ -160,7 +160,7 @@ public function testDebugReturn(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testExists(array $array): void { @@ -189,7 +189,7 @@ public function testFind(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testFirstMutable(array $array): void { @@ -209,7 +209,7 @@ public function testFirstMutable(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testFirstInLoop(array $array): void { @@ -242,7 +242,7 @@ public function testFirstInLoop(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testFirstImmutable(array $array): void { @@ -262,7 +262,7 @@ public function testFirstImmutable(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testFirstImmutableInLoop(array $array): void { @@ -361,7 +361,7 @@ public function testGetIteratorWithSubArray(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetKeys(array $array): void { @@ -374,7 +374,7 @@ public function testGetKeys(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetObject(array $array): void { @@ -387,7 +387,7 @@ public function testGetObject(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetRandom(array $array): void { @@ -398,14 +398,14 @@ public function testGetRandom(array $array): void static::assertNotNull($value[0]); static::assertContains($value[0], $arrayy->toArray()); } else { - static::assertIsArray($value); + static::assertIsArray($value); // @phpstan-ignore-line staticMethod.alreadyNarrowedType (the runtime assertion is retained although PHPStan has already narrowed this fixture) } } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetRandomKey(array $array): void { @@ -415,24 +415,24 @@ public function testGetRandomKey(array $array): void /** @var array-key $key */ $key = $arrayy->getRandomKey(); - static::assertNotNull($key); + static::assertNotNull($key); // @phpstan-ignore-line staticMethod.alreadyNarrowedType (the runtime assertion is retained although PHPStan has already narrowed this fixture) static::assertArrayHasKey($key, $arrayy->toArray()); } else { - static::assertIsArray($arrayy->getArray()); + static::assertIsArray($arrayy->getArray()); // @phpstan-ignore-line staticMethod.alreadyNarrowedType (the runtime assertion is retained although PHPStan has already narrowed this fixture) } } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetRandomKeys(array $array): void { $arrayy = $this->createArrayy($array); if (\count($array) < 2) { - static::assertIsArray($arrayy->getArray()); + static::assertIsArray($arrayy->getArray()); // @phpstan-ignore-line staticMethod.alreadyNarrowedType (the runtime assertion is retained although PHPStan has already narrowed this fixture) } else { $keys = $arrayy->getRandomKeys(2); @@ -462,32 +462,32 @@ public function testGetRandomKeysRangeException(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetRandomKeysShouldReturnArray(array $array): void { $arrayy = $this->createArrayy($array); if (\count($array) === 0) { - static::assertIsArray($arrayy->getArray()); + static::assertIsArray($arrayy->getArray()); // @phpstan-ignore-line staticMethod.alreadyNarrowedType (the runtime assertion is retained although PHPStan has already narrowed this fixture) } else { $keys = $arrayy->getRandomKeys(\count($array))->getArray(); - static::assertIsArray($keys); + static::assertIsArray($keys); // @phpstan-ignore-line staticMethod.alreadyNarrowedType (the runtime assertion is retained although PHPStan has already narrowed this fixture) } } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetRandomValueSingle(array $array): void { $arrayy = $this->createArrayy($array); if (\count($array) === 0) { - static::assertIsArray($arrayy->getArray()); + static::assertIsArray($arrayy->getArray()); // @phpstan-ignore-line staticMethod.alreadyNarrowedType (the runtime assertion is retained although PHPStan has already narrowed this fixture) } else { $value = $arrayy->getRandomValue(); @@ -503,14 +503,14 @@ public function testGetRandomValueSingle(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetRandomValues(array $array): void { $arrayy = $this->createArrayy($array); if (\count($array) < 2) { - static::assertIsArray($arrayy->getArray()); + static::assertIsArray($arrayy->getArray()); // @phpstan-ignore-line staticMethod.alreadyNarrowedType (the runtime assertion is retained although PHPStan has already narrowed this fixture) return; } @@ -528,14 +528,14 @@ public function testGetRandomValues(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetRandomValuesSingle(array $array): void { $arrayy = $this->createArrayy($array); if (\count($array) === 0) { - static::assertIsArray($arrayy->getArray()); + static::assertIsArray($arrayy->getArray()); // @phpstan-ignore-line staticMethod.alreadyNarrowedType (the runtime assertion is retained although PHPStan has already narrowed this fixture) return; } @@ -543,7 +543,7 @@ public function testGetRandomValuesSingle(array $array): void $values = $arrayy->getRandomValues(1)->getArray(); static::assertCount(1, $values); - static::assertIsArray($arrayy->getArray()); + static::assertIsArray($arrayy->getArray()); // @phpstan-ignore-line staticMethod.alreadyNarrowedType (the runtime assertion is retained although PHPStan has already narrowed this fixture) foreach ($values as $value) { if (!$value instanceof \Arrayy\Arrayy) { static::assertContains($value, $array); @@ -554,7 +554,7 @@ public function testGetRandomValuesSingle(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testIndexOf(array $array): void { @@ -569,7 +569,7 @@ public function testIndexOf(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array * @param string $type */ public function testIsAssoc(array $array, $type = null): void @@ -583,7 +583,7 @@ public function testIsAssoc(array $array, $type = null): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testIsEmpty(array $array): void { @@ -596,7 +596,7 @@ public function testIsEmpty(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array * @param string $type */ public function testIsNumeric(array $array, $type = null): void @@ -610,7 +610,7 @@ public function testIsNumeric(array $array, $type = null): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testKey(array $array): void { @@ -624,7 +624,7 @@ public function testKey(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testLast(array $array): void { @@ -646,7 +646,7 @@ public function testLast(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testLastKey(array $array): void { @@ -669,7 +669,7 @@ public function testLastKey(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testArrayyFirst(array $array): void { @@ -691,7 +691,7 @@ public function testArrayyFirst(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testArrayyLast(array $array): void { @@ -713,7 +713,7 @@ public function testArrayyLast(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testFirstKey(array $array): void { @@ -731,15 +731,14 @@ public function testFirstKey(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testMostUsedValue(array $array): void { $arrayy = $this->createArrayy($array); if ($arrayy->isMultiArray()) { // not supported by php (array_count_values) - static::assertTrue(true); - + static::addToAssertionCount(1); return; } @@ -777,15 +776,14 @@ public function testArrayyIterator(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testMostUsedValues(array $array): void { $arrayy = $this->createArrayy($array); if ($arrayy->isMultiArray()) { // not supported by php (array_count_values) - static::assertTrue(true); - + static::addToAssertionCount(1); return; } @@ -799,7 +797,7 @@ public function testMostUsedValues(array $array): void $firsts = []; } - if ($result instanceof Arrayy) { + if ($result instanceof Arrayy) { // @phpstan-ignore-line instanceof.alwaysTrue (the runtime branch is required for values that are less narrowly typed outside this fixture) $result = $result->getArray(); } @@ -809,7 +807,7 @@ public function testMostUsedValues(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testNext(array $array): void { @@ -822,7 +820,7 @@ public function testNext(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testOffsetExists(array $array): void { @@ -837,7 +835,7 @@ public function testOffsetExists(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testOffsetGet(array $array): void { @@ -852,7 +850,7 @@ public function testOffsetGet(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testPrevious(array $array): void { @@ -865,7 +863,7 @@ public function testPrevious(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testReIndex(array $array): void { @@ -975,7 +973,7 @@ public function testGetListViaGenerator(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testToJson(array $array): void { @@ -995,7 +993,7 @@ public function testToJson(array $array): void public function testToString($string, $separator): void { $array = \explode($separator, $string); - \assert(\is_array($array)); + \assert(\is_array($array)); // @phpstan-ignore-line function.alreadyNarrowedType (the runtime guard is retained for inputs from supported PHP versions and data providers) $arrayy = $this->createArrayy($array); $resultString = \implode(',', $array); @@ -1007,10 +1005,10 @@ public function testToString($string, $separator): void /** * @param A $arrayy * @param A $resultArrayy - * @param array $array - * @param array $resultArray + * @param array $array + * @param array $resultArray */ - protected function assertImmutable(A $arrayy, A $resultArrayy, array $array, array $resultArray): void + protected function assertImmutable(A $arrayy, A $resultArrayy, array $array, array $resultArray): void // @phpstan-ignore-line generics.lessTypes, missingType.iterableValue (the shared test helper intentionally accepts multiple concrete Arrayy generic shapes) { static::assertNotSame($arrayy, $resultArrayy); static::assertSame($array, $arrayy->toArray()); @@ -1020,9 +1018,9 @@ protected function assertImmutable(A $arrayy, A $resultArrayy, array $array, arr /** * @param A $arrayy * @param A $resultArrayy - * @param array $resultArray + * @param array $resultArray */ - protected function assertMutable(A $arrayy, A $resultArrayy, array $resultArray): void + protected function assertMutable(A $arrayy, A $resultArrayy, array $resultArray): void // @phpstan-ignore-line generics.lessTypes, missingType.iterableValue (the shared test helper intentionally accepts multiple concrete Arrayy generic shapes) { static::assertSame($arrayy, $resultArrayy); static::assertSame($resultArray, $arrayy->toArray()); @@ -1032,12 +1030,12 @@ protected function assertMutable(A $arrayy, A $resultArrayy, array $resultArray) // The method list order by ASC /** - * @param array $array + * @param array $array * * @return A */ - protected function createArrayy(array $array = []): A + protected function createArrayy(array $array = []): A // @phpstan-ignore-line generics.lessTypes, missingType.iterableValue (the shared test helper intentionally accepts multiple concrete Arrayy generic shapes) { - return new $this->arrayyClassName($array); + return new $this->arrayyClassName($array); // @phpstan-ignore return.type (the test factory selects its Arrayy subclass dynamically) } } diff --git a/tests/Collection/BoolTypeTest.php b/tests/Collection/BoolTypeTest.php index 444c6d8..335b071 100644 --- a/tests/Collection/BoolTypeTest.php +++ b/tests/Collection/BoolTypeTest.php @@ -44,7 +44,7 @@ public function testBoolArray(): void } \assert(\is_bool($test)); - static::assertTrue($test); + static::assertTrue($test); // @phpstan-ignore-line staticMethod.alreadyNarrowedType (the runtime assertion is retained although PHPStan has already narrowed this fixture) } public function testWrongValue(): void diff --git a/tests/Collection/CollectionTest.php b/tests/Collection/CollectionTest.php index 914aa56..fc5e03d 100644 --- a/tests/Collection/CollectionTest.php +++ b/tests/Collection/CollectionTest.php @@ -93,14 +93,14 @@ public function testUserDataCollectionFromJsonMulti(): void $userDataCollection->getAll(); $userData0 = $userDataCollection[0]; - static::assertSame('Lars', $userData0->firstName); - static::assertInstanceOf(CityData::class, $userData0->city); - static::assertSame('Düsseldorf', $userData0->city->name); + static::assertSame('Lars', $userData0->firstName); // @phpstan-ignore-line property.nonObject (the test intentionally exercises property access on a runtime-polymorphic value) + static::assertInstanceOf(CityData::class, $userData0->city); // @phpstan-ignore-line property.nonObject (the test intentionally exercises property access on a runtime-polymorphic value) + static::assertSame('Düsseldorf', $userData0->city->name); // @phpstan-ignore-line property.nonObject (the test intentionally exercises property access on a runtime-polymorphic value) $userData1 = $userDataCollection[1]; - static::assertSame('Sven', $userData1->firstName); - static::assertInstanceOf(CityData::class, $userData1->city); - static::assertSame('Köln', $userData1->city->name); + static::assertSame('Sven', $userData1->firstName); // @phpstan-ignore-line property.nonObject (the test intentionally exercises property access on a runtime-polymorphic value) + static::assertInstanceOf(CityData::class, $userData1->city); // @phpstan-ignore-line property.nonObject (the test intentionally exercises property access on a runtime-polymorphic value) + static::assertSame('Köln', $userData1->city->name); // @phpstan-ignore-line property.nonObject (the test intentionally exercises property access on a runtime-polymorphic value) } public function testSimpleCollection(): void @@ -136,7 +136,7 @@ public function testJsonSerializableCollection(): void $first = $jsonSerializableCollection->first(); if ($first) { - \assert($first instanceof \Arrayy\Arrayy); + \assert($first instanceof \Arrayy\Arrayy); // @phpstan-ignore-line function.alreadyNarrowedType, instanceof.alwaysTrue (the runtime guard covers dynamic values although this fixture is statically narrowed) static::assertSame('fooooo', $first->get('foo')); } } @@ -418,7 +418,7 @@ public function testWithGeneratorsV1(): void foreach ($barCollection as $item) { static::assertInstanceOf(ModelInterface::class, $item); - if ($item instanceof ModelInterface) { + if ($item instanceof ModelInterface) { // @phpstan-ignore-line instanceof.alwaysTrue (the runtime branch is required for values that are less narrowly typed outside this fixture) static::assertStringStartsWith('foo', $item->getFoo()); } } diff --git a/tests/Collection/StringTypeTest.php b/tests/Collection/StringTypeTest.php index ec4f5e2..2f62765 100644 --- a/tests/Collection/StringTypeTest.php +++ b/tests/Collection/StringTypeTest.php @@ -22,7 +22,7 @@ public function testArraySimple(): void $strings[] = 'A'; $strings[] = 'B'; $strings[] = 'C'; - $strings[] = 1.0; + $strings[] = 1.0; // @phpstan-ignore-line offsetAssign.valueType (the test intentionally verifies runtime rejection of an invalid collection value) } public function testArray(): void @@ -51,8 +51,7 @@ public function testWrongValue(): void { $this->expectException(\TypeError::class); - /* @phpstan-ignore offsetAssign.valueType */ - new StringCollection(['A', 'B', 'C', 1]); + new StringCollection(['A', 'B', 'C', 1]); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } public function testWrongValueFromJsonMapper(): void diff --git a/tests/Collection/TypeTypeTest.php b/tests/Collection/TypeTypeTest.php index 3af2fc2..5032b44 100644 --- a/tests/Collection/TypeTypeTest.php +++ b/tests/Collection/TypeTypeTest.php @@ -28,7 +28,6 @@ public function testWrongValue(): void /** @noinspection PhpParamsInspection */ /** @noinspection PhpStrictTypeCheckingInspection */ - /* @phpstan-ignore argument.type */ - new Collection(\stdClass::class, [new \stdClass(), 'A']); + new Collection(\stdClass::class, [new \stdClass(), 'A']); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) } } diff --git a/tests/Collection/TypesTest.php b/tests/Collection/TypesTest.php index 6389a68..cc51d53 100644 --- a/tests/Collection/TypesTest.php +++ b/tests/Collection/TypesTest.php @@ -58,7 +58,7 @@ public function testChainMethods(): void $users = UserDataCollection::createFromGeneratorFunction($data); $names = $users - ->filter(static function (UserData $person): bool { + ->filter(static function (UserData $person): bool { // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) return $person->id <= 30; }) ->customSortValuesImmutable(static function (UserData $a, UserData $b): int { diff --git a/tests/InfrastructureCoverageTest.php b/tests/InfrastructureCoverageTest.php index 858f48a..0174c31 100644 --- a/tests/InfrastructureCoverageTest.php +++ b/tests/InfrastructureCoverageTest.php @@ -17,6 +17,69 @@ */ final class InfrastructureCoverageTest extends TestCase { + public function testMagicGetWrapsNestedArraysByReference(): void + { + $arrayy = new Arrayy(['profile' => ['name' => 'Lars']]); + + $profile = $arrayy->__get('profile'); + static::assertInstanceOf(Arrayy::class, $profile); + + $profile->set('name', 'Sven'); + static::assertSame('Sven', $arrayy->get('profile.name')); + } + + public function testKeyDecorationRecursesIntoArrayyAndArrayValues(): void + { + $arrayy = new Arrayy([ + 'object' => new Arrayy(['name' => 'Lars']), + 'array' => ['name' => 'Sven'], + 'scalar' => 'kept', + ]); + + static::assertSame( + [ + 'prefix-object' => ['prefix-name' => 'Lars'], + 'prefix-array' => ['prefix-name' => 'Sven'], + 'prefix-scalar' => 'kept', + ], + $arrayy->appendToEachKey('prefix-')->toArray(true) + ); + static::assertSame( + [ + 'object' => ['name-suffix' => 'Lars'], + 'array' => ['name-suffix' => 'Sven'], + 'scalar-suffix' => 'kept', + ], + $arrayy->prependToEachKey('-suffix')->toArray(true) + ); + } + + public function testValueDecorationRecursesAndPreservesObjects(): void + { + $preserved = new \stdClass(); + $arrayy = new Arrayy([ + 'object-arrayy' => new Arrayy(['name' => 'Lars']), + 'array' => ['name' => 'Sven'], + 'object' => $preserved, + 'scalar' => 'value', + ]); + + $result = $arrayy->prependToEachValue('-suffix'); + + static::assertSame('Lars-suffix', $result->get('object-arrayy.name')); + static::assertSame('Sven-suffix', $result->get('array.name')); + static::assertSame($preserved, $result->get('object')); + static::assertSame('value-suffix', $result->get('scalar')); + } + + public function testEmptyRandomAndSearchOperationsReturnEmptyCollections(): void + { + $arrayy = new Arrayy([]); + + static::assertSame([], $arrayy->randomMutable()->toArray()); + static::assertSame([], $arrayy->searchValue('missing')->toArray()); + } + public function testArrayyIteratorOffsetGetWrapsNestedArrays(): void { $iterator = new ArrayyIterator([['foo' => 'bar']], 0, Arrayy::class); diff --git a/tests/JsonMapperCoverageTest.php b/tests/JsonMapperCoverageTest.php index a8d599f..ebc747f 100644 --- a/tests/JsonMapperCoverageTest.php +++ b/tests/JsonMapperCoverageTest.php @@ -18,7 +18,7 @@ public function testMapRejectsNonObjectTargets(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('JsonMapper::map() requires second argument to be an object, integer given.'); - (new Json())->map([], 123); + (new Json())->map([], 123); // @phpstan-ignore-line argument.templateType, argument.type (the invalid target is the subject of this test) } public function testMapInvokesUndefinedPropertyHandlerWithSafeName(): void @@ -37,6 +37,14 @@ public function testMapInvokesUndefinedPropertyHandlerWithSafeName(): void static::assertSame([$target, 'UnknownKey', 'value'], $captured); } + public function testMapPreservesTraversableEntries(): void + { + $input = new \ArrayIterator(['name' => 'From iterator']); + $target = (new Json())->map($input, new JsonMapperStringFixture()); + + static::assertSame('From iterator', $target->name); + } + public function testMapSkipsPrivatePropertiesWithoutSetters(): void { $mapper = new Json(); diff --git a/tests/JsonMapperTest.php b/tests/JsonMapperTest.php index 9abda9a..ea8c2d8 100644 --- a/tests/JsonMapperTest.php +++ b/tests/JsonMapperTest.php @@ -14,7 +14,7 @@ public function testJsonMappingV1(): void $found = false; - GetAccountsResponse::createFromJsonMapper($json) + GetAccountsResponse::createFromJsonMapper($json) // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) ->accounts ->each(function (Account $a) use (&$found) { static::assertTrue($a->accountName === 'Foo' || $a->accountName === 'Bar'); diff --git a/tests/MetaPhpStanIntegrationTest.php b/tests/MetaPhpStanIntegrationTest.php index 2f530d2..fd02df8 100644 --- a/tests/MetaPhpStanIntegrationTest.php +++ b/tests/MetaPhpStanIntegrationTest.php @@ -77,7 +77,7 @@ private function runPhpStanFixture(string $fixtureFile): array 'analyse', '--no-progress', '--error-format=raw', - '--configuration=' . $repoRoot . '/phpstan.neon', + '--configuration=' . $repoRoot . '/tests/PHPStan/phpstan-fixtures.neon', $repoRoot . '/tests/PHPStan/' . $fixtureFile, ]; diff --git a/tests/ModelA.php b/tests/ModelA.php index fdddd25..2862684 100644 --- a/tests/ModelA.php +++ b/tests/ModelA.php @@ -12,7 +12,7 @@ class ModelA extends \Arrayy\Arrayy implements ModelInterface /** * ModelA constructor. * - * @param array $array + * @param array $array * @param string $iteratorClass * * @phpstan-param class-string<\Arrayy\ArrayyIterator> $iteratorClass diff --git a/tests/PHPStan/AccessShapeProfile.php b/tests/PHPStan/AccessShapeProfile.php new file mode 100644 index 0000000..19a5bbf --- /dev/null +++ b/tests/PHPStan/AccessShapeProfile.php @@ -0,0 +1,28 @@ + + * @property-read string $name + * @property-read string|null $avatar + */ +final class AccessShapeProfile extends \Arrayy\Arrayy implements \Arrayy\PHPStan\DefaultDotNotationTypeInterface +{ + /** + * @param 'avatar'|'name' $offset + * @return string + */ + #[\ReturnTypeWillChange] + public function &offsetGet($offset) + { + $value = &parent::offsetGet($offset); + if ($value === null) { + throw new \OutOfBoundsException((string) $offset); + } + + return $value; + } +} diff --git a/tests/PHPStan/AccessShapeUser.php b/tests/PHPStan/AccessShapeUser.php new file mode 100644 index 0000000..6ebfd17 --- /dev/null +++ b/tests/PHPStan/AccessShapeUser.php @@ -0,0 +1,27 @@ + + * @property-read AccessShapeProfile $profile + */ +final class AccessShapeUser extends \Arrayy\Arrayy implements \Arrayy\PHPStan\DefaultDotNotationTypeInterface +{ + /** + * @param 'profile' $offset + * @return AccessShapeProfile + */ + #[\ReturnTypeWillChange] + public function &offsetGet($offset) + { + $value = &parent::offsetGet($offset); + if ($value === null) { + throw new \OutOfBoundsException((string) $offset); + } + + return $value; + } +} diff --git a/tests/PHPStan/AccessWaysTest.php b/tests/PHPStan/AccessWaysTest.php new file mode 100644 index 0000000..e87ace7 --- /dev/null +++ b/tests/PHPStan/AccessWaysTest.php @@ -0,0 +1,52 @@ + new AccessShapeProfile([ + 'name' => 'Lars', + ]), + ]); + + \PHPStan\Testing\assertType('string|null', $user->get('profile.name')); + \PHPStan\Testing\assertType('string', $user->get('profile.name', 'Guest')); + \PHPStan\Testing\assertType('string', $user->get('profile.avatar', 'default.png')); + + \PHPStan\Testing\assertType('Arrayy\tests\PHPStan\AccessShapeProfile', $user['profile']); + \PHPStan\Testing\assertType('string', $user['profile']['name']); + + \PHPStan\Testing\assertType('Arrayy\tests\PHPStan\AccessShapeProfile', $user->profile); + \PHPStan\Testing\assertType('string', $user->profile->name); + + $profileWithAvatar = new AccessShapeProfile(['name' => 'Lars', 'avatar' => 'avatar.png']); + \PHPStan\Testing\assertType('string', $profileWithAvatar['avatar']); + + self::assertSame('Lars', $user->get('profile.name')); + self::assertSame('Lars', $user['profile']['name']); + self::assertSame('Lars', $user->profile->name); + self::assertSame('default.png', $user->get('profile.avatar', 'default.png')); + self::assertSame('avatar.png', $profileWithAvatar['avatar']); + } + + public function testCustomSeparatorUsesConservativeMethodType(): void + { + $user = new CustomSeparatorAccessUser([ + 'profile' => new AccessShapeProfile(['name' => 'Lars']), + ]); + $user->changeSeparator('^'); + + \PHPStan\Testing\assertType('mixed', $user->get('profile.name', 42)); + self::assertSame(42, $user->get('profile.name', 42)); + } +} diff --git a/tests/PHPStan/AnalyseTest.php b/tests/PHPStan/AnalyseTest.php index 11c5e0f..ae235fd 100644 --- a/tests/PHPStan/AnalyseTest.php +++ b/tests/PHPStan/AnalyseTest.php @@ -22,7 +22,7 @@ public function testGenerics(): void static::assertTrue($user->city === null || $user->city instanceof \Arrayy\tests\CityData); /* @phpstan-ignore staticMethod.alreadyNarrowedType, instanceof.alwaysTrue, booleanOr.alwaysTrue */ \PHPStan\Testing\assertType('string|null', $user->city->name ?? null); - static::assertTrue(($user->city->name ?? null) === null || is_string($user->city->name ?? null)); + static::assertTrue(($user->city->name ?? null) === null || is_string($user->city->name ?? null)); // @phpstan-ignore-line booleanOr.rightAlwaysTrue, staticMethod.alreadyNarrowedType (the runtime assertion documents nullable behavior despite PHPStan already narrowing this fixture) } // ------------------------------------------------------------------------- @@ -31,7 +31,7 @@ public function testGenerics(): void $newSet = $set->chunk(2); foreach ($newSet as $chunk) { - \PHPStan\Testing\assertType('Arrayy\Arrayy<(int|string), string>', $chunk); + \PHPStan\Testing\assertType('Arrayy\Arrayy<(int|string), string, array>', $chunk); static::assertTrue($chunk->getArray() === ['A', 'B'] || $chunk->getArray() === ['C', 'D'] || $chunk->getArray() === ['E']); } @@ -63,6 +63,7 @@ public function testGenerics(): void // ------------------------------------------------------------------------- + /** @var \Arrayy\Type\DetectFirstValueTypeCollection $set */ $set = new \Arrayy\Type\DetectFirstValueTypeCollection([1, 2, 3, 4]); foreach ($set as $item) { @@ -72,6 +73,7 @@ public function testGenerics(): void // ------------------------------------------------------------------------- + /** @var \Arrayy\Type\DetectFirstValueTypeCollection $set */ $set = new \Arrayy\Type\DetectFirstValueTypeCollection([new \stdClass(), new \stdClass()]); foreach ($set as $item) { @@ -106,7 +108,7 @@ public function testGenerics(): void return $value === $search; }; \PHPStan\Testing\assertType('bool|float|int|string', $set->find($closure)); - static::assertTrue(is_scalar($set->find($closure))); + static::assertTrue(is_scalar($set->find($closure))); // @phpstan-ignore-line function.alreadyNarrowedType, staticMethod.alreadyNarrowedType (the runtime assertion is retained although PHPStan has already narrowed this fixture) // ------------------------------------------------------------------------- } diff --git a/tests/PHPStan/CallableGenericInferenceTest.php b/tests/PHPStan/CallableGenericInferenceTest.php new file mode 100644 index 0000000..327b2ae --- /dev/null +++ b/tests/PHPStan/CallableGenericInferenceTest.php @@ -0,0 +1,64 @@ + $numbers */ + $numbers = CallableGenericArrayy::create([1, 2]); + + $strings = $numbers->each( + static function ($value, $key): string { + \PHPStan\Testing\assertType('int', $value); + \PHPStan\Testing\assertType('int|string|null', $key); + + return $key . ':' . $value; + } + ); + + \PHPStan\Testing\assertType('Arrayy\tests\PHPStan\CallableGenericArrayy', $strings); + self::assertInstanceOf(CallableGenericArrayy::class, $strings); + self::assertSame(['0:1', '1:2'], $strings->toArray()); + } + + public function testMapInfersCallableInputAndOutputTypes(): void + { + /** @var CallableGenericArrayy $numbers */ + $numbers = CallableGenericArrayy::create([1, 2]); + + $strings = $numbers->map( + static function ($value, $key = null): string { + \PHPStan\Testing\assertType('int', $value); + \PHPStan\Testing\assertType('int', $key); + + return $key . ':' . $value; + }, + true + ); + + \PHPStan\Testing\assertType( + 'Arrayy\tests\PHPStan\CallableGenericArrayy', + $strings + ); + self::assertInstanceOf(CallableGenericArrayy::class, $strings); + self::assertSame(['0:1', '1:2'], $strings->toArray()); + } +} + +/** + * @template TKey of array-key + * @template TValue + * @extends \Arrayy\Arrayy> + */ +final class CallableGenericArrayy extends \Arrayy\Arrayy +{ +} diff --git a/tests/PHPStan/CustomSeparatorAccessUser.php b/tests/PHPStan/CustomSeparatorAccessUser.php new file mode 100644 index 0000000..a0d2be1 --- /dev/null +++ b/tests/PHPStan/CustomSeparatorAccessUser.php @@ -0,0 +1,12 @@ + + */ +final class CustomSeparatorAccessUser extends \Arrayy\Arrayy +{ +} diff --git a/tests/PHPStan/GetDynamicMethodReturnTypeExtensionTest.php b/tests/PHPStan/GetDynamicMethodReturnTypeExtensionTest.php new file mode 100644 index 0000000..1d5cd62 --- /dev/null +++ b/tests/PHPStan/GetDynamicMethodReturnTypeExtensionTest.php @@ -0,0 +1,124 @@ +createMock(MethodReflection::class); + $get->method('getName')->willReturn('get'); + $set = $this->createMock(MethodReflection::class); + $set->method('getName')->willReturn('set'); + + static::assertSame(Arrayy::class, $extension->getClass()); + static::assertTrue($extension->isMethodSupported($get)); + static::assertFalse($extension->isMethodSupported($set)); + } + + /** + * @dataProvider unsupportedPathProvider + */ + public function testUnsupportedPathsFallBackToTheNativeMethodType(string $path): void + { + $extension = new GetDynamicMethodReturnTypeExtension(); + $call = $this->createGetCall($path); + $scope = $this->createMock(Scope::class); + $scope->method('getType')->willReturnCallback( + static fn ($node) => $node instanceof String_ && $node->value === $path + ? new ConstantStringType($path) + : new MixedType() + ); + + static::assertNull( + $extension->getTypeFromMethodCall($this->createMock(MethodReflection::class), $call, $scope) + ); + } + + /** + * @return array + */ + public function unsupportedPathProvider(): array + { + return [ + 'not nested' => ['profile'], + 'wildcard' => ['profile.*'], + ]; + } + + public function testCallWithoutAPathIsNotHandled(): void + { + $extension = new GetDynamicMethodReturnTypeExtension(); + $scope = $this->createMock(Scope::class); + $call = new MethodCall(new Variable('arrayy'), 'get'); + + static::assertNull( + $extension->getTypeFromMethodCall($this->createMock(MethodReflection::class), $call, $scope) + ); + } + + public function testTypedPathOnAnUntypedReceiverIsNotHandled(): void + { + $extension = new GetDynamicMethodReturnTypeExtension(); + $call = $this->createGetCall('profile.name'); + $scope = $this->createMock(Scope::class); + $scope->method('getType')->willReturnCallback( + static fn ($node) => $node instanceof String_ && $node->value === 'profile.name' + ? new ConstantStringType('profile.name') + : new MixedType() + ); + + static::assertNull( + $extension->getTypeFromMethodCall($this->createMock(MethodReflection::class), $call, $scope) + ); + } + + public function testFallbackTypeHandlesMissingNeverAndConcreteDefaults(): void + { + $extension = new GetDynamicMethodReturnTypeExtension(); + $method = new \ReflectionMethod($extension, 'getFallbackType'); + $method->setAccessible(true); + $scope = $this->createMock(Scope::class); + + $withoutFallback = $this->createGetCall('profile.name'); + static::assertInstanceOf(NullType::class, $method->invoke($extension, $withoutFallback, $scope)); + + $withFallback = $this->createGetCall('profile.name', new Arg(new String_('Guest'))); + $scope->method('getType')->willReturnOnConsecutiveCalls(new NeverType(), new ConstantStringType('Guest')); + static::assertInstanceOf(NullType::class, $method->invoke($extension, $withFallback, $scope)); + + $fallback = $method->invoke($extension, $withFallback, $scope); + static::assertInstanceOf(ConstantStringType::class, $fallback); + static::assertSame('Guest', $fallback->getValue()); + } + + private function createGetCall(string $path, ?Arg $fallback = null): MethodCall + { + $args = [new Arg(new String_($path))]; + if ($fallback !== null) { + $args[] = $fallback; + } + + return new MethodCall(new Variable('arrayy'), 'get', $args); + } +} diff --git a/tests/PHPStan/phpstan-fixtures.neon b/tests/PHPStan/phpstan-fixtures.neon new file mode 100644 index 0000000..a440613 --- /dev/null +++ b/tests/PHPStan/phpstan-fixtures.neon @@ -0,0 +1,12 @@ +parameters: + level: 8 + +services: + - + class: Arrayy\PHPStan\GetDynamicMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicMethodReturnTypeExtension + - + class: Arrayy\PHPStan\MetaDynamicStaticMethodReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension diff --git a/tests/TypeCheckCoreCoverageTest.php b/tests/TypeCheckCoreCoverageTest.php index 4f0671d..704504e 100644 --- a/tests/TypeCheckCoreCoverageTest.php +++ b/tests/TypeCheckCoreCoverageTest.php @@ -208,15 +208,15 @@ public function testFromPhpDocumentorPropertyParsesSupportedPseudoTypes(): void static::assertContainsOnlyInstancesOf(Property::class, $tags); - $scalarTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[0]); - $callableTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[1]); - $objectTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[2]); - $arrayTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[3]); + $scalarTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[0]); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) + $callableTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[1]); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) + $objectTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[2]); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) + $arrayTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[3]); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) - static::assertSame(['string|int|float|bool'], $scalarTypeCheck->getTypes()); - static::assertSame(['callable'], $callableTypeCheck->getTypes()); - static::assertSame(['\\ArrayObject'], $objectTypeCheck->getTypes()); - static::assertSame(['string[]'], $arrayTypeCheck->getTypes()); + static::assertSame(['string|int|float|bool'], $scalarTypeCheck->getTypes()); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertSame(['callable'], $callableTypeCheck->getTypes()); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertSame(['\\ArrayObject'], $objectTypeCheck->getTypes()); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertSame(['string[]'], $arrayTypeCheck->getTypes()); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) } /** @@ -233,7 +233,7 @@ public function testFromDocTypeObjectNullableProducesNullableChecker(): void DOC); $tag = $docBlock->getTagsByName('property')[0]; - $checker = TypeCheckPhpDoc::fromDocTypeObject('city', $tag->getType()); + $checker = TypeCheckPhpDoc::fromDocTypeObject('city', $tag->getType()); // @phpstan-ignore-line method.notFound (the test intentionally exercises Arrayy dynamic method dispatch) static::assertSame(['\\ArrayObject', 'null'], $checker->getTypes()); @@ -256,7 +256,7 @@ public function testFromDocTypeObjectNestedArrayShapeYieldsArrayType(): void * @template T of array{data: array{x: int}} */ DOC); - $bound = $docBlock->getTagsByName('template')[0]->getBound(); + $bound = $docBlock->getTagsByName('template')[0]->getBound(); // @phpstan-ignore-line method.notFound (the test intentionally exercises Arrayy dynamic method dispatch) $nestedShapeType = $bound->getItems()[0]->getValue(); // array{x: int} $checker = TypeCheckPhpDoc::fromDocTypeObject('data', $nestedShapeType); @@ -282,7 +282,7 @@ public function testArrayShapeOptionalKeyAcceptsValidObjectValue(): void 'infos' => ['lall'], ]); - $model = new TypeCheckArrayShapeUserData([ + $model = new TypeCheckArrayShapeUserData([ // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $meta->id => 1, $meta->firstName => 'Lars', $meta->lastName => 'Moelleken', @@ -303,7 +303,7 @@ public function testArrayShapeOptionalNullableKeyAcceptsExplicitNull(): void { $meta = TypeCheckArrayShapeUserData::meta(); - $model = new TypeCheckArrayShapeUserData([ + $model = new TypeCheckArrayShapeUserData([ // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $meta->id => 1, $meta->firstName => 'Lars', $meta->lastName => 'Moelleken', @@ -334,12 +334,12 @@ public function testFromPhpDocumentorPropertyParsesScalarNullAndMixedKeywords(): $tags = $docBlock->getTagsByName('property'); - $boolTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[0]); - $floatTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[1]); - $stringTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[2]); - $intTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[3]); - $mixedTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[4]); - $nullTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[5]); + $boolTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[0]); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) + $floatTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[1]); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) + $stringTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[2]); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) + $intTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[3]); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) + $mixedTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[4]); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) + $nullTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[5]); // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $boolValue = true; $floatValue = 1.5; @@ -348,18 +348,18 @@ public function testFromPhpDocumentorPropertyParsesScalarNullAndMixedKeywords(): $mixedValue = ['foo' => 'bar']; $nullValue = null; - static::assertSame(['bool'], $boolTypeCheck->getTypes()); - static::assertSame(['float'], $floatTypeCheck->getTypes()); - static::assertSame(['string'], $stringTypeCheck->getTypes()); - static::assertSame(['int'], $intTypeCheck->getTypes()); - static::assertSame(['mixed'], $mixedTypeCheck->getTypes()); - static::assertSame(['null'], $nullTypeCheck->getTypes()); - static::assertTrue($boolTypeCheck->checkType($boolValue)); - static::assertTrue($floatTypeCheck->checkType($floatValue)); - static::assertTrue($stringTypeCheck->checkType($stringValue)); - static::assertTrue($intTypeCheck->checkType($intValue)); - static::assertTrue($mixedTypeCheck->checkType($mixedValue)); - static::assertTrue($nullTypeCheck->checkType($nullValue)); + static::assertSame(['bool'], $boolTypeCheck->getTypes()); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertSame(['float'], $floatTypeCheck->getTypes()); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertSame(['string'], $stringTypeCheck->getTypes()); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertSame(['int'], $intTypeCheck->getTypes()); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertSame(['mixed'], $mixedTypeCheck->getTypes()); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertSame(['null'], $nullTypeCheck->getTypes()); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertTrue($boolTypeCheck->checkType($boolValue)); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertTrue($floatTypeCheck->checkType($floatValue)); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertTrue($stringTypeCheck->checkType($stringValue)); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertTrue($intTypeCheck->checkType($intValue)); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertTrue($mixedTypeCheck->checkType($mixedValue)); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) + static::assertTrue($nullTypeCheck->checkType($nullValue)); // @phpstan-ignore-line method.nonObject (the test intentionally exercises a runtime value that PHPStan cannot prove is an object) } /** @@ -445,7 +445,7 @@ public function testTypeCheckCallbackValidatesAndSupportsNullableValues(): void public function testArrayShapeTemplateProvidesPropertyDefinitions(): void { $meta = TypeCheckArrayShapeUserData::meta(); - $model = new TypeCheckArrayShapeUserData([ + $model = new TypeCheckArrayShapeUserData([ // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $meta->id => 1, $meta->firstName => 'Lars', $meta->lastName => 'Moelleken', @@ -463,7 +463,7 @@ public function testArrayShapeTemplateRejectsInvalidPropertyTypes(): void $this->expectExceptionMessage('Invalid type: expected "infos" to be of type {string[]}'); $meta = TypeCheckArrayShapeUserData::meta(); - new TypeCheckArrayShapeUserData([ + new TypeCheckArrayShapeUserData([ // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $meta->id => 1, $meta->firstName => 'Lars', $meta->lastName => 'Moelleken', @@ -477,7 +477,7 @@ public function testArrayShapeTemplateRejectsUnknownProperties(): void $this->expectExceptionMessage('The key "unknown" does not exist'); $meta = TypeCheckArrayShapeUserData::meta(); - new TypeCheckArrayShapeUserData([ + new TypeCheckArrayShapeUserData([ // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $meta->id => 1, $meta->firstName => 'Lars', $meta->lastName => 'Moelleken', @@ -513,7 +513,7 @@ public function testArrayShapeOptionalKeyIsTypeCheckedWhenPresent(): void $this->expectExceptionMessage('Invalid type'); $meta = TypeCheckArrayShapeUserData::meta(); - new TypeCheckArrayShapeUserData([ + new TypeCheckArrayShapeUserData([ // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $meta->id => 1, $meta->firstName => 'Lars', $meta->lastName => 'Moelleken', @@ -648,7 +648,7 @@ public function testFromDocTypeObjectWithParsedTypeKeepsPropertyNameInErrors(): DOC); $tag = $docBlock->getTagsByName('property')[0]; - $checker = TypeCheckPhpDoc::fromDocTypeObject('myProp', $tag->getType()); + $checker = TypeCheckPhpDoc::fromDocTypeObject('myProp', $tag->getType()); // @phpstan-ignore-line method.notFound (the test intentionally exercises Arrayy dynamic method dispatch) static::assertSame(['int'], $checker->getTypes()); @@ -685,7 +685,7 @@ public function testArrayShapePostConstructionTypeCheckEnforced(): void $this->expectExceptionMessageMatches('#Invalid type: expected "id" to be of type \{int\}#'); $meta = TypeCheckArrayShapeUserData::meta(); - $model = new TypeCheckArrayShapeUserData([ + $model = new TypeCheckArrayShapeUserData([ // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $meta->id => 1, $meta->firstName => 'Lars', $meta->lastName => 'M', @@ -705,7 +705,7 @@ public function testArrayShapePostConstructionMismatchEnforced(): void $this->expectExceptionMessage('The key "ghost" does not exist'); $meta = TypeCheckArrayShapeUserData::meta(); - $model = new TypeCheckArrayShapeUserData([ + $model = new TypeCheckArrayShapeUserData([ // @phpstan-ignore-line argument.type (the runtime API intentionally accepts or transforms a value PHPStan cannot reconcile with the invariant template) $meta->id => 1, $meta->firstName => 'Lars', $meta->lastName => 'M', @@ -727,7 +727,7 @@ public static function invalidStringArrayProvider(): iterable final class TypeCheckNoTypeFixture { - public $value; + public $value; // @phpstan-ignore-line missingType.property (the property stores runtime metadata with heterogeneous value types) } final class TypeCheckDocTypesFixture @@ -740,7 +740,7 @@ final class TypeCheckDocTypesFixture /** * @var \ArrayObject */ - public $objectValue; + public $objectValue; // @phpstan-ignore-line missingType.generics (the runtime class-string may select Arrayy subclasses with different template arguments) /** * @var string[] @@ -758,7 +758,7 @@ final class TypeCheckDocOverridesNativeFixture /** * @var int|string */ - public string $value = ''; + public string $value = ''; // @phpstan-ignore-line property.phpDocType (runtime reflection supplies a broader property value than the declared PHPDoc permits) } /** @@ -885,7 +885,7 @@ final class TypeCheckArrayShapeWrongTemplateName extends \Arrayy\Arrayy * * @extends stdClass */ -final class TypeCheckNonArrayyExtendsData extends \Arrayy\Arrayy +final class TypeCheckNonArrayyExtendsData extends \Arrayy\Arrayy // @phpstan-ignore-line generics.wrongParent, missingType.generics (the runtime subtype specializes its parent beyond what PHPStan can express here) { protected $checkPropertyTypes = true; }