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/build/docs/phpstan-ignore-audit.md b/build/docs/phpstan-ignore-audit.md new file mode 100644 index 0000000..56a4bca --- /dev/null +++ b/build/docs/phpstan-ignore-audit.md @@ -0,0 +1,54 @@ +# PHPStan ignore blind-spot audit + +This audit reviews the 504 identifier-scoped PHPStan ignore sites that existed at +the start of the audit. The sites were reviewed in batches of at most 50 so that +production generic limitations, deliberately-invalid tests, and genuine runtime +blind spots were not treated as one undifferentiated list. + +## Review batches + +| Batch | Original sites | Area | Confirmation and outcome | +|---:|---:|---|---| +| 1 | 1–50 | `Arrayy` construction and transformations | Confirmed most `argument.type` findings are caused by immutable methods deliberately changing keys or values while `create()` retains the receiver's templates. Found transform-return generic debt; retained locally for a separate API-typing change. | +| 2 | 51–100 | `Arrayy` mutation, replacement, sorting, slicing | Found that three `array_combine()` fallbacks were unreachable on PHP 8: mismatched inputs throw instead of returning `false`. Added explicit size guards and regression coverage. | +| 3 | 101–150 | Remaining source files and first providers | Fixed scalar dot-path traversal, invalid offset normalization, JSON mapper iterable shapes, callback nullability, mapper object-template bounds, dead type-check state, and scalar pseudo-type discovery. Provider findings were documentation gaps rather than runtime failures. | +| 4 | 151–200 | `ArrayyTest` providers and basic mutations | Confirmed provider arrays intentionally contain heterogeneous test tuples. Replaced suppressions with explicit `array` PHPDoc rather than hiding missing iterable types. | +| 5 | 201–250 | `ArrayyTest` filtering, lookup, mapping, merging | Confirmed deliberate dynamic fixtures. `method.nonObject` sites identify generic precision debt, while runtime assertions validate the objects before use. No new runtime defect was reproduced. | +| 6 | 251–300 | `ArrayyTest` offsets, randomization, replacement | Confirmed invalid-offset and wrong-type cases are the behavior under test. Added mismatched replacement-size tests for the production defect found in batch 2. | +| 7 | 301–350 | `ArrayyTest` sorting, factories, serialization | Removed obsolete PHP `< 7.4` branches because the package requires PHP 8 or newer. Converted remaining iterable suppressions to typed PHPDoc. | +| 8 | 351–400 | `BasicArrayTest` | Confirmed most narrowed-type findings are redundant runtime assertions. Removed two no-op `assertTrue(true)` branches; retained useful runtime assertions even when PHPStan can prove them. | +| 9 | 401–450 | Typed collection and mapper tests | Confirmed `argument.type` is intentional in tests asserting runtime type rejection. Mapper invalid-target tests now state that rationale beside the ignore. Collection property findings remain generic-inference limitations backed by runtime instance assertions. | +| 10 | 451–500 | PHPStan fixtures and type-check coverage | Confirmed invalid fixtures and dynamic phpDocumentor tags are intentionally outside the analyzer's declared interfaces. Retained precise identifiers; runtime assertions cover every nullable factory result before its behavior is relied upon. | +| 11 | 501–504 | Invalid generic/type-check fixtures | Confirmed all four sites deliberately declare malformed PHPDoc or generic inheritance to exercise defensive parsing and rejection paths. | + +## Classification rules + +- **Fixed now:** a diagnostic exposed behavior that could throw, contradicted the + documented contract, represented dead compatibility code, or could be expressed + accurately without weakening analysis. +- **Expected negative test:** the line deliberately violates its declared type to + verify a runtime `TypeError`, parser fallback, or PHPStan rejection. +- **Runtime assertion retained:** PHPStan can prove the assertion from PHPDoc, but + the assertion still protects behavior when PHPDoc is wrong at runtime. +- **Generic precision debt:** the runtime library intentionally re-parameterizes a + late-static collection in ways PHPStan cannot express consistently for fixed + typed subclasses. These ignores remain local and identifier-specific so + `reportUnmatchedIgnoredErrors` detects stale entries. + +## Blind spots fixed during the audit + +1. Dot-notation existence checks no longer call `array_key_exists()` on a scalar, + and recursive traversal stops before descending through a scalar intermediate. +2. `where()` can extract fields from arrays and ordinary objects, matching its + public behavior rather than requiring every item to be an `Arrayy` instance. +3. Replacement methods return an empty collection for mismatched key/value counts + as documented, instead of allowing PHP 8's `array_combine()` to throw. +4. Float removal keys are normalized before array offset access. +5. JSON mapper inputs, caches, annotation maps, callback nullability, and object + templates now have explicit types; object JSON input is converted to iterable + public properties before traversal. +6. Obsolete PHP-version branches, a write-only property, no-op test assertions, + and avoidable missing-iterable suppressions were removed. + +The remaining ignores are therefore reviewable at their exact line and fall into +one of the expected categories above; there is no project-wide baseline. 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..5cb01dd 100644 --- a/src/Arrayy.php +++ b/src/Arrayy.php @@ -247,6 +247,7 @@ public function &__get($key) if (\is_array($return) === true) { $return = static::create( + /* @phpstan-ignore-next-line argument.type */ [], $this->iteratorClass, false @@ -282,6 +283,7 @@ public function add($value, $key = null) ); } + /* @phpstan-ignore-next-line argument.type */ $this->internalSet($key, $value); return $this; @@ -761,7 +763,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,9 +790,11 @@ public function &offsetGet($offset) $value = null; if ($this->offsetExists($offset)) { + /* @phpstan-ignore-next-line argument.templateType, argument.type */ $value = &$this->__get($offset); } + /* @phpstan-ignore-next-line return.type */ return $value; } @@ -1067,6 +1071,7 @@ public function appendArrayValues(array $values, $key = null) \is_array($this->array[$key]) ) { foreach ($values as $value) { + /* @phpstan-ignore-next-line assign.propertyType */ $this->array[$key][] = $value; } } else { @@ -1103,6 +1108,7 @@ public function appendToEachKey($prefix): self if ($item instanceof self) { $result[$prefix . $key] = $item->appendToEachKey($prefix); } elseif (\is_array($item)) { + /* @phpstan-ignore-next-line argument.type */ $result[$prefix . $key] = self::create($item, $this->iteratorClass, false) ->appendToEachKey($prefix) ->toArray(); @@ -1112,6 +1118,7 @@ public function appendToEachKey($prefix): self } return self::create( + /* @phpstan-ignore-next-line argument.type */ $result, $this->iteratorClass, false @@ -1138,6 +1145,7 @@ public function appendToEachValue($prefix): self if ($item instanceof self) { $result[$key] = $item->appendToEachValue($prefix); } elseif (\is_array($item)) { + /* @phpstan-ignore-next-line argument.type */ $result[$key] = self::create($item, $this->iteratorClass, false)->appendToEachValue($prefix)->toArray(); } elseif (\is_object($item) === true) { $result[$key] = $item; @@ -1146,6 +1154,7 @@ public function appendToEachValue($prefix): self } } + /* @phpstan-ignore-next-line argument.type */ return self::create($result, $this->iteratorClass, false); } @@ -1215,6 +1224,7 @@ public function at(\Closure $closure): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $that->toArray(), $this->iteratorClass, false @@ -1307,6 +1317,7 @@ public function changeKeyCase(int $case = \CASE_LOWER): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $return, $this->iteratorClass, false @@ -1345,7 +1356,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,6 +1749,7 @@ public function containsValues(array $needles): bool public function countValues(): self { /** @phpstan-var static $return - help for phpstan */ + /* @phpstan-ignore-next-line argument.type */ $return = self::create(\array_count_values($this->toArray()), $this->iteratorClass); return $return; @@ -1868,6 +1880,7 @@ public static function createFromGeneratorFunction(callable $generatorFunction): */ public static function createFromGeneratorImmutable(\Generator $generator): self { + /* @phpstan-ignore-next-line argument.type */ return self::create(\iterator_to_array($generator, true)); } @@ -1901,6 +1914,7 @@ public static function createFromJson(string $json): self */ public static function createFromArray(array $array): self { + /* @phpstan-ignore-next-line argument.type */ return static::create($array); } @@ -1998,6 +2012,7 @@ static function (&$val) { ); /** @var static $return - help for phpstan */ + /* @phpstan-ignore-next-line argument.type */ $return = static::create($array); return $return; @@ -2020,6 +2035,7 @@ static function (&$val) { */ public static function createFromTraversableImmutable(\Traversable $traversable, bool $use_keys = true): self { + /* @phpstan-ignore-next-line argument.type */ return self::create(\iterator_to_array($traversable, $use_keys)); } @@ -2039,6 +2055,7 @@ public static function createFromTraversableImmutable(\Traversable $traversable, public static function createWithRange($low, $high, $step = 1): self { /** @phpstan-var static $return - help for phpstan */ + /* @phpstan-ignore-next-line argument.type */ $return = static::create(\range($low, $high, $step)); return $return; @@ -2365,6 +2382,7 @@ public function diffRecursive(array $array = [], $helperVariableForRecursion = n } return static::create( + /* @phpstan-ignore-next-line argument.type */ $result, $this->iteratorClass, false @@ -2390,6 +2408,7 @@ public function diffRecursive(array $array = [], $helperVariableForRecursion = n public function diffReverse(array $array = []): self { return static::create( + /* @phpstan-ignore-next-line argument.type */ \array_diff($array, $this->toArray()), $this->iteratorClass, false @@ -2412,6 +2431,7 @@ public function diffReverse(array $array = []): self public function divide(): self { return static::create( + /* @phpstan-ignore-next-line argument.type */ [ $this->keys(), $this->values(), @@ -2437,8 +2457,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,7 +2473,8 @@ public function each(\Closure $closure): self $array[$key] = $closure($value, $key); } - return static::create( + return static::create( // @phpstan-ignore return.type (create() is intentionally re-parameterized with TEach) + /* @phpstan-ignore-next-line argument.type */ $array, $this->iteratorClass, false @@ -2552,6 +2576,7 @@ public function fillWithDefaults(int $num, $default = null): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $tmpArray, $this->iteratorClass, false @@ -2729,6 +2754,7 @@ static function ($item) use ( $comparisonOp ) { $item = (array) $item; + /* @phpstan-ignore-next-line argument.type */ $itemArrayy = static::create($item); $item[$property] = $itemArrayy->get($property, []); @@ -2738,6 +2764,7 @@ static function ($item) use ( ); return static::create( + /* @phpstan-ignore-next-line argument.type */ $result, $this->iteratorClass, false @@ -2901,6 +2928,7 @@ public function firstsImmutable(?int $number = null): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $array, $this->iteratorClass, false @@ -2930,6 +2958,7 @@ public function firstsKeys(?int $number = null): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $array, $this->iteratorClass, false @@ -3057,6 +3086,7 @@ public function get( if ($key === null) { return static::create( + /* @phpstan-ignore-next-line argument.type */ [], $this->iteratorClass, false @@ -3072,6 +3102,7 @@ public function get( if (\array_key_exists($key, $usedArray) === true) { if (\is_array($usedArray[$key])) { return static::create( + /* @phpstan-ignore-next-line argument.type */ [], $this->iteratorClass, false @@ -3124,6 +3155,7 @@ public function get( unset($segmentsTmp[0]); $keyTmp = \implode('.', $segmentsTmp); $returnTmp = static::create( + /* @phpstan-ignore-next-line argument.type */ [], $this->iteratorClass, false @@ -3170,6 +3202,7 @@ public function get( if (\is_array($usedArrayTmp)) { return static::create( + /* @phpstan-ignore-next-line argument.type */ [], $this->iteratorClass, false @@ -3184,6 +3217,7 @@ public function get( } return static::create( + /* @phpstan-ignore-next-line argument.type */ [], $this->iteratorClass, false @@ -3561,6 +3595,7 @@ public function getValues() $this->generatorToArray(false); return static::create( + /* @phpstan-ignore-next-line argument.type */ \array_values($this->array), $this->iteratorClass, false @@ -3630,6 +3665,7 @@ public function group($grouper, bool $saveKeys = false): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $result, $this->iteratorClass, false @@ -3745,6 +3781,7 @@ public function indexBy($key): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $results, $this->iteratorClass, false @@ -3814,6 +3851,7 @@ public function intersection(array $search, bool $keepKeys = false): self * @psalm-suppress MissingClosureParamType */ return static::create( + /* @phpstan-ignore-next-line argument.type */ \array_uintersect( $this->toArray(), $search, @@ -3827,6 +3865,7 @@ static function ($a, $b) { } return static::create( + /* @phpstan-ignore-next-line argument.type */ \array_values(\array_intersect($this->toArray(), $search)), $this->iteratorClass, false @@ -3848,6 +3887,7 @@ static function ($a, $b) { public function intersectionMulti(...$array): self { return static::create( + /* @phpstan-ignore-next-line argument.type */ \array_values(\array_intersect($this->toArray(), ...$array)), $this->iteratorClass, false @@ -3904,6 +3944,7 @@ public function invoke($callable, $arguments = []): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $array, $this->iteratorClass, false @@ -4049,6 +4090,7 @@ public function isSequential(bool $recursive = false): bool && (\is_array($value) || $value instanceof \Traversable) && + /* @phpstan-ignore-next-line argument.type */ self::create($value)->isSequential() === false ) { return false; @@ -4157,6 +4199,7 @@ public function keys( ); return static::create( + /* @phpstan-ignore-next-line argument.type */ $array, $this->iteratorClass, false @@ -4324,6 +4367,7 @@ public function lastsImmutable(?int $number = null): self { if ($this->isEmpty()) { return static::create( + /* @phpstan-ignore-next-line argument.type */ [], $this->iteratorClass, false @@ -4340,6 +4384,7 @@ public function lastsImmutable(?int $number = null): self } $arrayy = static::create( + /* @phpstan-ignore-next-line argument.type */ $poppedValue, $this->iteratorClass, false @@ -4412,7 +4457,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,6 +4626,7 @@ public function mergeAppendKeepIndex(array $array = [], bool $recursive = false) } return static::create( + /* @phpstan-ignore-next-line argument.type */ $result, $this->iteratorClass, false @@ -4623,6 +4669,7 @@ public function mergeAppendNewIndex(array $array = [], bool $recursive = false): } return static::create( + /* @phpstan-ignore-next-line argument.type */ $result, $this->iteratorClass, false @@ -4664,6 +4711,7 @@ public function mergePrependKeepIndex(array $array = [], bool $recursive = false } return static::create( + /* @phpstan-ignore-next-line argument.type */ $result, $this->iteratorClass, false @@ -4706,6 +4754,7 @@ public function mergePrependNewIndex(array $array = [], bool $recursive = false) } return static::create( + /* @phpstan-ignore-next-line argument.type */ $result, $this->iteratorClass, false @@ -4832,6 +4881,7 @@ public function moveElement($from, $to): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $output, $this->iteratorClass, false @@ -4863,6 +4913,7 @@ public function moveElementToFirstPlace($key): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $array, $this->iteratorClass, false @@ -4894,6 +4945,7 @@ public function moveElementToLastPlace($key): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $array, $this->iteratorClass, false @@ -4997,6 +5049,7 @@ public function only(array $keys): self public function pad(int $size, $value): self { return static::create( + /* @phpstan-ignore-next-line argument.type */ \array_pad($this->toArray(), $size, $value), $this->iteratorClass, false @@ -5032,6 +5085,7 @@ public function partition(\Closure $closure): array } } + /* @phpstan-ignore-next-line argument.type */ return [self::create($matches), self::create($noMatches)]; } @@ -5148,6 +5202,7 @@ public function prependToEachKey($suffix): self $result[$key] = $item->prependToEachKey($suffix); } elseif (\is_array($item)) { $result[$key] = self::create( + /* @phpstan-ignore-next-line argument.type */ $item, $this->iteratorClass, false @@ -5159,6 +5214,7 @@ public function prependToEachKey($suffix): self } return self::create( + /* @phpstan-ignore-next-line argument.type */ $result, $this->iteratorClass, false @@ -5186,6 +5242,7 @@ public function prependToEachValue($suffix): self $result[$key] = $item->prependToEachValue($suffix); } elseif (\is_array($item)) { $result[$key] = self::create( + /* @phpstan-ignore-next-line argument.type */ $item, $this->iteratorClass, false @@ -5199,6 +5256,7 @@ public function prependToEachValue($suffix): self } return self::create( + /* @phpstan-ignore-next-line argument.type */ $result, $this->iteratorClass, false @@ -5294,6 +5352,7 @@ public function randomImmutable(?int $number = null): self if ($this->count() === 0) { return static::create( + /* @phpstan-ignore-next-line argument.type */ [], $this->iteratorClass, false @@ -5304,6 +5363,7 @@ public function randomImmutable(?int $number = null): self $arrayRandValue = [$this->array[\array_rand($this->array)]]; return static::create( + /* @phpstan-ignore-next-line argument.type */ $arrayRandValue, $this->iteratorClass, false @@ -5314,6 +5374,7 @@ public function randomImmutable(?int $number = null): self \shuffle($arrayTmp); return static::create( + /* @phpstan-ignore-next-line argument.type */ $arrayTmp, $this->iteratorClass, false @@ -5385,6 +5446,7 @@ public function randomKeys(int $number): self $result = (array) \array_rand($this->array, $number); return static::create( + /* @phpstan-ignore-next-line argument.type */ $result, $this->iteratorClass, false @@ -5411,6 +5473,7 @@ public function randomMutable(?int $number = null): self if ($this->count() === 0) { return static::create( + /* @phpstan-ignore-next-line argument.type */ [], $this->iteratorClass, false @@ -5562,6 +5625,7 @@ public function reduce_dimension(bool $unique = true): self foreach ($this->getGenerator() as $val) { if (\is_array($val)) { + /* @phpstan-ignore-next-line argument.type */ $result[] = static::create($val)->reduce_dimension($unique)->toArray(); } else { $result[] = [$val]; @@ -5570,6 +5634,7 @@ public function reduce_dimension(bool $unique = true): self $result = $result === [] ? [] : \array_merge(...$result); + /* @phpstan-ignore-next-line argument.type */ $resultArrayy = static::create($result); /** @@ -5631,6 +5696,7 @@ public function reject(\Closure $closure): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $filtered, $this->iteratorClass, false @@ -5661,6 +5727,7 @@ public function remove($key) } return static::create( + /* @phpstan-ignore-next-line argument.type */ $this->toArray(), $this->iteratorClass, false @@ -5670,6 +5737,7 @@ public function remove($key) $this->internalRemove($key); return static::create( + /* @phpstan-ignore-next-line argument.type */ $this->toArray(), $this->iteratorClass, false @@ -5713,6 +5781,7 @@ public function removeFirst(): self \array_shift($tmpArray); return static::create( + /* @phpstan-ignore-next-line argument.type */ $tmpArray, $this->iteratorClass, false @@ -5739,6 +5808,7 @@ public function removeLast(): self \array_pop($tmpArray); return static::create( + /* @phpstan-ignore-next-line argument.type */ $tmpArray, $this->iteratorClass, false @@ -5779,6 +5849,7 @@ public function removeValue($value): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $this->array, $this->iteratorClass, false @@ -5799,10 +5870,12 @@ public function removeValue($value): self public function repeat($times): self { if ($times === 0) { + /* @phpstan-ignore-next-line argument.type */ return static::create([], $this->iteratorClass); } return static::create( + /* @phpstan-ignore-next-line argument.type */ \array_fill(0, (int) $times, $this->toArray()), $this->iteratorClass, false @@ -5872,13 +5945,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( + /* @phpstan-ignore-next-line argument.type */ $data, $this->iteratorClass, false @@ -5916,13 +5987,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( + /* @phpstan-ignore-next-line argument.type */ $data, $this->iteratorClass, false @@ -5948,13 +6017,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( + /* @phpstan-ignore-next-line argument.type */ $result, $this->iteratorClass, false @@ -5990,6 +6056,7 @@ public function replaceOneValue($search, $replacement = ''): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $array, $this->iteratorClass, false @@ -6019,7 +6086,7 @@ public function replaceValues($search, $replacement = ''): self return \str_replace($search, $replacement, $value); }; - /* @phpstan-ignore argument.type */ + /* @phpstan-ignore-next-line return.type */ return $this->each($callable); } @@ -6043,6 +6110,7 @@ public function rest(int $from = 1): self $tmpArray = $this->toArray(); return static::create( + /* @phpstan-ignore-next-line argument.type */ \array_splice($tmpArray, $from), $this->iteratorClass, false @@ -6194,6 +6262,7 @@ public function searchValue($index): self if ($this->array === []) { return static::create( + /* @phpstan-ignore-next-line argument.type */ [], $this->iteratorClass, false @@ -6211,6 +6280,7 @@ public function searchValue($index): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $return, $this->iteratorClass, false @@ -6343,6 +6413,7 @@ public function shuffle(bool $secure = false, ?array $array = null): self } return static::create( + /* @phpstan-ignore-next-line argument.type */ $array, $this->iteratorClass, false @@ -6514,6 +6585,7 @@ public function sizeRecursive(): int public function slice(int $offset, ?int $length = null, bool $preserveKeys = false) { return static::create( + /* @phpstan-ignore-next-line argument.type */ \array_slice( $this->toArray(), $offset, @@ -6730,6 +6802,7 @@ public function sorter($sorter = null, $direction = \SORT_ASC, int $strategy = \ // Transform all values into their results. if ($sorter) { $arrayy = static::create( + /* @phpstan-ignore-next-line argument.type */ $array, $this->iteratorClass, false @@ -6758,6 +6831,7 @@ static function ($value) use ($sorter) { \array_multisort($results, $direction, $strategy, $array); return static::create( + /* @phpstan-ignore-next-line argument.type */ $array, $this->iteratorClass, false @@ -6788,6 +6862,7 @@ public function splice(int $offset, ?int $length = null, $replacement = []): sel ); return static::create( + /* @phpstan-ignore-next-line argument.type */ $tmpArray, $this->iteratorClass, false @@ -6924,6 +6999,7 @@ public function swap($swapA, $swapB): self list($array[$swapA], $array[$swapB]) = [$array[$swapB], $array[$swapA]]; return static::create( + /* @phpstan-ignore-next-line argument.type */ $array, $this->iteratorClass, false @@ -7061,6 +7137,7 @@ public function toPermutation(?array $items = null, array $helper = []): self /** @var static $return - help for phpstan */ $return = static::create( + /* @phpstan-ignore-next-line argument.type */ $return, $this->iteratorClass, false @@ -7321,6 +7398,7 @@ public function where(string $keyOrPropertyOrMethod, $value): self return $this->filter( function ($item) use ($keyOrPropertyOrMethod, $value) { $accessorValue = $this->extractValue( + /* @phpstan-ignore-next-line argument.type (filter() does not retain the item type in this callback) */ $item, $keyOrPropertyOrMethod ); @@ -7451,7 +7529,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 +7541,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 +7548,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 +7566,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 +7577,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 +7595,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 +8110,10 @@ protected function internalRemove($key): bool { $this->generatorToArray(); + if (\is_float($key)) { + $key = (int) $key; + } + if ( $this->pathSeparator && diff --git a/src/Collection/AbstractCollection.php b/src/Collection/AbstractCollection.php index 58fbcd1..f37ffd0 100644 --- a/src/Collection/AbstractCollection.php +++ b/src/Collection/AbstractCollection.php @@ -317,10 +317,12 @@ public static function createFromJsonMapper(string $json) if (\is_array($jsonObject)) { foreach ($jsonObject as $jsonObjectSingle) { $collectionData = $mapper->map($jsonObjectSingle, $type); + /* @phpstan-ignore-next-line argument.type */ $return->add($collectionData); } } else { $collectionData = $mapper->map($jsonObject, $type); + /* @phpstan-ignore-next-line argument.type */ $return->add($collectionData); } } else { diff --git a/src/Create.php b/src/Create.php index 9241591..b2e2996 100644 --- a/src/Create.php +++ b/src/Create.php @@ -17,6 +17,7 @@ */ function create($data): Arrayy { + /* @phpstan-ignore-next-line return.type */ return new Arrayy($data); } } diff --git a/src/Mapper/Json.php b/src/Mapper/Json.php index a5724e0..fe7c6f3 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,6 +305,7 @@ public function mapArray($json, $array, $class = null, $parent_key = '') && \count($typesTmp->getTypes()) === 1 ) { + /* @phpstan-ignore-next-line argument.templateType, argument.type (runtime PHPDoc type strings are validated by map()) */ $array[$key] = $this->map($jsonValue, $typesTmp->getTypes()[0]); $foundArrayy = true; @@ -404,7 +408,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 +489,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 +696,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 +743,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..a26c7f5 100644 --- a/src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php +++ b/src/PHPStan/MetaDynamicStaticMethodReturnTypeExtension.php @@ -38,6 +38,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, } $className = $scope->resolveName($methodCall->class); + /* @phpstan-ignore-next-line phpstanApi.runtimeReflection */ if (!\is_a($className, Arrayy::class, true)) { return null; } diff --git a/src/Type/DetectFirstValueTypeCollection.php b/src/Type/DetectFirstValueTypeCollection.php index b0693a1..46aefc4 100644 --- a/src/Type/DetectFirstValueTypeCollection.php +++ b/src/Type/DetectFirstValueTypeCollection.php @@ -40,6 +40,7 @@ public function __construct( */ if ($data instanceof Arrayy) { $firstValue = $data->first(); + /* @phpstan-ignore-next-line function.alreadyNarrowedType */ } elseif (\is_array($data)) { $firstValue = array_first($data); } else { 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..12cdc92 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,6 +1881,7 @@ public function testAdd(): void $resultArrayy = $arrayy->add(3); $array[] = 3; + /* @phpstan-ignore-next-line argument.type */ self::assertMutable($arrayy, $resultArrayy, $array); } @@ -1906,6 +1893,7 @@ public function testAppendImmutableYield(): void $arrayResult = $array; $arrayResult[] = 3; + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $arrayResult); } @@ -1916,14 +1904,15 @@ public function testPrependImmutableYield(): void $resultArrayy = $arrayy->prependImmutable(3); $arrayResult = [3, 3 => 1, 2]; + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $arrayResult); } /** * @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 +1925,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 +1938,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 +1951,7 @@ public function testAppendToEachValue($array, $result): void /** * @dataProvider averageProvider() * - * @param array $array + * @param array $array * @param mixed $value * @param float|int $expected */ @@ -2156,7 +2145,7 @@ public function testChangeKeyCase(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testChunk(array $array): void { @@ -2164,6 +2153,7 @@ public function testChunk(array $array): void $resultArrayy = $arrayy->chunk(2); $resultArray = \array_chunk($array, 2); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // --- @@ -2177,8 +2167,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 +2180,7 @@ public function testClean($array, $result): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testClear(array $array): void { @@ -2298,7 +2288,7 @@ public function testConstructWithArray(): void /** * @dataProvider containsOnlyProvider() * - * @param array $array + * @param array $array * @param mixed $value * @param bool $expected */ @@ -2312,7 +2302,7 @@ public function testContainsOnly($array, $value, $expected): void /** * @dataProvider containsProvider() * - * @param array $array + * @param array $array * @param mixed $value * @param bool $expected */ @@ -2328,7 +2318,7 @@ public function testContains($array, $value, $expected): void /** * @dataProvider containsCaseInsensitiveProvider() * - * @param array $array + * @param array $array * @param mixed $value * @param bool $expected */ @@ -2343,7 +2333,7 @@ public function testContainsCaseInsensitive($array, $value, $expected): void /** * @dataProvider containsCaseInsensitiveProviderRecursive() * - * @param array $array + * @param array $array * @param mixed $value * @param bool $expected */ @@ -2416,7 +2406,7 @@ public function testContainsKeysRecursive(): void /** * @dataProvider containsProviderRecursive() * - * @param array $array + * @param array $array * @param mixed $value * @param bool $expected */ @@ -2440,7 +2430,7 @@ public function testContainsValues(): void /** * @dataProvider countProvider() * - * @param array $array + * @param array $array * @param int $expected */ public function testCount($array, $expected): void @@ -2457,7 +2447,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,12 +2566,14 @@ public function testCreateFromString($string, $separator): void } else { $array = [$string]; } + /* @phpstan-ignore-next-line function.alreadyNarrowedType */ \assert(\is_array($array)); $arrayy = new A($array); $resultArrayy = A::createFromString($string, $separator); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $array); } @@ -2593,6 +2585,7 @@ public function testCreateFromTraversableImmutable(): void $resultArrayy = A::createFromTraversableImmutable($iterator); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $array); } @@ -2650,7 +2643,7 @@ public function testCreateWithRange(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testCustomSort(array $array): void { @@ -2673,7 +2666,7 @@ public function testCustomSort(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testCustomSortImmutable(array $array): void { @@ -2690,13 +2683,14 @@ public function testCustomSortImmutable(array $array): void $resultArray = $array; \usort($resultArray, $callable); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testCustomSortKeys(array $array): void { @@ -2780,6 +2774,7 @@ public function testCustomSortKeysSimple(): void 'two' => 2, ]; static::assertSame($expected, $resultArrayy->getArray()); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $input, $resultArrayy->getArray()); } @@ -2808,8 +2803,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 +2824,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,7 +2852,9 @@ public function testCustomSortValuesByDateTimeObject(): void /* @var $resultMatch Arrayy|Arrayy[] */ $resultMatch = $birthDatesAraayy->reduce($closureReduce); + /* @phpstan-ignore-next-line method.nonObject */ $thisYear = $resultMatch['thisYear']->customSortValues($closureSort); + /* @phpstan-ignore-next-line method.nonObject */ $nextYear = $resultMatch['nextYear']->customSortValues($closureSort); $resultMatch = $nextYear->reverse()->mergePrependNewIndex($thisYear->reverse()->getArray()); @@ -2894,9 +2891,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 +2905,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 +2919,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 +2997,7 @@ public function testDiffRecursive(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testDiffWith(array $array): void { @@ -3014,6 +3011,7 @@ public function testDiffWith(array $array): void $resultArrayy = $arrayy->diff($secondArray); $resultArray = \array_diff($array, $secondArray); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); } @@ -3063,10 +3061,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,6 +3137,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 { + /* @phpstan-ignore-next-line notIdentical.alwaysTrue */ return ($value % 2 !== 0) && ($key & 2 !== 0); }, \ARRAY_FILTER_USE_BOTH @@ -3172,16 +3171,19 @@ public function testFilterBy(): void $b = $arrayy->filterBy('name', 'baz'); static::assertCount(1, $b); /** @noinspection OffsetOperationsInspection */ + /* @phpstan-ignore-next-line offsetAccess.notFound */ static::assertSame(2365, $b[0]['value']); $b = $arrayy->filterBy('name', ['baz']); static::assertCount(1, $b); /** @noinspection OffsetOperationsInspection */ + /* @phpstan-ignore-next-line offsetAccess.notFound */ static::assertSame(2365, $b[0]['value']); $c = $arrayy->filterBy('value', 2468); static::assertCount(1, $c); /** @noinspection OffsetOperationsInspection */ + /* @phpstan-ignore-next-line offsetAccess.notFound */ static::assertSame('primary', $c[0]['group']); $d = $arrayy->filterBy('group', 'primary'); @@ -3190,6 +3192,7 @@ public function testFilterBy(): void $e = $arrayy->filterBy('value', 2000, 'lt'); static::assertCount(1, $e); /** @noinspection OffsetOperationsInspection */ + /* @phpstan-ignore-next-line offsetAccess.notFound */ static::assertSame(1468, $e[0]['value']); $e = $arrayy->filterBy('value', [2468, 2365], 'contains'); @@ -3207,7 +3210,7 @@ public function testFilterBy(): void /** * @dataProvider findProvider() * - * @param array $array + * @param array $array * @param mixed $search * @param false|mixed $result */ @@ -3224,7 +3227,7 @@ public function testFind($array, $search, $result): void } /** - * @return array + * @return array */ public function findKeyProvider(): array { @@ -3246,7 +3249,7 @@ public function findKeyProvider(): array /** * @dataProvider findKeyProvider() * - * @param array $array + * @param array $array * @param mixed $search * @param false|mixed $result */ @@ -3305,8 +3308,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 +3321,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 +3371,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 +3403,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 +3415,7 @@ public function testHas($expected, $array, $key): void /** * @dataProvider implodeProvider() * - * @param array $array + * @param array $array * @param string $result * @param string $with */ @@ -3426,7 +3429,7 @@ public function testImplode($array, $result, $with = ','): void /** * @dataProvider implodeKeysProvider() * - * @param array $array + * @param array $array * @param string $result * @param string $with */ @@ -3464,8 +3467,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 +3625,7 @@ public function testIsArrayMultidim(): void /** * @dataProvider isAssocProvider() * - * @param array $array + * @param array $array * @param bool $result */ public function testIsAssoc($array, $result): void @@ -3879,8 +3882,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 +3950,7 @@ public function testMagicSetViaDotNotation(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testMap(array $array): void { @@ -3957,6 +3960,7 @@ public function testMap(array $array): void $arrayy = new A($array); $resultArrayy = $arrayy->map($callable); $resultArray = \array_map($callable, $array); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // --- @@ -3970,6 +3974,7 @@ public function testMap(array $array): void /* @phpstan-ignore argument.type */ $resultArrayy = $arrayy->map('str_repeat', false, 2); $resultArray = \array_map($callable, $array); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); } @@ -3983,7 +3988,7 @@ public function testMapSimpleExample(): void /** * @dataProvider matchesProvider() * - * @param array $array + * @param array $array * @param mixed $search * @param bool $result */ @@ -4007,8 +4012,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 +4074,7 @@ public function testMatchesSimple(): void /** * @dataProvider maxProvider() * - * @param array $array + * @param array $array * @param mixed $expected */ public function testMax($array, $expected): void @@ -4127,9 +4132,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 +4146,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 +4160,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 +4174,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 +4188,7 @@ public function testMergePrependNewIndex($array, $arrayNew, $result): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testMergePrependNewIndexV2(array $array): void { @@ -4197,13 +4202,14 @@ public function testMergePrependNewIndexV2(array $array): void $resultArrayy = $arrayy->mergePrependNewIndex($secondArray); $resultArray = \array_merge($secondArray, $array); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testMergeToRecursively(array $array): void { @@ -4217,13 +4223,14 @@ public function testMergeToRecursively(array $array): void $resultArrayy = $arrayy->mergePrependNewIndex($secondArray, true); $resultArray = \array_merge_recursive($secondArray, $array); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testMergeWith(array $array): void { @@ -4237,13 +4244,14 @@ public function testMergeWith(array $array): void $resultArrayy = $arrayy->mergeAppendNewIndex($secondArray); $resultArray = \array_merge($array, $secondArray); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testMergeWithRecursively(array $array): void { @@ -4257,13 +4265,14 @@ public function testMergeWithRecursively(array $array): void $resultArrayy = $arrayy->mergeAppendNewIndex($secondArray, true); $resultArray = \array_merge_recursive($array, $secondArray); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); } /** * @dataProvider minProvider() * - * @param array $array + * @param array $array * @param mixed $expected */ public function testMin($array, $expected): void @@ -4378,7 +4387,7 @@ public function testNested(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testOffsetNullSet(array $array): void { @@ -4395,7 +4404,7 @@ public function testOffsetNullSet(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testOffsetSet(array $array): void { @@ -4412,7 +4421,7 @@ public function testOffsetSet(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testOffsetUnset(array $array): void { @@ -4431,7 +4440,7 @@ public function testOffsetUnset(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testDeleteKey(array $array): void { @@ -4477,10 +4486,22 @@ public function testOffsetUnsetViaDotNotation(): void static::assertSame([0 => 'a', 'b' => []], $array); static::assertSame($array, $arrayy->toArray()); + /* @phpstan-ignore-next-line isset.offset, staticMethod.alreadyNarrowedType */ static::assertFalse(isset($array[$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 +4600,7 @@ public function testOrderByValueNewIndex(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testPad(array $array): void { @@ -4587,13 +4608,14 @@ public function testPad(array $array): void $resultArrayy = $arrayy->pad(10, 5); $resultArray = \array_pad($array, 10, 5); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testPop(array $array): void { @@ -4609,8 +4631,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 +4678,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 +4698,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 +4711,7 @@ public function testPrependToEachValue($array, $result): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testPush(array $array): void { @@ -4707,7 +4729,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 +4782,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 +4816,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 +4838,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 +4853,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 +4867,7 @@ public function testTestgetValues(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetValuesYield(array $array): void { @@ -4864,7 +4886,7 @@ public function testGetValuesYield(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetGetBackwardsGenerator(array $array): void { @@ -4883,7 +4905,7 @@ public function testGetGetBackwardsGenerator(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testReindex(array $array): void { @@ -4983,9 +5005,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 { @@ -4997,8 +5019,8 @@ public function testRemove($array, $key, $result): void /** * @dataProvider removeFirstProvider() * - * @param array $array - * @param array $result + * @param array $array + * @param array $result */ public function testRemoveFirst($array, $result): void { @@ -5010,8 +5032,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 +5045,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 +5059,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,6 +5180,7 @@ public function testReplaceAllValues(): void $resultArrayy = $arrayy->replaceAllValues($secondArray); $resultArray = (array) \array_combine($firstArray, $secondArray); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $firstArray, $resultArray); } @@ -5189,7 +5212,7 @@ public function testReplaceAllValuesV2(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testReplaceIn(array $array): void { @@ -5209,7 +5232,7 @@ public function testReplaceIn(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testReplaceInRecursively(array $array): void { @@ -5239,6 +5262,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 +5307,7 @@ public function testReplaceValues(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testReplaceWith(array $array): void { @@ -5295,7 +5327,7 @@ public function testReplaceWith(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testReplaceWithRecursively(array $array): void { @@ -5315,8 +5347,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 +5361,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 +5375,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 +5388,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 +5417,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 +5439,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 +5455,7 @@ public function testSerializeSimple(): void /** * @dataProvider setProvider() * - * @param array $array + * @param array $array * @param mixed $key * @param mixed $value */ @@ -5449,7 +5469,7 @@ public function testSet($array, $key, $value): void /** * @dataProvider setAndGetProvider() * - * @param array $array + * @param array $array * @param mixed $key * @param mixed $value */ @@ -5519,11 +5539,13 @@ public function testSetViaDotNotation(): void { $arrayy = new A(['Lars' => ['lastname' => 'Moelleken']]); + /* @phpstan-ignore-next-line method.nonObject */ static::assertSame(['lastname' => 'Moelleken'], $arrayy['Lars']->getArray()); $result = $arrayy->get('Lars.lastname'); static::assertSame('Moelleken', $result); + /* @phpstan-ignore-next-line method.nonObject */ static::assertSame(['lastname' => 'Moelleken'], $arrayy['Lars']->getArray()); /* @phpstan-ignore property.notFound */ @@ -5532,6 +5554,7 @@ public function testSetViaDotNotation(): void /* @phpstan-ignore property.notFound */ static::assertSame('Moelleken', $arrayy->Lars->lastname); + /* @phpstan-ignore-next-line offsetAccess.notFound */ static::assertSame('Moelleken', $arrayy['Lars']['lastname']); $tmp = $arrayy['Lars']; @@ -5584,7 +5607,7 @@ public function testSetViaDotNotation(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testShift(array $array): void { @@ -5715,7 +5738,7 @@ public function testSimpleRandomWeighted(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSlice(array $array): void { @@ -5723,6 +5746,7 @@ public function testSlice(array $array): void $resultArrayy = $arrayy->slice(1, 1); $resultArray = \array_slice($array, 1, 1); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); } @@ -5774,7 +5798,7 @@ static function ($value) { /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSortAscWithPreserveKeys(array $array): void { @@ -5801,13 +5825,14 @@ public function testSortAscWithPreserveKeys(array $array): void $arrayV2 = new A($array); $resultArrayV2 = $arrayV2->asortImmutable(); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSortAscWithoutPreserveKeys(array $array): void { @@ -5831,7 +5856,7 @@ public function testSortAscWithoutPreserveKeys(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSortDescWithPreserveKeys(array $array): void { @@ -5855,7 +5880,7 @@ public function testSortDescWithPreserveKeys(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSortImmutableDescWithPreserveKeys(array $array): void { @@ -5864,6 +5889,7 @@ public function testSortImmutableDescWithPreserveKeys(array $array): void $resultArray = $array; \arsort($resultArray, \SORT_REGULAR); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArray); // --- @@ -5873,13 +5899,14 @@ public function testSortImmutableDescWithPreserveKeys(array $array): void $arrayV2 = new A($array); $resultArrayV2 = $arrayV2->arsortImmutable(); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSortDescWithoutPreserveKeys(array $array): void { @@ -5906,14 +5933,15 @@ public function testSortDescWithoutPreserveKeys(array $array): void $arrayV2 = new A($array); $resultArrayV2 = $arrayV2->rsortImmutable(); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); } /** * @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 +5954,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 +5981,14 @@ public function testSortKeysAsc(array $array): void $arrayV2 = new A($array); $resultArrayV2 = $arrayV2->ksortImmutable(); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testNatcasesort(array $array): void { @@ -5975,7 +6004,7 @@ public function testNatcasesort(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testNatsortImmutable(array $array): void { @@ -5985,13 +6014,14 @@ public function testNatsortImmutable(array $array): void $arrayResult = $arrayyResult->getArray(); static::assertSame($array, $arrayResult); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $arrayyResult, $array, $arrayResult); } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testNatsort(array $array): void { @@ -6007,7 +6037,7 @@ public function testNatsort(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testNatcasesortImmutable(array $array): void { @@ -6017,13 +6047,14 @@ public function testNatcasesortImmutable(array $array): void $arrayResult = $arrayyResult->getArray(); static::assertSame($array, $arrayResult); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $arrayyResult, $array, $arrayResult); } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testUasort(array $array): void { @@ -6046,7 +6077,7 @@ public function testUasort(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testUasortImmutable(array $array): void { @@ -6063,13 +6094,14 @@ public function testUasortImmutable(array $array): void $arrayResult = $arrayyResult->getArray(); static::assertSame($array, $arrayResult); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $arrayyResult, $array, $arrayResult); } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testSortKeysDesc(array $array): void { @@ -6096,6 +6128,7 @@ public function testSortKeysDesc(array $array): void $arrayV2 = new A($array); $resultArrayV2 = $arrayV2->krsortImmutable(); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $resultArrayV2->getArray()); } @@ -6172,20 +6205,21 @@ 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); + /* @phpstan-ignore-next-line argument.type */ self::assertImmutable($arrayy, $resultArrayy, $array, $array); } /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testStaticCreateFromGeneratorImmutableFromArray(array $array): void { @@ -6199,7 +6233,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 +6253,7 @@ static function () use ($arrayy) { /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testStaticCreateFromJson(array $array): void { @@ -6234,7 +6268,7 @@ public function testStaticCreateFromJson(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testStaticCreateFromObject(array $array): void { @@ -6334,6 +6368,7 @@ public function testStaticCreateFromString($string, $separator): void } else { $array = [$string]; } + /* @phpstan-ignore-next-line function.alreadyNarrowedType */ \assert(\is_array($array)); $arrayy = A::create($array); @@ -6369,9 +6404,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 +6418,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 +6432,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 +6442,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 +6457,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 +6491,7 @@ public function testUnsetSimple(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testUnshift(array $array): void { @@ -6480,10 +6515,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 +6557,7 @@ public function testWalk(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testWalkRecursively(array $array): void { @@ -6547,7 +6602,7 @@ public function testWalkSimpleRecursively(): void } /** - * @return array + * @return array */ public function toStringProvider(): array { @@ -6562,7 +6617,7 @@ public function toStringProvider(): array } /** - * @return array + * @return array */ public function uniqueProvider(): array { @@ -6619,7 +6674,7 @@ public function uniqueProvider(): array } /** - * @return array + * @return array */ public function uniqueProviderKeepIndex(): array { @@ -6678,8 +6733,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 +6746,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..1dd5a96 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,6 +398,7 @@ public function testGetRandom(array $array): void static::assertNotNull($value[0]); static::assertContains($value[0], $arrayy->toArray()); } else { + /* @phpstan-ignore-next-line staticMethod.alreadyNarrowedType */ static::assertIsArray($value); } } @@ -405,7 +406,7 @@ public function testGetRandom(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetRandomKey(array $array): void { @@ -415,9 +416,11 @@ public function testGetRandomKey(array $array): void /** @var array-key $key */ $key = $arrayy->getRandomKey(); + /* @phpstan-ignore-next-line staticMethod.alreadyNarrowedType */ static::assertNotNull($key); static::assertArrayHasKey($key, $arrayy->toArray()); } else { + /* @phpstan-ignore-next-line staticMethod.alreadyNarrowedType */ static::assertIsArray($arrayy->getArray()); } } @@ -425,13 +428,14 @@ public function testGetRandomKey(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetRandomKeys(array $array): void { $arrayy = $this->createArrayy($array); if (\count($array) < 2) { + /* @phpstan-ignore-next-line staticMethod.alreadyNarrowedType */ static::assertIsArray($arrayy->getArray()); } else { $keys = $arrayy->getRandomKeys(2); @@ -462,17 +466,19 @@ 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) { + /* @phpstan-ignore-next-line staticMethod.alreadyNarrowedType */ static::assertIsArray($arrayy->getArray()); } else { $keys = $arrayy->getRandomKeys(\count($array))->getArray(); + /* @phpstan-ignore-next-line staticMethod.alreadyNarrowedType */ static::assertIsArray($keys); } } @@ -480,13 +486,14 @@ public function testGetRandomKeysShouldReturnArray(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testGetRandomValueSingle(array $array): void { $arrayy = $this->createArrayy($array); if (\count($array) === 0) { + /* @phpstan-ignore-next-line staticMethod.alreadyNarrowedType */ static::assertIsArray($arrayy->getArray()); } else { $value = $arrayy->getRandomValue(); @@ -503,13 +510,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) { + /* @phpstan-ignore-next-line staticMethod.alreadyNarrowedType */ static::assertIsArray($arrayy->getArray()); return; @@ -528,13 +536,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) { + /* @phpstan-ignore-next-line staticMethod.alreadyNarrowedType */ static::assertIsArray($arrayy->getArray()); return; @@ -543,6 +552,7 @@ public function testGetRandomValuesSingle(array $array): void $values = $arrayy->getRandomValues(1)->getArray(); static::assertCount(1, $values); + /* @phpstan-ignore-next-line staticMethod.alreadyNarrowedType */ static::assertIsArray($arrayy->getArray()); foreach ($values as $value) { if (!$value instanceof \Arrayy\Arrayy) { @@ -554,7 +564,7 @@ public function testGetRandomValuesSingle(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testIndexOf(array $array): void { @@ -569,7 +579,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 +593,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 +606,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 +620,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 +634,7 @@ public function testKey(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testLast(array $array): void { @@ -646,7 +656,7 @@ public function testLast(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testLastKey(array $array): void { @@ -669,7 +679,7 @@ public function testLastKey(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testArrayyFirst(array $array): void { @@ -691,7 +701,7 @@ public function testArrayyFirst(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testArrayyLast(array $array): void { @@ -713,7 +723,7 @@ public function testArrayyLast(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testFirstKey(array $array): void { @@ -731,15 +741,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 +786,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,6 +807,7 @@ public function testMostUsedValues(array $array): void $firsts = []; } + /* @phpstan-ignore-next-line instanceof.alwaysTrue */ if ($result instanceof Arrayy) { $result = $result->getArray(); } @@ -809,7 +818,7 @@ public function testMostUsedValues(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testNext(array $array): void { @@ -822,7 +831,7 @@ public function testNext(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testOffsetExists(array $array): void { @@ -837,7 +846,7 @@ public function testOffsetExists(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testOffsetGet(array $array): void { @@ -852,7 +861,7 @@ public function testOffsetGet(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testPrevious(array $array): void { @@ -865,7 +874,7 @@ public function testPrevious(array $array): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testReIndex(array $array): void { @@ -975,7 +984,7 @@ public function testGetListViaGenerator(): void /** * @dataProvider simpleArrayProvider * - * @param array $array + * @param array $array */ public function testToJson(array $array): void { @@ -995,6 +1004,7 @@ public function testToJson(array $array): void public function testToString($string, $separator): void { $array = \explode($separator, $string); + /* @phpstan-ignore-next-line function.alreadyNarrowedType */ \assert(\is_array($array)); $arrayy = $this->createArrayy($array); @@ -1007,9 +1017,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 */ + /* @phpstan-ignore-next-line generics.lessTypes, missingType.iterableValue */ protected function assertImmutable(A $arrayy, A $resultArrayy, array $array, array $resultArray): void { static::assertNotSame($arrayy, $resultArrayy); @@ -1020,8 +1031,9 @@ protected function assertImmutable(A $arrayy, A $resultArrayy, array $array, arr /** * @param A $arrayy * @param A $resultArrayy - * @param array $resultArray + * @param array $resultArray */ + /* @phpstan-ignore-next-line generics.lessTypes, missingType.iterableValue */ protected function assertMutable(A $arrayy, A $resultArrayy, array $resultArray): void { static::assertSame($arrayy, $resultArrayy); @@ -1032,12 +1044,14 @@ protected function assertMutable(A $arrayy, A $resultArrayy, array $resultArray) // The method list order by ASC /** - * @param array $array + * @param array $array * * @return A */ + /* @phpstan-ignore-next-line generics.lessTypes, missingType.iterableValue */ protected function createArrayy(array $array = []): A { + /* @phpstan-ignore-next-line return.type */ return new $this->arrayyClassName($array); } } diff --git a/tests/Collection/BoolTypeTest.php b/tests/Collection/BoolTypeTest.php index 444c6d8..a068cf1 100644 --- a/tests/Collection/BoolTypeTest.php +++ b/tests/Collection/BoolTypeTest.php @@ -44,6 +44,7 @@ public function testBoolArray(): void } \assert(\is_bool($test)); + /* @phpstan-ignore-next-line staticMethod.alreadyNarrowedType */ static::assertTrue($test); } diff --git a/tests/Collection/CollectionTest.php b/tests/Collection/CollectionTest.php index 914aa56..8186b3c 100644 --- a/tests/Collection/CollectionTest.php +++ b/tests/Collection/CollectionTest.php @@ -93,13 +93,19 @@ public function testUserDataCollectionFromJsonMulti(): void $userDataCollection->getAll(); $userData0 = $userDataCollection[0]; + /* @phpstan-ignore-next-line property.nonObject */ static::assertSame('Lars', $userData0->firstName); + /* @phpstan-ignore-next-line property.nonObject */ static::assertInstanceOf(CityData::class, $userData0->city); + /* @phpstan-ignore-next-line property.nonObject */ static::assertSame('Düsseldorf', $userData0->city->name); $userData1 = $userDataCollection[1]; + /* @phpstan-ignore-next-line property.nonObject */ static::assertSame('Sven', $userData1->firstName); + /* @phpstan-ignore-next-line property.nonObject */ static::assertInstanceOf(CityData::class, $userData1->city); + /* @phpstan-ignore-next-line property.nonObject */ static::assertSame('Köln', $userData1->city->name); } @@ -136,6 +142,7 @@ public function testJsonSerializableCollection(): void $first = $jsonSerializableCollection->first(); if ($first) { + /* @phpstan-ignore-next-line function.alreadyNarrowedType, instanceof.alwaysTrue */ \assert($first instanceof \Arrayy\Arrayy); static::assertSame('fooooo', $first->get('foo')); } @@ -418,6 +425,7 @@ public function testWithGeneratorsV1(): void foreach ($barCollection as $item) { static::assertInstanceOf(ModelInterface::class, $item); + /* @phpstan-ignore-next-line instanceof.alwaysTrue */ if ($item instanceof ModelInterface) { static::assertStringStartsWith('foo', $item->getFoo()); } diff --git a/tests/Collection/StringTypeTest.php b/tests/Collection/StringTypeTest.php index ec4f5e2..1bd2865 100644 --- a/tests/Collection/StringTypeTest.php +++ b/tests/Collection/StringTypeTest.php @@ -22,6 +22,7 @@ public function testArraySimple(): void $strings[] = 'A'; $strings[] = 'B'; $strings[] = 'C'; + /* @phpstan-ignore-next-line offsetAssign.valueType */ $strings[] = 1.0; } @@ -51,7 +52,7 @@ public function testWrongValue(): void { $this->expectException(\TypeError::class); - /* @phpstan-ignore offsetAssign.valueType */ + /* @phpstan-ignore-next-line argument.type */ new StringCollection(['A', 'B', 'C', 1]); } diff --git a/tests/Collection/TypeTypeTest.php b/tests/Collection/TypeTypeTest.php index 3af2fc2..8670935 100644 --- a/tests/Collection/TypeTypeTest.php +++ b/tests/Collection/TypeTypeTest.php @@ -28,7 +28,7 @@ public function testWrongValue(): void /** @noinspection PhpParamsInspection */ /** @noinspection PhpStrictTypeCheckingInspection */ - /* @phpstan-ignore argument.type */ + /* @phpstan-ignore-next-line argument.type */ new Collection(\stdClass::class, [new \stdClass(), 'A']); } } diff --git a/tests/Collection/TypesTest.php b/tests/Collection/TypesTest.php index 6389a68..8a35c23 100644 --- a/tests/Collection/TypesTest.php +++ b/tests/Collection/TypesTest.php @@ -58,6 +58,7 @@ public function testChainMethods(): void $users = UserDataCollection::createFromGeneratorFunction($data); $names = $users + /* @phpstan-ignore-next-line argument.type */ ->filter(static function (UserData $person): bool { return $person->id <= 30; }) diff --git a/tests/JsonMapperCoverageTest.php b/tests/JsonMapperCoverageTest.php index a8d599f..7ee273d 100644 --- a/tests/JsonMapperCoverageTest.php +++ b/tests/JsonMapperCoverageTest.php @@ -18,6 +18,7 @@ public function testMapRejectsNonObjectTargets(): void $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('JsonMapper::map() requires second argument to be an object, integer given.'); + /* @phpstan-ignore-next-line argument.templateType, argument.type (the invalid target is the subject of this test) */ (new Json())->map([], 123); } @@ -37,6 +38,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..25ce80a 100644 --- a/tests/JsonMapperTest.php +++ b/tests/JsonMapperTest.php @@ -14,6 +14,7 @@ public function testJsonMappingV1(): void $found = false; + /* @phpstan-ignore-next-line argument.type */ GetAccountsResponse::createFromJsonMapper($json) ->accounts ->each(function (Account $a) use (&$found) { 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..897b332 100644 --- a/tests/PHPStan/AnalyseTest.php +++ b/tests/PHPStan/AnalyseTest.php @@ -22,6 +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); + /* @phpstan-ignore-next-line booleanOr.rightAlwaysTrue, staticMethod.alreadyNarrowedType */ static::assertTrue(($user->city->name ?? null) === null || is_string($user->city->name ?? null)); } @@ -31,7 +32,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 +64,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 +74,7 @@ public function testGenerics(): void // ------------------------------------------------------------------------- + /** @var \Arrayy\Type\DetectFirstValueTypeCollection $set */ $set = new \Arrayy\Type\DetectFirstValueTypeCollection([new \stdClass(), new \stdClass()]); foreach ($set as $item) { @@ -106,6 +109,7 @@ public function testGenerics(): void return $value === $search; }; \PHPStan\Testing\assertType('bool|float|int|string', $set->find($closure)); + /* @phpstan-ignore-next-line function.alreadyNarrowedType, staticMethod.alreadyNarrowedType */ static::assertTrue(is_scalar($set->find($closure))); // ------------------------------------------------------------------------- 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/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..645e02d 100644 --- a/tests/TypeCheckCoreCoverageTest.php +++ b/tests/TypeCheckCoreCoverageTest.php @@ -208,14 +208,22 @@ public function testFromPhpDocumentorPropertyParsesSupportedPseudoTypes(): void static::assertContainsOnlyInstancesOf(Property::class, $tags); + /* @phpstan-ignore-next-line argument.type */ $scalarTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[0]); + /* @phpstan-ignore-next-line argument.type */ $callableTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[1]); + /* @phpstan-ignore-next-line argument.type */ $objectTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[2]); + /* @phpstan-ignore-next-line argument.type */ $arrayTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[3]); + /* @phpstan-ignore-next-line method.nonObject */ static::assertSame(['string|int|float|bool'], $scalarTypeCheck->getTypes()); + /* @phpstan-ignore-next-line method.nonObject */ static::assertSame(['callable'], $callableTypeCheck->getTypes()); + /* @phpstan-ignore-next-line method.nonObject */ static::assertSame(['\\ArrayObject'], $objectTypeCheck->getTypes()); + /* @phpstan-ignore-next-line method.nonObject */ static::assertSame(['string[]'], $arrayTypeCheck->getTypes()); } @@ -233,6 +241,7 @@ public function testFromDocTypeObjectNullableProducesNullableChecker(): void DOC); $tag = $docBlock->getTagsByName('property')[0]; + /* @phpstan-ignore-next-line method.notFound */ $checker = TypeCheckPhpDoc::fromDocTypeObject('city', $tag->getType()); static::assertSame(['\\ArrayObject', 'null'], $checker->getTypes()); @@ -256,6 +265,7 @@ public function testFromDocTypeObjectNestedArrayShapeYieldsArrayType(): void * @template T of array{data: array{x: int}} */ DOC); + /* @phpstan-ignore-next-line method.notFound */ $bound = $docBlock->getTagsByName('template')[0]->getBound(); $nestedShapeType = $bound->getItems()[0]->getValue(); // array{x: int} @@ -282,6 +292,7 @@ public function testArrayShapeOptionalKeyAcceptsValidObjectValue(): void 'infos' => ['lall'], ]); + /* @phpstan-ignore-next-line argument.type */ $model = new TypeCheckArrayShapeUserData([ $meta->id => 1, $meta->firstName => 'Lars', @@ -303,6 +314,7 @@ public function testArrayShapeOptionalNullableKeyAcceptsExplicitNull(): void { $meta = TypeCheckArrayShapeUserData::meta(); + /* @phpstan-ignore-next-line argument.type */ $model = new TypeCheckArrayShapeUserData([ $meta->id => 1, $meta->firstName => 'Lars', @@ -334,11 +346,17 @@ public function testFromPhpDocumentorPropertyParsesScalarNullAndMixedKeywords(): $tags = $docBlock->getTagsByName('property'); + /* @phpstan-ignore-next-line argument.type */ $boolTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[0]); + /* @phpstan-ignore-next-line argument.type */ $floatTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[1]); + /* @phpstan-ignore-next-line argument.type */ $stringTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[2]); + /* @phpstan-ignore-next-line argument.type */ $intTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[3]); + /* @phpstan-ignore-next-line argument.type */ $mixedTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[4]); + /* @phpstan-ignore-next-line argument.type */ $nullTypeCheck = TypeCheckPhpDoc::fromPhpDocumentorProperty($tags[5]); $boolValue = true; @@ -348,17 +366,29 @@ public function testFromPhpDocumentorPropertyParsesScalarNullAndMixedKeywords(): $mixedValue = ['foo' => 'bar']; $nullValue = null; + /* @phpstan-ignore-next-line method.nonObject */ static::assertSame(['bool'], $boolTypeCheck->getTypes()); + /* @phpstan-ignore-next-line method.nonObject */ static::assertSame(['float'], $floatTypeCheck->getTypes()); + /* @phpstan-ignore-next-line method.nonObject */ static::assertSame(['string'], $stringTypeCheck->getTypes()); + /* @phpstan-ignore-next-line method.nonObject */ static::assertSame(['int'], $intTypeCheck->getTypes()); + /* @phpstan-ignore-next-line method.nonObject */ static::assertSame(['mixed'], $mixedTypeCheck->getTypes()); + /* @phpstan-ignore-next-line method.nonObject */ static::assertSame(['null'], $nullTypeCheck->getTypes()); + /* @phpstan-ignore-next-line method.nonObject */ static::assertTrue($boolTypeCheck->checkType($boolValue)); + /* @phpstan-ignore-next-line method.nonObject */ static::assertTrue($floatTypeCheck->checkType($floatValue)); + /* @phpstan-ignore-next-line method.nonObject */ static::assertTrue($stringTypeCheck->checkType($stringValue)); + /* @phpstan-ignore-next-line method.nonObject */ static::assertTrue($intTypeCheck->checkType($intValue)); + /* @phpstan-ignore-next-line method.nonObject */ static::assertTrue($mixedTypeCheck->checkType($mixedValue)); + /* @phpstan-ignore-next-line method.nonObject */ static::assertTrue($nullTypeCheck->checkType($nullValue)); } @@ -445,6 +475,7 @@ public function testTypeCheckCallbackValidatesAndSupportsNullableValues(): void public function testArrayShapeTemplateProvidesPropertyDefinitions(): void { $meta = TypeCheckArrayShapeUserData::meta(); + /* @phpstan-ignore-next-line argument.type */ $model = new TypeCheckArrayShapeUserData([ $meta->id => 1, $meta->firstName => 'Lars', @@ -463,6 +494,7 @@ public function testArrayShapeTemplateRejectsInvalidPropertyTypes(): void $this->expectExceptionMessage('Invalid type: expected "infos" to be of type {string[]}'); $meta = TypeCheckArrayShapeUserData::meta(); + /* @phpstan-ignore-next-line argument.type */ new TypeCheckArrayShapeUserData([ $meta->id => 1, $meta->firstName => 'Lars', @@ -477,6 +509,7 @@ public function testArrayShapeTemplateRejectsUnknownProperties(): void $this->expectExceptionMessage('The key "unknown" does not exist'); $meta = TypeCheckArrayShapeUserData::meta(); + /* @phpstan-ignore-next-line argument.type */ new TypeCheckArrayShapeUserData([ $meta->id => 1, $meta->firstName => 'Lars', @@ -513,6 +546,7 @@ public function testArrayShapeOptionalKeyIsTypeCheckedWhenPresent(): void $this->expectExceptionMessage('Invalid type'); $meta = TypeCheckArrayShapeUserData::meta(); + /* @phpstan-ignore-next-line argument.type */ new TypeCheckArrayShapeUserData([ $meta->id => 1, $meta->firstName => 'Lars', @@ -648,6 +682,7 @@ public function testFromDocTypeObjectWithParsedTypeKeepsPropertyNameInErrors(): DOC); $tag = $docBlock->getTagsByName('property')[0]; + /* @phpstan-ignore-next-line method.notFound */ $checker = TypeCheckPhpDoc::fromDocTypeObject('myProp', $tag->getType()); static::assertSame(['int'], $checker->getTypes()); @@ -685,6 +720,7 @@ public function testArrayShapePostConstructionTypeCheckEnforced(): void $this->expectExceptionMessageMatches('#Invalid type: expected "id" to be of type \{int\}#'); $meta = TypeCheckArrayShapeUserData::meta(); + /* @phpstan-ignore-next-line argument.type */ $model = new TypeCheckArrayShapeUserData([ $meta->id => 1, $meta->firstName => 'Lars', @@ -705,6 +741,7 @@ public function testArrayShapePostConstructionMismatchEnforced(): void $this->expectExceptionMessage('The key "ghost" does not exist'); $meta = TypeCheckArrayShapeUserData::meta(); + /* @phpstan-ignore-next-line argument.type */ $model = new TypeCheckArrayShapeUserData([ $meta->id => 1, $meta->firstName => 'Lars', @@ -727,6 +764,7 @@ public static function invalidStringArrayProvider(): iterable final class TypeCheckNoTypeFixture { + /* @phpstan-ignore-next-line missingType.property */ public $value; } @@ -740,6 +778,7 @@ final class TypeCheckDocTypesFixture /** * @var \ArrayObject */ + /* @phpstan-ignore-next-line missingType.generics */ public $objectValue; /** @@ -758,6 +797,7 @@ final class TypeCheckDocOverridesNativeFixture /** * @var int|string */ + /* @phpstan-ignore-next-line property.phpDocType */ public string $value = ''; } @@ -885,6 +925,7 @@ final class TypeCheckArrayShapeWrongTemplateName extends \Arrayy\Arrayy * * @extends stdClass */ +/* @phpstan-ignore-next-line generics.wrongParent, missingType.generics */ final class TypeCheckNonArrayyExtendsData extends \Arrayy\Arrayy { protected $checkPropertyTypes = true;