From 68f176893c587a076e82160f6d0600fd1c4a951d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 27 Dec 2025 02:55:10 +0100 Subject: [PATCH 1/8] cs --- src/Utils/Arrays.php | 4 ++-- src/Utils/FileInfo.php | 12 +++++------- src/Utils/Html.php | 6 +----- src/Utils/Image.php | 8 ++++---- src/Utils/Iterables.php | 2 +- src/Utils/ReflectionMethod.php | 2 +- src/Utils/Strings.php | 6 +++--- src/Utils/Validators.php | 6 +++--- 8 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/Utils/Arrays.php b/src/Utils/Arrays.php index 986118c8..663a337f 100644 --- a/src/Utils/Arrays.php +++ b/src/Utils/Arrays.php @@ -117,7 +117,7 @@ public static function searchKey(array $array, $key): ?int */ public static function contains(array $array, mixed $value): bool { - return in_array($value, $array, true); + return in_array($value, $array, strict: true); } @@ -293,7 +293,7 @@ public static function associate(array $array, $path): array|\stdClass : preg_split('#(\[\]|->|=|\|)#', $path, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); if (!$parts || $parts === ['->'] || $parts[0] === '=' || $parts[0] === '|') { - throw new Nette\InvalidArgumentException("Invalid path '$path'."); + throw new Nette\InvalidArgumentException("Invalid path '" . (is_array($path) ? implode('', $path) : $path) . "'."); } $res = $parts[0] === '->' ? new \stdClass : []; diff --git a/src/Utils/FileInfo.php b/src/Utils/FileInfo.php index 59dd7227..24e582e5 100644 --- a/src/Utils/FileInfo.php +++ b/src/Utils/FileInfo.php @@ -19,14 +19,12 @@ */ final class FileInfo extends \SplFileInfo { - private readonly string $relativePath; - - - public function __construct(string $file, string $relativePath = '') - { + public function __construct( + string $file, + private readonly string $relativePath = '', + ) { parent::__construct($file); - $this->setInfoClass(static::class); - $this->relativePath = $relativePath; + $this->setInfoClass(self::class); } diff --git a/src/Utils/Html.php b/src/Utils/Html.php index de24a964..55e70a51 100644 --- a/src/Utils/Html.php +++ b/src/Utils/Html.php @@ -764,10 +764,6 @@ final public function endTag(): string */ final public function attributes(): string { - if (!is_array($this->attrs)) { - return ''; - } - $s = ''; $attrs = $this->attrs; foreach ($attrs as $key => $value) { @@ -780,7 +776,7 @@ final public function attributes(): string continue; } elseif (is_array($value)) { - if (strncmp($key, 'data-', 5) === 0) { + if (str_starts_with($key, 'data-')) { $value = Json::encode($value); } else { diff --git a/src/Utils/Image.php b/src/Utils/Image.php index a6bb4782..062fb168 100644 --- a/src/Utils/Image.php +++ b/src/Utils/Image.php @@ -224,9 +224,9 @@ public static function fromBlank(int $width, int $height, ImageColor|array|null $image = new static(imagecreatetruecolor($width, $height)); if ($color) { - $image->alphablending(false); - $image->filledrectangle(0, 0, $width - 1, $height - 1, $color); - $image->alphablending(true); + $image->alphaBlending(false); + $image->filledRectangle(0, 0, $width - 1, $height - 1, $color); + $image->alphaBlending(true); } return $image; @@ -804,7 +804,7 @@ public function __serialize(): array public function resolveColor(ImageColor|array $color): int { - $color = $color instanceof ImageColor ? $color->toRGBA() : array_values($color); + $color = $color instanceof ImageColor ? $color->toRGBA() : array_values($color + ['alpha' => 0]); return imagecolorallocatealpha($this->image, ...$color) ?: imagecolorresolvealpha($this->image, ...$color); } diff --git a/src/Utils/Iterables.php b/src/Utils/Iterables.php index 28c9269c..93a5a5dd 100644 --- a/src/Utils/Iterables.php +++ b/src/Utils/Iterables.php @@ -215,7 +215,7 @@ public static function memoize(iterable $iterable): \IteratorAggregate { return new class (self::toIterator($iterable)) implements \IteratorAggregate { public function __construct( - private \Iterator $iterator, + private readonly \Iterator $iterator, private array $cache = [], ) { } diff --git a/src/Utils/ReflectionMethod.php b/src/Utils/ReflectionMethod.php index 2a8a55c6..26a79278 100644 --- a/src/Utils/ReflectionMethod.php +++ b/src/Utils/ReflectionMethod.php @@ -18,7 +18,7 @@ */ final class ReflectionMethod extends \ReflectionMethod { - private \ReflectionClass $originalClass; + private readonly \ReflectionClass $originalClass; public function __construct(object|string $objectOrMethod, ?string $method = null) diff --git a/src/Utils/Strings.php b/src/Utils/Strings.php index eb44b481..41a2e274 100644 --- a/src/Utils/Strings.php +++ b/src/Utils/Strings.php @@ -135,7 +135,7 @@ public static function substring(string $s, int $start, ?int $length = null): st public static function normalize(string $s): string { // convert to compressed normal form (NFC) - if (class_exists('Normalizer', false) && ($n = \Normalizer::normalize($s, \Normalizer::FORM_C)) !== false) { + if (class_exists('Normalizer', autoload: false) && ($n = \Normalizer::normalize($s, \Normalizer::FORM_C)) !== false) { $s = $n; } @@ -323,7 +323,7 @@ public static function capitalize(string $s): string */ public static function compare(string $left, string $right, ?int $length = null): bool { - if (class_exists('Normalizer', false)) { + if (class_exists('Normalizer', autoload: false)) { $left = \Normalizer::normalize($left, \Normalizer::FORM_D); // form NFD is faster $right = \Normalizer::normalize($right, \Normalizer::FORM_D); // form NFD is faster } @@ -688,7 +688,7 @@ public static function pcre(string $func, array $args) }); if (($code = preg_last_error()) // run-time error, but preg_last_error & return code are liars - && ($res === null || !in_array($func, ['preg_filter', 'preg_replace_callback', 'preg_replace'], true)) + && ($res === null || !in_array($func, ['preg_filter', 'preg_replace_callback', 'preg_replace'], strict: true)) ) { throw new RegexpException(preg_last_error_msg() . ' (pattern: ' . implode(' or ', (array) $args[0]) . ')', $code); diff --git a/src/Utils/Validators.php b/src/Utils/Validators.php index 940c3eb4..bd7834c9 100644 --- a/src/Utils/Validators.php +++ b/src/Utils/Validators.php @@ -126,10 +126,10 @@ public static function assertField( ): void { if (!array_key_exists($key, $array)) { - throw new AssertionException('Missing ' . str_replace('%', $key, $label) . '.'); + throw new AssertionException('Missing ' . str_replace('%', (string) $key, $label) . '.'); } elseif ($expected) { - static::assert($array[$key], $expected, str_replace('%', $key, $label)); + static::assert($array[$key], $expected, str_replace('%', (string) $key, $label)); } } @@ -159,7 +159,7 @@ public static function is(mixed $value, string $expected): bool if (!static::$validators[$type]($value)) { continue; } - } catch (\TypeError $e) { + } catch (\TypeError) { continue; } } elseif ($type === 'pattern') { From 558b3684d306937558625fb689adce18f52042e2 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 24 Jan 2026 15:33:45 +0100 Subject: [PATCH 2/8] improved native types --- src/Utils/Arrays.php | 6 +++--- src/Utils/Callback.php | 2 +- src/Utils/DateTime.php | 2 +- src/Utils/Html.php | 2 +- src/Utils/Image.php | 4 ++-- src/Utils/Strings.php | 2 +- src/Utils/Type.php | 2 +- src/Utils/Validators.php | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Utils/Arrays.php b/src/Utils/Arrays.php index 663a337f..9ef193f7 100644 --- a/src/Utils/Arrays.php +++ b/src/Utils/Arrays.php @@ -106,7 +106,7 @@ public static function getKeyOffset(array $array, string|int $key): ?int /** * @deprecated use getKeyOffset() */ - public static function searchKey(array $array, $key): ?int + public static function searchKey(array $array, string|int $key): ?int { return self::getKeyOffset($array, $key); } @@ -482,7 +482,7 @@ public static function mapWithKeys(array $array, callable $transformer): array * Invokes all callbacks and returns array of results. * @param callable[] $callbacks */ - public static function invoke(iterable $callbacks, ...$args): array + public static function invoke(iterable $callbacks, mixed ...$args): array { $res = []; foreach ($callbacks as $k => $cb) { @@ -497,7 +497,7 @@ public static function invoke(iterable $callbacks, ...$args): array * Invokes method on every object in an array and returns array of results. * @param object[] $objects */ - public static function invokeMethod(iterable $objects, string $method, ...$args): array + public static function invokeMethod(iterable $objects, string $method, mixed ...$args): array { $res = []; foreach ($objects as $k => $obj) { diff --git a/src/Utils/Callback.php b/src/Utils/Callback.php index 7d384f25..d91eb6f1 100644 --- a/src/Utils/Callback.php +++ b/src/Utils/Callback.php @@ -53,7 +53,7 @@ public static function invokeSafe(string $function, array $args, callable $onErr * @return callable * @throws Nette\InvalidArgumentException */ - public static function check(mixed $callable, bool $syntax = false) + public static function check(mixed $callable, bool $syntax = false): mixed { if (!is_callable($callable, $syntax)) { throw new Nette\InvalidArgumentException( diff --git a/src/Utils/DateTime.php b/src/Utils/DateTime.php index 6191223f..59573768 100644 --- a/src/Utils/DateTime.php +++ b/src/Utils/DateTime.php @@ -142,7 +142,7 @@ public static function relativeToSeconds(string $relativeTime): int } - private function apply(string $datetime, $timezone = null, bool $ctr = false): void + private function apply(string $datetime, \DateTimeZone|string|null $timezone = null, bool $ctr = false): void { $relPart = ''; $absPart = preg_replace_callback( diff --git a/src/Utils/Html.php b/src/Utils/Html.php index 55e70a51..ac6327a8 100644 --- a/src/Utils/Html.php +++ b/src/Utils/Html.php @@ -634,7 +634,7 @@ final public function offsetSet($index, $child): void * Returns child node (\ArrayAccess implementation). * @param int $index */ - final public function offsetGet($index): HtmlStringable|string + final public function offsetGet($index): self|string { return $this->children[$index]; } diff --git a/src/Utils/Image.php b/src/Utils/Image.php index 062fb168..5e1e4b99 100644 --- a/src/Utils/Image.php +++ b/src/Utils/Image.php @@ -237,7 +237,7 @@ public static function fromBlank(int $width, int $height, ImageColor|array|null * Returns the type of image from file. * @return ImageType::*|null */ - public static function detectTypeFromFile(string $file, &$width = null, &$height = null): ?int + public static function detectTypeFromFile(string $file, mixed &$width = null, mixed &$height = null): ?int { [$width, $height, $type] = Helpers::falseToNull(@getimagesize($file)); // @ - files smaller than 12 bytes causes read error return $type && isset(self::Formats[$type]) ? $type : null; @@ -248,7 +248,7 @@ public static function detectTypeFromFile(string $file, &$width = null, &$height * Returns the type of image from string. * @return ImageType::*|null */ - public static function detectTypeFromString(string $s, &$width = null, &$height = null): ?int + public static function detectTypeFromString(string $s, mixed &$width = null, mixed &$height = null): ?int { [$width, $height, $type] = Helpers::falseToNull(@getimagesizefromstring($s)); // @ - strings smaller than 12 bytes causes read error return $type && isset(self::Formats[$type]) ? $type : null; diff --git a/src/Utils/Strings.php b/src/Utils/Strings.php index 41a2e274..427cccec 100644 --- a/src/Utils/Strings.php +++ b/src/Utils/Strings.php @@ -680,7 +680,7 @@ private static function bytesToChars(string $s, array $groups): array /** @internal */ - public static function pcre(string $func, array $args) + public static function pcre(string $func, array $args): mixed { $res = Callback::invokeSafe($func, $args, function (string $message) use ($args): void { // compile-time error, not detectable by preg_last_error diff --git a/src/Utils/Type.php b/src/Utils/Type.php index 92aac80d..ef0f117f 100644 --- a/src/Utils/Type.php +++ b/src/Utils/Type.php @@ -107,7 +107,7 @@ public static function fromValue(mixed $value): self */ public static function resolve( string $type, - \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $of, + \ReflectionFunction|\ReflectionMethod|\ReflectionParameter|\ReflectionProperty $of, ): string { $lower = strtolower($type); diff --git a/src/Utils/Validators.php b/src/Utils/Validators.php index bd7834c9..2d01c2d8 100644 --- a/src/Utils/Validators.php +++ b/src/Utils/Validators.php @@ -120,7 +120,7 @@ public static function assert(mixed $value, string $expected, string $label = 'v */ public static function assertField( array $array, - $key, + int|string $key, ?string $expected = null, string $label = "item '%' in array", ): void From 70b4765746182b1f8cd81418393b31915452ec1d Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 9 Jan 2026 10:58:07 +0100 Subject: [PATCH 3/8] improved phpDoc --- src/HtmlStringable.php | 5 +- src/Iterators/CachingIterator.php | 29 +--- src/SmartObject.php | 3 + src/StaticClass.php | 2 +- src/Translator.php | 2 +- src/Utils/ArrayHash.php | 6 +- src/Utils/ArrayList.php | 9 +- src/Utils/Arrays.php | 27 +++- src/Utils/Callback.php | 9 +- src/Utils/Finder.php | 21 ++- src/Utils/Helpers.php | 4 + src/Utils/Html.php | 233 ++++++++++++++++-------------- src/Utils/Image.php | 48 ++++-- src/Utils/ImageColor.php | 4 + src/Utils/Iterables.php | 5 + src/Utils/Paginator.php | 49 +------ src/Utils/Reflection.php | 12 +- src/Utils/ReflectionMethod.php | 2 + src/Utils/Strings.php | 18 ++- src/Utils/Type.php | 9 +- src/Utils/Validators.php | 7 +- 21 files changed, 285 insertions(+), 219 deletions(-) diff --git a/src/HtmlStringable.php b/src/HtmlStringable.php index d749d4ee..cdd0bbdf 100644 --- a/src/HtmlStringable.php +++ b/src/HtmlStringable.php @@ -10,10 +10,13 @@ namespace Nette; +/** + * Represents object convertible to HTML string. + */ interface HtmlStringable { /** - * Returns string in HTML format + * Returns string in HTML format. */ function __toString(): string; } diff --git a/src/Iterators/CachingIterator.php b/src/Iterators/CachingIterator.php index bf6ef85c..1be69e65 100644 --- a/src/Iterators/CachingIterator.php +++ b/src/Iterators/CachingIterator.php @@ -13,8 +13,11 @@ /** - * Smarter caching iterator. + * Enhanced caching iterator with first/last/counter tracking. * + * @template TKey + * @template TValue + * @extends \CachingIterator> * @property-read bool $first * @property-read bool $last * @property-read bool $empty @@ -31,6 +34,9 @@ class CachingIterator extends \CachingIterator implements \Countable private int $counter = 0; + /** + * @param iterable|\stdClass $iterable + */ public function __construct(iterable|\stdClass $iterable) { $iterable = $iterable instanceof \stdClass @@ -58,45 +64,30 @@ public function isLast(?int $gridWidth = null): bool } - /** - * Is the iterator empty? - */ public function isEmpty(): bool { return $this->counter === 0; } - /** - * Is the counter odd? - */ public function isOdd(): bool { return $this->counter % 2 === 1; } - /** - * Is the counter even? - */ public function isEven(): bool { return $this->counter % 2 === 0; } - /** - * Returns the counter. - */ public function getCounter(): int { return $this->counter; } - /** - * Returns the count of elements. - */ public function count(): int { $inner = $this->getInnerIterator(); @@ -131,18 +122,12 @@ public function rewind(): void } - /** - * Returns the next key. - */ public function getNextKey(): mixed { return $this->getInnerIterator()->key(); } - /** - * Returns the next element. - */ public function getNextValue(): mixed { return $this->getInnerIterator()->current(); diff --git a/src/SmartObject.php b/src/SmartObject.php index 3b2203f1..a1029798 100644 --- a/src/SmartObject.php +++ b/src/SmartObject.php @@ -22,6 +22,7 @@ trait SmartObject { /** + * @param list $args * @return mixed * @throws MemberAccessException */ @@ -47,6 +48,8 @@ public function __call(string $name, array $args) /** + * @param list $args + * @return never * @throws MemberAccessException */ public static function __callStatic(string $name, array $args) diff --git a/src/StaticClass.php b/src/StaticClass.php index 46b27866..0364cf9a 100644 --- a/src/StaticClass.php +++ b/src/StaticClass.php @@ -11,7 +11,7 @@ /** - * Static class. + * Prevents instantiation. */ trait StaticClass { diff --git a/src/Translator.php b/src/Translator.php index f973f5f1..16afdc6d 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -11,7 +11,7 @@ /** - * Translator adapter. + * Translation provider. */ interface Translator { diff --git a/src/Utils/ArrayHash.php b/src/Utils/ArrayHash.php index c83b84f7..69473fb8 100644 --- a/src/Utils/ArrayHash.php +++ b/src/Utils/ArrayHash.php @@ -14,7 +14,7 @@ /** - * Provides objects to work as array. + * Array-like object with property access. * @template T * @implements \IteratorAggregate * @implements \ArrayAccess @@ -39,7 +39,6 @@ public static function from(array $array, bool $recursive = true): static /** - * Returns an iterator over all items. * @return \Iterator */ public function &getIterator(): \Iterator @@ -50,9 +49,6 @@ public function &getIterator(): \Iterator } - /** - * Returns items count. - */ public function count(): int { return count((array) $this); diff --git a/src/Utils/ArrayList.php b/src/Utils/ArrayList.php index 98a50821..3e5d4f35 100644 --- a/src/Utils/ArrayList.php +++ b/src/Utils/ArrayList.php @@ -14,13 +14,14 @@ /** - * Provides the base class for a generic list (items can be accessed by index). + * Generic list with integer indices. * @template T * @implements \IteratorAggregate * @implements \ArrayAccess */ class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate { + /** @var list */ private array $list = []; @@ -41,7 +42,6 @@ public static function from(array $array): static /** - * Returns an iterator over all items. * @return \Iterator */ public function &getIterator(): \Iterator @@ -52,9 +52,6 @@ public function &getIterator(): \Iterator } - /** - * Returns items count. - */ public function count(): int { return count($this->list); @@ -63,7 +60,7 @@ public function count(): int /** * Replaces or appends an item. - * @param int|null $index + * @param ?int $index * @param T $value * @throws Nette\OutOfRangeException */ diff --git a/src/Utils/Arrays.php b/src/Utils/Arrays.php index 9ef193f7..fe72796a 100644 --- a/src/Utils/Arrays.php +++ b/src/Utils/Arrays.php @@ -79,7 +79,7 @@ public static function &getRef(array &$array, string|int|array $key): mixed * @template T2 * @param array $array1 * @param array $array2 - * @return array + * @return array> */ public static function mergeTree(array $array1, array $array2): array { @@ -96,6 +96,7 @@ public static function mergeTree(array $array1, array $array2): array /** * Returns zero-indexed position of given array key. Returns null if key is not found. + * @param array $array */ public static function getKeyOffset(array $array, string|int $key): ?int { @@ -104,6 +105,7 @@ public static function getKeyOffset(array $array, string|int $key): ?int /** + * @param array $array * @deprecated use getKeyOffset() */ public static function searchKey(array $array, string|int $key): ?int @@ -114,6 +116,7 @@ public static function searchKey(array $array, string|int $key): ?int /** * Tests an array for the presence of value. + * @param array $array */ public static function contains(array $array, mixed $value): bool { @@ -127,6 +130,7 @@ public static function contains(array $array, mixed $value): bool * @template V * @param array $array * @param ?callable(V, K, array): bool $predicate + * @param ?callable(): V $else * @return ?V */ public static function first(array $array, ?callable $predicate = null, ?callable $else = null): mixed @@ -144,6 +148,7 @@ public static function first(array $array, ?callable $predicate = null, ?callabl * @template V * @param array $array * @param ?callable(V, K, array): bool $predicate + * @param ?callable(): V $else * @return ?V */ public static function last(array $array, ?callable $predicate = null, ?callable $else = null): mixed @@ -196,6 +201,8 @@ public static function lastKey(array $array, ?callable $predicate = null): int|s /** * Inserts the contents of the $inserted array into the $array immediately after the $key. * If $key is null (or does not exist), it is inserted at the beginning. + * @param array $array + * @param array $inserted */ public static function insertBefore(array &$array, string|int|null $key, array $inserted): void { @@ -209,6 +216,8 @@ public static function insertBefore(array &$array, string|int|null $key, array $ /** * Inserts the contents of the $inserted array into the $array before the $key. * If $key is null (or does not exist), it is inserted at the end. + * @param array $array + * @param array $inserted */ public static function insertAfter(array &$array, string|int|null $key, array $inserted): void { @@ -224,6 +233,7 @@ public static function insertAfter(array &$array, string|int|null $key, array $i /** * Renames key in array. + * @param array $array */ public static function renameKey(array &$array, string|int $oldKey, string|int $newKey): bool { @@ -260,6 +270,8 @@ public static function grep( /** * Transforms multidimensional array to flat array. + * @param array $array + * @return array */ public static function flatten(array $array, bool $preserveKeys = false): array { @@ -284,7 +296,9 @@ public static function isList(mixed $value): bool /** * Reformats table to associative tree. Path looks like 'field|field[]field->field=field'. - * @param string|string[] $path + * @param array $array + * @param string|list $path + * @return array|\stdClass */ public static function associate(array $array, $path): array|\stdClass { @@ -338,6 +352,8 @@ public static function associate(array $array, $path): array|\stdClass /** * Normalizes array to associative array. Replace numeric keys with their values, the new value will be $filling. + * @param array $array + * @return array */ public static function normalize(array $array, mixed $filling = null): array { @@ -480,7 +496,9 @@ public static function mapWithKeys(array $array, callable $transformer): array /** * Invokes all callbacks and returns array of results. - * @param callable[] $callbacks + * @param iterable $callbacks + * @param mixed ...$args + * @return array */ public static function invoke(iterable $callbacks, mixed ...$args): array { @@ -496,6 +514,8 @@ public static function invoke(iterable $callbacks, mixed ...$args): array /** * Invokes method on every object in an array and returns array of results. * @param object[] $objects + * @param mixed ...$args + * @return array */ public static function invokeMethod(iterable $objects, string $method, mixed ...$args): array { @@ -511,6 +531,7 @@ public static function invokeMethod(iterable $objects, string $method, mixed ... /** * Copies the elements of the $array array to the $object object and then returns it. * @template T of object + * @param iterable $array * @param T $object * @return T */ diff --git a/src/Utils/Callback.php b/src/Utils/Callback.php index d91eb6f1..fe89b1e8 100644 --- a/src/Utils/Callback.php +++ b/src/Utils/Callback.php @@ -22,6 +22,8 @@ final class Callback /** * Invokes internal PHP function with own error handler. + * @param list $args + * @param callable(string, int): ?bool $onError */ public static function invokeSafe(string $function, array $args, callable $onError): mixed { @@ -50,7 +52,7 @@ public static function invokeSafe(string $function, array $args, callable $onErr /** * Checks that $callable is valid PHP callback. Otherwise throws exception. If the $syntax is set to true, only verifies * that $callable has a valid structure to be used as a callback, but does not verify if the class or method actually exists. - * @return callable + * @return callable(): mixed * @throws Nette\InvalidArgumentException */ public static function check(mixed $callable, bool $syntax = false): mixed @@ -84,7 +86,7 @@ public static function toString(mixed $callable): string /** * Returns reflection for method or function used in PHP callback. - * @param callable $callable type check is escalated to ReflectionException + * @param callable(): mixed $callable type check is escalated to ReflectionException * @throws \ReflectionException if callback is not valid */ public static function toReflection($callable): \ReflectionMethod|\ReflectionFunction @@ -107,6 +109,7 @@ public static function toReflection($callable): \ReflectionMethod|\ReflectionFun /** * Checks whether PHP callback is function or static method. + * @param callable(): mixed $callable */ public static function isStatic(callable $callable): bool { @@ -116,6 +119,8 @@ public static function isStatic(callable $callable): bool /** * Unwraps closure created by Closure::fromCallable(). + * @param \Closure(): mixed $closure + * @return \Closure|array{object|class-string, string}|callable-string */ public static function unwrap(\Closure $closure): callable|array { diff --git a/src/Utils/Finder.php b/src/Utils/Finder.php index 2f5f7d16..fd96ff2c 100644 --- a/src/Utils/Finder.php +++ b/src/Utils/Finder.php @@ -32,10 +32,10 @@ class Finder implements \IteratorAggregate /** @var string[] */ private array $in = []; - /** @var \Closure[] */ + /** @var array<\Closure(FileInfo): bool> */ private array $filters = []; - /** @var \Closure[] */ + /** @var array<\Closure(FileInfo): bool> */ private array $descentFilters = []; /** @var array */ @@ -50,6 +50,7 @@ class Finder implements \IteratorAggregate /** * Begins search for files and directories matching mask. + * @param string|list $masks */ public static function find(string|array $masks = ['*']): static { @@ -60,6 +61,7 @@ public static function find(string|array $masks = ['*']): static /** * Begins search for files matching mask. + * @param string|list $masks */ public static function findFiles(string|array $masks = ['*']): static { @@ -70,6 +72,7 @@ public static function findFiles(string|array $masks = ['*']): static /** * Begins search for directories matching mask. + * @param string|list $masks */ public static function findDirectories(string|array $masks = ['*']): static { @@ -80,6 +83,7 @@ public static function findDirectories(string|array $masks = ['*']): static /** * Finds files matching the specified masks. + * @param string|list $masks */ public function files(string|array $masks = ['*']): static { @@ -89,6 +93,7 @@ public function files(string|array $masks = ['*']): static /** * Finds directories matching the specified masks. + * @param string|list $masks */ public function directories(string|array $masks = ['*']): static { @@ -96,6 +101,7 @@ public function directories(string|array $masks = ['*']): static } + /** @param list $masks */ private function addMask(array $masks, string $mode): static { foreach ($masks as $mask) { @@ -117,6 +123,7 @@ private function addMask(array $masks, string $mode): static /** * Searches in the given directories. Wildcards are allowed. + * @param string|list $paths */ public function in(string|array $paths): static { @@ -128,6 +135,7 @@ public function in(string|array $paths): static /** * Searches recursively from the given directories. Wildcards are allowed. + * @param string|list $paths */ public function from(string|array $paths): static { @@ -137,6 +145,7 @@ public function from(string|array $paths): static } + /** @param list $paths */ private function addLocation(array $paths, string $ext): void { foreach ($paths as $path) { @@ -192,6 +201,7 @@ public function sortByName(): static /** * Adds the specified paths or appends a new finder that returns. + * @param string|list|null $paths */ public function append(string|array|null $paths = null): static { @@ -209,6 +219,7 @@ public function append(string|array|null $paths = null): static /** * Skips entries that matches the given masks relative to the ones defined with the in() or from() methods. + * @param string|list $masks */ public function exclude(string|array $masks): static { @@ -401,6 +412,7 @@ private function traverseDir(string $dir, array $searches, array $subdirs = []): } + /** @param iterable $pathNames */ private function convertToFiles(iterable $pathNames, string $relativePath, bool $absolute): \Generator { foreach ($pathNames as $pathName) { @@ -413,6 +425,10 @@ private function convertToFiles(iterable $pathNames, string $relativePath, bool } + /** + * @param (\Closure(FileInfo): bool)[] $filters + * @param array $cache + */ private function proveFilters(array $filters, FileInfo $file, array &$cache): bool { foreach ($filters as $filter) { @@ -468,6 +484,7 @@ private function buildPlan(): array /** * Since glob() does not know ** wildcard, we divide the path into a part for glob and a part for manual traversal. + * @return array{string, string, bool} */ private static function splitRecursivePart(string $path): array { diff --git a/src/Utils/Helpers.php b/src/Utils/Helpers.php index c7d78943..afdbad68 100644 --- a/src/Utils/Helpers.php +++ b/src/Utils/Helpers.php @@ -14,6 +14,9 @@ use const PHP_OS_FAMILY; +/** + * Miscellaneous utilities. + */ class Helpers { public const IsWindows = PHP_OS_FAMILY === 'Windows'; @@ -21,6 +24,7 @@ class Helpers /** * Executes a callback and returns the captured output as a string. + * @param callable(): void $func */ public static function capture(callable $func): string { diff --git a/src/Utils/Html.php b/src/Utils/Html.php index ac6327a8..87180a1e 100644 --- a/src/Utils/Html.php +++ b/src/Utils/Html.php @@ -17,113 +17,113 @@ /** * HTML helper. * - * @property string|null $accept - * @property string|null $accesskey - * @property string|null $action - * @property string|null $align - * @property string|null $allow - * @property string|null $alt - * @property bool|null $async - * @property string|null $autocapitalize - * @property string|null $autocomplete - * @property bool|null $autofocus - * @property bool|null $autoplay - * @property string|null $charset - * @property bool|null $checked - * @property string|null $cite - * @property string|null $class - * @property int|null $cols - * @property int|null $colspan - * @property string|null $content - * @property bool|null $contenteditable - * @property bool|null $controls - * @property string|null $coords - * @property string|null $crossorigin - * @property string|null $data - * @property string|null $datetime - * @property string|null $decoding - * @property bool|null $default - * @property bool|null $defer - * @property string|null $dir - * @property string|null $dirname - * @property bool|null $disabled - * @property bool|null $download - * @property string|null $draggable - * @property string|null $dropzone - * @property string|null $enctype - * @property string|null $for - * @property string|null $form - * @property string|null $formaction - * @property string|null $formenctype - * @property string|null $formmethod - * @property bool|null $formnovalidate - * @property string|null $formtarget - * @property string|null $headers - * @property int|null $height - * @property bool|null $hidden - * @property float|null $high - * @property string|null $href - * @property string|null $hreflang - * @property string|null $id - * @property string|null $integrity - * @property string|null $inputmode - * @property bool|null $ismap - * @property string|null $itemprop - * @property string|null $kind - * @property string|null $label - * @property string|null $lang - * @property string|null $list - * @property bool|null $loop - * @property float|null $low - * @property float|null $max - * @property int|null $maxlength - * @property int|null $minlength - * @property string|null $media - * @property string|null $method - * @property float|null $min - * @property bool|null $multiple - * @property bool|null $muted - * @property string|null $name - * @property bool|null $novalidate - * @property bool|null $open - * @property float|null $optimum - * @property string|null $pattern - * @property string|null $ping - * @property string|null $placeholder - * @property string|null $poster - * @property string|null $preload - * @property string|null $radiogroup - * @property bool|null $readonly - * @property string|null $rel - * @property bool|null $required - * @property bool|null $reversed - * @property int|null $rows - * @property int|null $rowspan - * @property string|null $sandbox - * @property string|null $scope - * @property bool|null $selected - * @property string|null $shape - * @property int|null $size - * @property string|null $sizes - * @property string|null $slot - * @property int|null $span - * @property string|null $spellcheck - * @property string|null $src - * @property string|null $srcdoc - * @property string|null $srclang - * @property string|null $srcset - * @property int|null $start - * @property float|null $step - * @property string|null $style - * @property int|null $tabindex - * @property string|null $target - * @property string|null $title - * @property string|null $translate - * @property string|null $type - * @property string|null $usemap - * @property string|null $value - * @property int|null $width - * @property string|null $wrap + * @property ?string $accept + * @property ?string $accesskey + * @property ?string $action + * @property ?string $align + * @property ?string $allow + * @property ?string $alt + * @property ?bool $async + * @property ?string $autocapitalize + * @property ?string $autocomplete + * @property ?bool $autofocus + * @property ?bool $autoplay + * @property ?string $charset + * @property ?bool $checked + * @property ?string $cite + * @property ?string $class + * @property ?int $cols + * @property ?int $colspan + * @property ?string $content + * @property ?bool $contenteditable + * @property ?bool $controls + * @property ?string $coords + * @property ?string $crossorigin + * @property ?string $data + * @property ?string $datetime + * @property ?string $decoding + * @property ?bool $default + * @property ?bool $defer + * @property ?string $dir + * @property ?string $dirname + * @property ?bool $disabled + * @property ?bool $download + * @property ?string $draggable + * @property ?string $dropzone + * @property ?string $enctype + * @property ?string $for + * @property ?string $form + * @property ?string $formaction + * @property ?string $formenctype + * @property ?string $formmethod + * @property ?bool $formnovalidate + * @property ?string $formtarget + * @property ?string $headers + * @property ?int $height + * @property ?bool $hidden + * @property ?float $high + * @property ?string $href + * @property ?string $hreflang + * @property ?string $id + * @property ?string $integrity + * @property ?string $inputmode + * @property ?bool $ismap + * @property ?string $itemprop + * @property ?string $kind + * @property ?string $label + * @property ?string $lang + * @property ?string $list + * @property ?bool $loop + * @property ?float $low + * @property ?float $max + * @property ?int $maxlength + * @property ?int $minlength + * @property ?string $media + * @property ?string $method + * @property ?float $min + * @property ?bool $multiple + * @property ?bool $muted + * @property ?string $name + * @property ?bool $novalidate + * @property ?bool $open + * @property ?float $optimum + * @property ?string $pattern + * @property ?string $ping + * @property ?string $placeholder + * @property ?string $poster + * @property ?string $preload + * @property ?string $radiogroup + * @property ?bool $readonly + * @property ?string $rel + * @property ?bool $required + * @property ?bool $reversed + * @property ?int $rows + * @property ?int $rowspan + * @property ?string $sandbox + * @property ?string $scope + * @property ?bool $selected + * @property ?string $shape + * @property ?int $size + * @property ?string $sizes + * @property ?string $slot + * @property ?int $span + * @property ?string $spellcheck + * @property ?string $src + * @property ?string $srcdoc + * @property ?string $srclang + * @property ?string $srcset + * @property ?int $start + * @property ?float $step + * @property ?string $style + * @property ?int $tabindex + * @property ?string $target + * @property ?string $title + * @property ?string $translate + * @property ?string $type + * @property ?string $usemap + * @property ?string $value + * @property ?int $width + * @property ?string $wrap * * @method self accept(?string $val) * @method self accesskey(?string $val, bool $state = null) @@ -230,20 +230,23 @@ * @method self value(?string $val) * @method self width(?int $val) * @method self wrap(?string $val) + * + * @implements \IteratorAggregate + * @implements \ArrayAccess */ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringable { /** @var array element's attributes */ public array $attrs = []; - /** void elements */ + /** @var array void elements */ public static array $emptyElements = [ 'img' => 1, 'hr' => 1, 'br' => 1, 'input' => 1, 'meta' => 1, 'area' => 1, 'embed' => 1, 'keygen' => 1, 'source' => 1, 'base' => 1, 'col' => 1, 'link' => 1, 'param' => 1, 'basefont' => 1, 'frame' => 1, 'isindex' => 1, 'wbr' => 1, 'command' => 1, 'track' => 1, ]; - /** @var array nodes */ + /** @var array nodes */ protected array $children = []; /** element's name */ @@ -254,7 +257,7 @@ class Html implements \ArrayAccess, \Countable, \IteratorAggregate, HtmlStringab /** * Constructs new HTML element. - * @param array|string $attrs element's attributes or plain text content + * @param array|string|null $attrs element's attributes or plain text content */ public static function el(?string $name = null, array|string|null $attrs = null): static { @@ -355,6 +358,7 @@ final public function isEmpty(): bool /** * Sets multiple attributes. + * @param array $attrs */ public function addAttributes(array $attrs): static { @@ -417,6 +421,7 @@ public function removeAttribute(string $name): static /** * Unsets element's attributes. + * @param list $attributes */ public function removeAttributes(array $attributes): static { @@ -466,6 +471,7 @@ final public function __unset(string $name): void /** * Overloaded setter for element's attribute. + * @param array $args */ final public function __call(string $m, array $args): mixed { @@ -496,6 +502,7 @@ final public function __call(string $m, array $args): mixed /** * Special setter for element's attribute. + * @param array $query */ final public function href(string $path, array $query = []): static { @@ -594,6 +601,7 @@ public function addText(\Stringable|string|int|null $text): static /** * Creates and adds a new Html child. + * @param array|string|null $attrs */ final public function create(string $name, array|string|null $attrs = null): static { @@ -621,7 +629,7 @@ public function insert(?int $index, HtmlStringable|string $child, bool $replace /** * Inserts (replaces) child node (\ArrayAccess implementation). - * @param int|null $index position or null for appending + * @param ?int $index position or null for appending * @param Html|string $child Html node or raw HTML string */ final public function offsetSet($index, $child): void @@ -682,7 +690,7 @@ public function removeChildren(): void /** * Iterates over elements. - * @return \ArrayIterator + * @return \ArrayIterator */ final public function getIterator(): \ArrayIterator { @@ -692,6 +700,7 @@ final public function getIterator(): \ArrayIterator /** * Returns all children. + * @return array */ final public function getChildren(): array { diff --git a/src/Utils/Image.php b/src/Utils/Image.php index 5e1e4b99..cb82df40 100644 --- a/src/Utils/Image.php +++ b/src/Utils/Image.php @@ -24,7 +24,7 @@ * $image->send(); * * - * @method Image affine(array $affine, ?array $clip = null) + * @method Image affine(array $affine, ?array{x: int, y: int, width: int, height: int} $clip = null) * @method void alphaBlending(bool $enable) * @method void antialias(bool $enable) * @method void arc(int $centerX, int $centerY, int $width, int $height, int $startAngle, int $endAngle, ImageColor $color) @@ -41,51 +41,51 @@ * @method int colorResolve(int $red, int $green, int $blue) * @method int colorResolveAlpha(int $red, int $green, int $blue, int $alpha) * @method void colorSet(int $index, int $red, int $green, int $blue, int $alpha = 0) - * @method array colorsForIndex(int $color) + * @method array{red: int, green: int, blue: int, alpha: int} colorsForIndex(int $color) * @method int colorsTotal() * @method int colorTransparent(?int $color = null) - * @method void convolution(array $matrix, float $div, float $offset) + * @method void convolution(array> $matrix, float $div, float $offset) * @method void copy(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH) * @method void copyMerge(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH, int $pct) * @method void copyMergeGray(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $srcW, int $srcH, int $pct) * @method void copyResampled(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $dstW, int $dstH, int $srcW, int $srcH) * @method void copyResized(Image $src, int $dstX, int $dstY, int $srcX, int $srcY, int $dstW, int $dstH, int $srcW, int $srcH) - * @method Image cropAuto(int $mode = IMG_CROP_DEFAULT, float $threshold = .5, ?ImageColor $color = null) + * @method Image cropAuto(int $mode = 0, float $threshold = .5, ?ImageColor $color = null) * @method void ellipse(int $centerX, int $centerY, int $width, int $height, ImageColor $color) * @method void fill(int $x, int $y, ImageColor $color) * @method void filledArc(int $centerX, int $centerY, int $width, int $height, int $startAngle, int $endAngle, ImageColor $color, int $style) * @method void filledEllipse(int $centerX, int $centerY, int $width, int $height, ImageColor $color) - * @method void filledPolygon(array $points, ImageColor $color) + * @method void filledPolygon(array $points, ImageColor $color) * @method void filledRectangle(int $x1, int $y1, int $x2, int $y2, ImageColor $color) * @method void fillToBorder(int $x, int $y, ImageColor $borderColor, ImageColor $color) * @method void filter(int $filter, ...$args) * @method void flip(int $mode) - * @method array ftText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontFile, string $text, array $options = []) + * @method array ftText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontFile, string $text, array $options = []) * @method void gammaCorrect(float $inputgamma, float $outputgamma) - * @method array getClip() + * @method array{int, int, int, int} getClip() * @method int getInterpolation() * @method int interlace(?bool $enable = null) * @method bool isTrueColor() * @method void layerEffect(int $effect) * @method void line(int $x1, int $y1, int $x2, int $y2, ImageColor $color) - * @method void openPolygon(array $points, ImageColor $color) + * @method void openPolygon(array $points, ImageColor $color) * @method void paletteCopy(Image $source) * @method void paletteToTrueColor() - * @method void polygon(array $points, ImageColor $color) + * @method void polygon(array $points, ImageColor $color) * @method void rectangle(int $x1, int $y1, int $x2, int $y2, ImageColor $color) * @method mixed resolution(?int $resolutionX = null, ?int $resolutionY = null) * @method Image rotate(float $angle, ImageColor $backgroundColor) * @method void saveAlpha(bool $enable) - * @method Image scale(int $newWidth, int $newHeight = -1, int $mode = IMG_BILINEAR_FIXED) + * @method Image scale(int $newWidth, int $newHeight = -1, int $mode = 3) * @method void setBrush(Image $brush) * @method void setClip(int $x1, int $y1, int $x2, int $y2) - * @method void setInterpolation(int $method = IMG_BILINEAR_FIXED) + * @method void setInterpolation(int $method = 3) * @method void setPixel(int $x, int $y, ImageColor $color) - * @method void setStyle(array $style) + * @method void setStyle(array $style) * @method void setThickness(int $thickness) * @method void setTile(Image $tile) * @method void trueColorToPalette(bool $dither, int $ncolors) - * @method array ttfText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontfile, string $text, array $options = []) + * @method array ttfText(float $size, float $angle, int $x, int $y, ImageColor $color, string $fontfile, string $text, array $options = []) * @property-read positive-int $width * @property-read positive-int $height * @property-read \GdImage $imageResource @@ -146,6 +146,7 @@ class Image /** * Returns RGB color (0..255) and transparency (0..127). * @deprecated use ImageColor::rgb() + * @return array{red: int, green: int, blue: int, alpha: int} */ public static function rgb(int $red, int $green, int $blue, int $transparency = 0): array { @@ -213,6 +214,7 @@ private static function invokeSafe(string $func, string $arg, string $message, s * Creates a new true color image of the given dimensions. The default color is black. * @param positive-int $width * @param positive-int $height + * @param ImageColor|array{red: int, green: int, blue: int, alpha?: int}|null $color * @throws Nette\NotSupportedException if gd extension is not loaded */ public static function fromBlank(int $width, int $height, ImageColor|array|null $color = null): static @@ -235,7 +237,9 @@ public static function fromBlank(int $width, int $height, ImageColor|array|null /** * Returns the type of image from file. - * @return ImageType::*|null + * @param-out ?int $width + * @param-out ?int $height + * @return ?ImageType::* */ public static function detectTypeFromFile(string $file, mixed &$width = null, mixed &$height = null): ?int { @@ -246,7 +250,9 @@ public static function detectTypeFromFile(string $file, mixed &$width = null, mi /** * Returns the type of image from string. - * @return ImageType::*|null + * @param-out ?int $width + * @param-out ?int $height + * @return ?ImageType::* */ public static function detectTypeFromString(string $s, mixed &$width = null, mixed &$height = null): ?int { @@ -418,7 +424,10 @@ public function resize(int|string|null $width, int|string|null $height, int $mod /** * Calculates dimensions of resized image. Width and height accept pixels or percent. + * @param int|string|null $newWidth + * @param int|string|null $newHeight * @param int-mask-of $mode + * @return array{int, int} */ public static function calculateSize( int $srcWidth, @@ -506,6 +515,7 @@ public function crop(int|string $left, int|string $top, int|string $width, int|s /** * Calculates dimensions of cutout in image. Arguments accepts pixels or percent. + * @return array{int, int, int, int} */ public static function calculateCutout( int $srcWidth, @@ -626,6 +636,8 @@ public function place(self $image, int|string $left = 0, int|string $top = 0, in /** * Calculates the bounding box for a TrueType text. Returns keys left, top, width and height. + * @param array $options + * @return array{left: int, top: int, width: int, height: int} */ public static function calculateTextBox( string $text, @@ -670,7 +682,7 @@ public function filledRectangleWH(int $x, int $y, int $width, int $height, Image /** * Saves image to the file. Quality is in the range 0..100 for JPEG (default 85), WEBP (default 80) and AVIF (default 30) and 0..9 for PNG (default 9). - * @param ImageType::*|null $type + * @param ?ImageType::* $type * @throws ImageException */ public function save(string $file, ?int $quality = null, ?int $type = null): void @@ -746,6 +758,7 @@ private function output(int $type, ?int $quality, ?string $file = null): void /** * Call to undefined method. + * @param array $args * @throws Nette\MemberAccessException */ public function __call(string $name, array $args): mixed @@ -802,6 +815,9 @@ public function __serialize(): array } + /** + * @param ImageColor|array{red: int, green: int, blue: int, alpha?: int} $color + */ public function resolveColor(ImageColor|array $color): int { $color = $color instanceof ImageColor ? $color->toRGBA() : array_values($color + ['alpha' => 0]); diff --git a/src/Utils/ImageColor.php b/src/Utils/ImageColor.php index 66966620..aae5f051 100644 --- a/src/Utils/ImageColor.php +++ b/src/Utils/ImageColor.php @@ -64,6 +64,10 @@ private function __construct( } + /** + * Returns GD-compatible color array [R, G, B, alpha]. + * @return array{int, int, int, int} + */ public function toRGBA(): array { return [ diff --git a/src/Utils/Iterables.php b/src/Utils/Iterables.php index 93a5a5dd..28191eb7 100644 --- a/src/Utils/Iterables.php +++ b/src/Utils/Iterables.php @@ -22,6 +22,7 @@ final class Iterables /** * Tests for the presence of value. + * @param iterable $iterable */ public static function contains(iterable $iterable, mixed $value): bool { @@ -36,6 +37,7 @@ public static function contains(iterable $iterable, mixed $value): bool /** * Tests for the presence of key. + * @param iterable $iterable */ public static function containsKey(iterable $iterable, mixed $key): bool { @@ -54,6 +56,7 @@ public static function containsKey(iterable $iterable, mixed $key): bool * @template V * @param iterable $iterable * @param ?callable(V, K, iterable): bool $predicate + * @param ?callable(): V $else * @return ?V */ public static function first(iterable $iterable, ?callable $predicate = null, ?callable $else = null): mixed @@ -73,6 +76,7 @@ public static function first(iterable $iterable, ?callable $predicate = null, ?c * @template V * @param iterable $iterable * @param ?callable(V, K, iterable): bool $predicate + * @param ?callable(): K $else * @return ?K */ public static function firstKey(iterable $iterable, ?callable $predicate = null, ?callable $else = null): mixed @@ -216,6 +220,7 @@ public static function memoize(iterable $iterable): \IteratorAggregate return new class (self::toIterator($iterable)) implements \IteratorAggregate { public function __construct( private readonly \Iterator $iterator, + /** @var array */ private array $cache = [], ) { } diff --git a/src/Utils/Paginator.php b/src/Utils/Paginator.php index aa4812c0..77e68867 100644 --- a/src/Utils/Paginator.php +++ b/src/Utils/Paginator.php @@ -17,17 +17,17 @@ * * @property int $page * @property-read int $firstPage - * @property-read int|null $lastPage + * @property-read ?int $lastPage * @property-read int<0,max> $firstItemOnPage * @property-read int<0,max> $lastItemOnPage * @property int $base * @property-read bool $first * @property-read bool $last - * @property-read int<0,max>|null $pageCount + * @property-read ?int<0,max> $pageCount * @property positive-int $itemsPerPage - * @property int<0,max>|null $itemCount + * @property ?int<0,max> $itemCount * @property-read int<0,max> $offset - * @property-read int<0,max>|null $countdownOffset + * @property-read ?int<0,max> $countdownOffset * @property-read int<0,max> $length */ class Paginator @@ -41,13 +41,10 @@ class Paginator private int $page = 1; - /** @var int<0, max>|null */ + /** @var ?int<0, max> */ private ?int $itemCount = null; - /** - * Sets current page number. - */ public function setPage(int $page): static { $this->page = $page; @@ -55,27 +52,18 @@ public function setPage(int $page): static } - /** - * Returns current page number. - */ public function getPage(): int { return $this->base + $this->getPageIndex(); } - /** - * Returns first page number. - */ public function getFirstPage(): int { return $this->base; } - /** - * Returns last page number. - */ public function getLastPage(): ?int { return $this->itemCount === null @@ -106,9 +94,6 @@ public function getLastItemOnPage(): int } - /** - * Sets first page (base) number. - */ public function setBase(int $base): static { $this->base = $base; @@ -116,9 +101,6 @@ public function setBase(int $base): static } - /** - * Returns first page (base) number. - */ public function getBase(): int { return $this->base; @@ -138,18 +120,12 @@ protected function getPageIndex(): int } - /** - * Is the current page the first one? - */ public function isFirst(): bool { return $this->getPageIndex() === 0; } - /** - * Is the current page the last one? - */ public function isLast(): bool { return $this->itemCount === null @@ -159,8 +135,7 @@ public function isLast(): bool /** - * Returns the total number of pages. - * @return int<0, max>|null + * @return ?int<0, max> */ public function getPageCount(): ?int { @@ -170,9 +145,6 @@ public function getPageCount(): ?int } - /** - * Sets the number of items to display on a single page. - */ public function setItemsPerPage(int $itemsPerPage): static { $this->itemsPerPage = max(1, $itemsPerPage); @@ -181,7 +153,6 @@ public function setItemsPerPage(int $itemsPerPage): static /** - * Returns the number of items to display on a single page. * @return positive-int */ public function getItemsPerPage(): int @@ -190,9 +161,6 @@ public function getItemsPerPage(): int } - /** - * Sets the total number of items. - */ public function setItemCount(?int $itemCount = null): static { $this->itemCount = $itemCount === null ? null : max(0, $itemCount); @@ -201,8 +169,7 @@ public function setItemCount(?int $itemCount = null): static /** - * Returns the total number of items. - * @return int<0, max>|null + * @return ?int<0, max> */ public function getItemCount(): ?int { @@ -222,7 +189,7 @@ public function getOffset(): int /** * Returns the absolute index of the first item on current page in countdown paging. - * @return int<0, max>|null + * @return ?int<0, max> */ public function getCountdownOffset(): ?int { diff --git a/src/Utils/Reflection.php b/src/Utils/Reflection.php index 02209842..f4bb54a6 100644 --- a/src/Utils/Reflection.php +++ b/src/Utils/Reflection.php @@ -68,6 +68,7 @@ public static function getParameterDefaultValue(\ReflectionParameter $param): mi /** * Returns a reflection of a class or trait that contains a declaration of given property. Property can also be declared in the trait. + * @return \ReflectionClass */ public static function getPropertyDeclaringClass(\ReflectionProperty $prop): \ReflectionClass { @@ -151,6 +152,7 @@ public static function toString(\Reflector $ref): string /** * Expands the name of the class to full name in the given context of given class. * Thus, it returns how the PHP parser would understand $name if it were written in the body of the class $context. + * @param \ReflectionClass $context * @throws Nette\InvalidArgumentException */ public static function expandClassName(string $name, \ReflectionClass $context): string @@ -189,7 +191,10 @@ public static function expandClassName(string $name, \ReflectionClass $context): } - /** @return array of [alias => class] */ + /** + * @param \ReflectionClass $class + * @return array of [alias => class] + */ public static function getUseStatements(\ReflectionClass $class): array { if ($class->isAnonymous()) { @@ -212,6 +217,7 @@ public static function getUseStatements(\ReflectionClass $class): array /** * Parses PHP code to [class => [alias => class, ...]] + * @return array> */ private static function parseUseStatements(string $code, ?string $forClass = null): array { @@ -301,6 +307,10 @@ private static function parseUseStatements(string $code, ?string $forClass = nul } + /** + * @param \PhpToken[] $tokens + * @param string|int|int[] $take + */ private static function fetch(array &$tokens, string|int|array $take): ?string { $res = null; diff --git a/src/Utils/ReflectionMethod.php b/src/Utils/ReflectionMethod.php index 26a79278..f2abdcfd 100644 --- a/src/Utils/ReflectionMethod.php +++ b/src/Utils/ReflectionMethod.php @@ -18,6 +18,7 @@ */ final class ReflectionMethod extends \ReflectionMethod { + /** @var \ReflectionClass */ private readonly \ReflectionClass $originalClass; @@ -31,6 +32,7 @@ public function __construct(object|string $objectOrMethod, ?string $method = nul } + /** @return \ReflectionClass */ public function getOriginalClass(): \ReflectionClass { return $this->originalClass; diff --git a/src/Utils/Strings.php b/src/Utils/Strings.php index 427cccec..2120e4c5 100644 --- a/src/Utils/Strings.php +++ b/src/Utils/Strings.php @@ -499,6 +499,7 @@ private static function pos(string $haystack, string $needle, int $nth = 1): ?in /** * Divides the string into arrays according to the regular expression. Expressions in parentheses will be captured and returned as well. + * @return ($captureOffset is true ? list : list) */ public static function split( string $subject, @@ -525,6 +526,7 @@ public static function split( /** * Searches the string for the part matching the regular expression and returns * an array with the found expression and individual subexpressions, or `null`. + * @return ($captureOffset is true ? ?array : ?array) */ public static function match( string $subject, @@ -560,7 +562,10 @@ public static function match( /** * Searches the string for all occurrences matching the regular expression and * returns an array of arrays containing the found expression and each subexpression. - * @return ($lazy is true ? \Generator : array[]) + * @return ($lazy is true + * ? \Generator : array)> + * : ($captureOffset is true ? list> : list>) + * ) */ public static function matchAll( string $subject, @@ -619,6 +624,8 @@ public static function matchAll( /** * Replaces all occurrences matching regular expression $pattern which can be string or array in the form `pattern => replacement`. + * @param string|array $pattern + * @param string|(callable(array): string) $replacement */ public static function replace( string $subject, @@ -659,6 +666,10 @@ public static function replace( } + /** + * @param list> $groups + * @return list> + */ private static function bytesToChars(string $s, array $groups): array { $lastBytes = $lastChars = 0; @@ -679,7 +690,10 @@ private static function bytesToChars(string $s, array $groups): array } - /** @internal */ + /** + * @param list $args + * @internal + */ public static function pcre(string $func, array $args): mixed { $res = Callback::invokeSafe($func, $args, function (string $message) use ($args): void { diff --git a/src/Utils/Type.php b/src/Utils/Type.php index ef0f117f..92149b59 100644 --- a/src/Utils/Type.php +++ b/src/Utils/Type.php @@ -40,6 +40,7 @@ public static function fromReflection( } + /** @param \ReflectionFunctionAbstract|\ReflectionParameter|\ReflectionProperty $of */ private static function fromReflectionType(\ReflectionType $type, $of, bool $asObject): self|string { if ($type instanceof \ReflectionNamedType) { @@ -125,6 +126,7 @@ public static function resolve( } + /** @param array $types */ private function __construct(array $types, string $kind = '|') { $o = array_search('null', $types, strict: true); @@ -173,7 +175,7 @@ public function with(string|self $type): self /** * Returns the array of subtypes that make up the compound type as strings. - * @return array + * @return list>> */ public function getNames(): array { @@ -279,6 +281,7 @@ public function allows(string|self $type): bool } + /** @param (string|self)[] $givenTypes */ private function allowsAny(array $givenTypes): bool { return $this->isUnion() @@ -287,6 +290,10 @@ private function allowsAny(array $givenTypes): bool } + /** + * @param (string|self)[] $ourTypes + * @param (string|self)[] $givenTypes + */ private function allowsAll(array $ourTypes, array $givenTypes): bool { return Arrays::every( diff --git a/src/Utils/Validators.php b/src/Utils/Validators.php index 2d01c2d8..30454761 100644 --- a/src/Utils/Validators.php +++ b/src/Utils/Validators.php @@ -26,7 +26,7 @@ class Validators 'never' => 1, 'true' => 1, ]; - /** @var array */ + /** @var array */ protected static $validators = [ // PHP types 'array' => 'is_array', @@ -76,7 +76,7 @@ class Validators 'type' => [self::class, 'isType'], ]; - /** @var array */ + /** @var array */ protected static $counters = [ 'string' => 'strlen', 'unicode' => [Strings::class, 'length'], @@ -261,7 +261,7 @@ public static function isUnicode(mixed $value): bool /** * Checks if the value is 0, '', false or null. - * @return ($value is 0|''|false|null ? true : false) + * @return ($value is 0|0.0|''|false|null ? true : false) */ public static function isNone(mixed $value): bool { @@ -290,6 +290,7 @@ public static function isList(mixed $value): bool /** * Checks if the value is in the given range [min, max], where the upper or lower limit can be omitted (null). * Numbers, strings and DateTime objects can be compared. + * @param array{int|float|string|\DateTimeInterface|null, int|float|string|\DateTimeInterface|null} $range */ public static function isInRange(mixed $value, array $range): bool { From 033549edfbd95ba77b4b4b2d686e1cc8da37c42c Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 24 Jan 2026 16:25:23 +0100 Subject: [PATCH 4/8] normalized callable to Closure --- src/Iterators/Mapper.php | 5 ++--- src/Utils/Finder.php | 10 +++++----- src/Utils/Iterables.php | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Iterators/Mapper.php b/src/Iterators/Mapper.php index 284da29d..94a4722b 100644 --- a/src/Iterators/Mapper.php +++ b/src/Iterators/Mapper.php @@ -15,14 +15,13 @@ */ class Mapper extends \IteratorIterator { - /** @var callable */ - private $callback; + private \Closure $callback; public function __construct(\Traversable $iterator, callable $callback) { parent::__construct($iterator); - $this->callback = $callback; + $this->callback = $callback(...); } diff --git a/src/Utils/Finder.php b/src/Utils/Finder.php index fd96ff2c..365d95c0 100644 --- a/src/Utils/Finder.php +++ b/src/Utils/Finder.php @@ -42,8 +42,8 @@ class Finder implements \IteratorAggregate private array $appends = []; private bool $childFirst = false; - /** @var ?callable */ - private $sort; + /** @var ?(\Closure(FileInfo, FileInfo): int) */ + private ?\Closure $sort = null; private int $maxDepth = -1; private bool $ignoreUnreadableDirs = true; @@ -184,7 +184,7 @@ public function ignoreUnreadableDirs(bool $state = true): static */ public function sortBy(callable $callback): static { - $this->sort = $callback; + $this->sort = $callback(...); return $this; } @@ -250,7 +250,7 @@ public function exclude(string|array $masks): static */ public function filter(callable $callback): static { - $this->filters[] = \Closure::fromCallable($callback); + $this->filters[] = $callback(...); return $this; } @@ -261,7 +261,7 @@ public function filter(callable $callback): static */ public function descentFilter(callable $callback): static { - $this->descentFilters[] = \Closure::fromCallable($callback); + $this->descentFilters[] = $callback(...); return $this; } diff --git a/src/Utils/Iterables.php b/src/Utils/Iterables.php index 28191eb7..815a5039 100644 --- a/src/Utils/Iterables.php +++ b/src/Utils/Iterables.php @@ -192,9 +192,9 @@ public static function mapWithKeys(iterable $iterable, callable $transformer): \ */ public static function repeatable(callable $factory): \IteratorAggregate { - return new class ($factory) implements \IteratorAggregate { + return new class ($factory(...)) implements \IteratorAggregate { public function __construct( - private $factory, + private \Closure $factory, ) { } From 4c9d11ec42b302f372af2b0c708f12e1c5d3670e Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 29 Dec 2025 01:00:47 +0100 Subject: [PATCH 5/8] improved tests --- .github/workflows/tests.yml | 4 +- phpstan.neon | 1 + tests/Utils/Arrays.associate().phpt | 371 +++++++++++++----------- tests/Utils/Arrays.insertBefore().phpt | 155 +++++++++- tests/Utils/Arrays.mergeTree().phpt | 108 +++++++ tests/Utils/Arrays.normalize.phpt | 79 +++-- tests/Utils/Arrays.renameKey().phpt | 256 +++++++++++----- tests/Utils/Arrays.renameKey().ref.phpt | 99 +++++-- tests/Utils/Strings.compare().phpt | 163 ++++++++++- tests/Utils/Strings.length().phpt | 46 ++- tests/Utils/Strings.pad.phpt | 144 +++++++-- tests/Utils/Strings.truncate().phpt | 149 +++++++--- tests/types/utils-types.php | 105 +++++++ 13 files changed, 1317 insertions(+), 363 deletions(-) create mode 100644 tests/Utils/Arrays.mergeTree().phpt create mode 100644 tests/types/utils-types.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8deadba7..4ad6fa8a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: coverage: none - run: composer install --no-progress --prefer-dist - - run: vendor/bin/tester tests -s -C + - run: composer tester - if: failure() uses: actions/upload-artifact@v4 with: @@ -42,7 +42,7 @@ jobs: coverage: none - run: composer install --no-progress --prefer-dist - - run: vendor/bin/tester -p phpdbg tests -s -C --coverage ./coverage.xml --coverage-src ./src + - run: composer tester -- -p phpdbg --coverage ./coverage.xml --coverage-src ./src - run: wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.3/php-coveralls.phar - env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/phpstan.neon b/phpstan.neon index 5da06271..f0d4d641 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,6 +3,7 @@ parameters: paths: - src + - tests/types bootstrapFiles: - tests/phpstan-bootstrap.php diff --git a/tests/Utils/Arrays.associate().phpt b/tests/Utils/Arrays.associate().phpt index b4d18e90..a38a1cc1 100644 --- a/tests/Utils/Arrays.associate().phpt +++ b/tests/Utils/Arrays.associate().phpt @@ -22,187 +22,218 @@ $arr = [ ]; -Assert::same( - [ - 'John' => ['name' => 'John', 'age' => 11], - 'Mary' => ['name' => 'Mary', 'age' => null], - 'Paul' => ['name' => 'Paul', 'age' => 44], - ], - Arrays::associate($arr, 'name'), -); - -Assert::same( - [], - Arrays::associate([], 'name'), -); - -Assert::same( - [ - 'John' => ['name' => 'John', 'age' => 11], - 'Mary' => ['name' => 'Mary', 'age' => null], - 'Paul' => ['name' => 'Paul', 'age' => 44], - ], - Arrays::associate($arr, 'name='), -); - -Assert::same( - ['John' => 22, 'Mary' => null, 'Paul' => 44], - Arrays::associate($arr, 'name=age'), -); - -Assert::same(// path as array - ['John' => 22, 'Mary' => null, 'Paul' => 44], - Arrays::associate($arr, ['name', '=', 'age']), -); - -Assert::equal( - [ - 'John' => (object) [ - 'name' => 'John', - 'age' => 11, - ], - 'Mary' => (object) [ - 'name' => 'Mary', - 'age' => null, - ], - 'Paul' => (object) [ - 'name' => 'Paul', - 'age' => 44, +test('basic key association', function () use ($arr) { + Assert::same( + [ + 'John' => ['name' => 'John', 'age' => 11], + 'Mary' => ['name' => 'Mary', 'age' => null], + 'Paul' => ['name' => 'Paul', 'age' => 44], ], - ], - Arrays::associate($arr, 'name->'), -); + Arrays::associate($arr, 'name'), + ); +}); + -Assert::equal( - [ - 11 => (object) [ +test('empty array', function () { + Assert::same([], Arrays::associate([], 'name')); +}); + + +test('key association with whole row as value', function () use ($arr) { + Assert::same( + [ 'John' => ['name' => 'John', 'age' => 11], - ], - 22 => (object) [ - 'John' => ['name' => 'John', 'age' => 22], - ], - '' => (object) [ 'Mary' => ['name' => 'Mary', 'age' => null], - ], - 44 => (object) [ 'Paul' => ['name' => 'Paul', 'age' => 44], ], - ], - Arrays::associate($arr, 'age->name'), -); - -Assert::equal( - (object) [ - 'John' => ['name' => 'John', 'age' => 11], - 'Mary' => ['name' => 'Mary', 'age' => null], - 'Paul' => ['name' => 'Paul', 'age' => 44], - ], - Arrays::associate($arr, '->name'), -); - -Assert::equal( - (object) [], - Arrays::associate([], '->name'), -); - -Assert::same( - [ - 'John' => [ - 11 => ['name' => 'John', 'age' => 11], - 22 => ['name' => 'John', 'age' => 22], - ], - 'Mary' => [ - '' => ['name' => 'Mary', 'age' => null], - ], - 'Paul' => [ - 44 => ['name' => 'Paul', 'age' => 44], - ], - ], - Arrays::associate($arr, 'name|age'), -); - -Assert::same( - [ - 'John' => ['name' => 'John', 'age' => 11], - 'Mary' => ['name' => 'Mary', 'age' => null], - 'Paul' => ['name' => 'Paul', 'age' => 44], - ], - Arrays::associate($arr, 'name|'), -); - -Assert::same( - [ - 'John' => [ - ['name' => 'John', 'age' => 11], - ['name' => 'John', 'age' => 22], - ], - 'Mary' => [ - ['name' => 'Mary', 'age' => null], - ], - 'Paul' => [ - ['name' => 'Paul', 'age' => 44], + Arrays::associate($arr, 'name='), + ); +}); + + +test('key-value pair association', function () use ($arr) { + Assert::same( + ['John' => 22, 'Mary' => null, 'Paul' => 44], + Arrays::associate($arr, 'name=age'), + ); +}); + + +test('path as array', function () use ($arr) { + Assert::same( + ['John' => 22, 'Mary' => null, 'Paul' => 44], + Arrays::associate($arr, ['name', '=', 'age']), + ); +}); + + +test('object result with key-based access', function () use ($arr) { + Assert::equal( + [ + 'John' => (object) ['name' => 'John', 'age' => 11], + 'Mary' => (object) ['name' => 'Mary', 'age' => null], + 'Paul' => (object) ['name' => 'Paul', 'age' => 44], ], - ], - Arrays::associate($arr, 'name[]'), -); - -Assert::same( - [ - ['John' => ['name' => 'John', 'age' => 11]], - ['John' => ['name' => 'John', 'age' => 22]], - ['Mary' => ['name' => 'Mary', 'age' => null]], - ['Paul' => ['name' => 'Paul', 'age' => 44]], - ], - Arrays::associate($arr, '[]name'), -); - -Assert::same( - ['John', 'John', 'Mary', 'Paul'], - Arrays::associate($arr, '[]=name'), -); - -Assert::same( - [ - 'John' => [ - [11 => ['name' => 'John', 'age' => 11]], - [22 => ['name' => 'John', 'age' => 22]], + Arrays::associate($arr, 'name->'), + ); +}); + + +test('nested object with property-based keys', function () use ($arr) { + Assert::equal( + [ + 11 => (object) ['John' => ['name' => 'John', 'age' => 11]], + 22 => (object) ['John' => ['name' => 'John', 'age' => 22]], + '' => (object) ['Mary' => ['name' => 'Mary', 'age' => null]], + 44 => (object) ['Paul' => ['name' => 'Paul', 'age' => 44]], ], - 'Mary' => [ - ['' => ['name' => 'Mary', 'age' => null]], + Arrays::associate($arr, 'age->name'), + ); +}); + + +test('object as root result', function () use ($arr) { + Assert::equal( + (object) [ + 'John' => ['name' => 'John', 'age' => 11], + 'Mary' => ['name' => 'Mary', 'age' => null], + 'Paul' => ['name' => 'Paul', 'age' => 44], ], - 'Paul' => [ - [44 => ['name' => 'Paul', 'age' => 44]], + Arrays::associate($arr, '->name'), + ); + + Assert::equal( + (object) [], + Arrays::associate([], '->name'), + ); +}); + + +test('grouping with pipe operator', function () use ($arr) { + Assert::same( + [ + 'John' => [ + 11 => ['name' => 'John', 'age' => 11], + 22 => ['name' => 'John', 'age' => 22], + ], + 'Mary' => [ + '' => ['name' => 'Mary', 'age' => null], + ], + 'Paul' => [ + 44 => ['name' => 'Paul', 'age' => 44], + ], + ], + Arrays::associate($arr, 'name|age'), + ); +}); + + +test('grouping with pipe - last value wins on collision', function () use ($arr) { + Assert::same( + [ + 'John' => ['name' => 'John', 'age' => 11], + 'Mary' => ['name' => 'Mary', 'age' => null], + 'Paul' => ['name' => 'Paul', 'age' => 44], ], - ], - Arrays::associate($arr, 'name[]age'), -); - -Assert::same( - $arr, - Arrays::associate($arr, '[]'), -); - -// converts object to array -Assert::same( - $arr, - Arrays::associate($arr = [ + Arrays::associate($arr, 'name|'), + ); +}); + + +test('array grouping with brackets', function () use ($arr) { + Assert::same( + [ + 'John' => [ + ['name' => 'John', 'age' => 11], + ['name' => 'John', 'age' => 22], + ], + 'Mary' => [ + ['name' => 'Mary', 'age' => null], + ], + 'Paul' => [ + ['name' => 'Paul', 'age' => 44], + ], + ], + Arrays::associate($arr, 'name[]'), + ); +}); + + +test('prefix array with keyed items', function () use ($arr) { + Assert::same( + [ + ['John' => ['name' => 'John', 'age' => 11]], + ['John' => ['name' => 'John', 'age' => 22]], + ['Mary' => ['name' => 'Mary', 'age' => null]], + ['Paul' => ['name' => 'Paul', 'age' => 44]], + ], + Arrays::associate($arr, '[]name'), + ); +}); + + +test('flat array with extracted values', function () use ($arr) { + Assert::same( + ['John', 'John', 'Mary', 'Paul'], + Arrays::associate($arr, '[]=name'), + ); +}); + + +test('complex combination with nested arrays', function () use ($arr) { + Assert::same( + [ + 'John' => [ + [11 => ['name' => 'John', 'age' => 11]], + [22 => ['name' => 'John', 'age' => 22]], + ], + 'Mary' => [ + ['' => ['name' => 'Mary', 'age' => null]], + ], + 'Paul' => [ + [44 => ['name' => 'Paul', 'age' => 44]], + ], + ], + Arrays::associate($arr, 'name[]age'), + ); +}); + + +test('identity transformation with empty brackets', function () use ($arr) { + Assert::same($arr, Arrays::associate($arr, '[]')); +}); + + +test('converts objects to arrays in input', function () { + $arr = [ (object) ['name' => 'John', 'age' => 11], (object) ['name' => 'John', 'age' => 22], (object) ['name' => 'Mary', 'age' => null], (object) ['name' => 'Paul', 'age' => 44], - ], '[]'), -); - -// allowes objects in keys -Assert::equal( - ['2014-02-05 00:00:00' => new DateTime('2014-02-05')], - Arrays::associate($arr = [ - ['date' => new DateTime('2014-02-05')], - ], 'date=date'), -); -Assert::equal( - (object) ['2014-02-05 00:00:00' => new DateTime('2014-02-05')], - Arrays::associate($arr = [ - ['date' => new DateTime('2014-02-05')], - ], '->date=date'), -); + ]; + + Assert::same( + [ + ['name' => 'John', 'age' => 11], + ['name' => 'John', 'age' => 22], + ['name' => 'Mary', 'age' => null], + ['name' => 'Paul', 'age' => 44], + ], + Arrays::associate($arr, '[]'), + ); +}); + + +test('allows objects as keys and values', function () { + $arr = [['date' => new DateTime('2014-02-05')]]; + + Assert::equal( + ['2014-02-05 00:00:00' => new DateTime('2014-02-05')], + Arrays::associate($arr, 'date=date'), + ); + + Assert::equal( + (object) ['2014-02-05 00:00:00' => new DateTime('2014-02-05')], + Arrays::associate($arr, '->date=date'), + ); +}); diff --git a/tests/Utils/Arrays.insertBefore().phpt b/tests/Utils/Arrays.insertBefore().phpt index 1db1cbc2..2e1051a0 100644 --- a/tests/Utils/Arrays.insertBefore().phpt +++ b/tests/Utils/Arrays.insertBefore().phpt @@ -21,7 +21,7 @@ $arr = [ ]; -test('first item', function () use ($arr) { +test('insertBefore/After with null key - beginning/end', function () use ($arr) { $dolly = $arr; Arrays::insertBefore($dolly, null, ['new' => 'value']); Assert::same([ @@ -32,7 +32,6 @@ test('first item', function () use ($arr) { 7 => 'fourth', ], $dolly); - $dolly = $arr; Arrays::insertAfter($dolly, null, ['new' => 'value']); Assert::same([ @@ -45,7 +44,7 @@ test('first item', function () use ($arr) { }); -test('last item', function () use ($arr) { +test('insertBefore/After last item', function () use ($arr) { $dolly = $arr; Arrays::insertBefore($dolly, 7, ['new' => 'value']); Assert::same([ @@ -56,7 +55,6 @@ test('last item', function () use ($arr) { 7 => 'fourth', ], $dolly); - $dolly = $arr; Arrays::insertAfter($dolly, 7, ['new' => 'value']); Assert::same([ @@ -69,7 +67,7 @@ test('last item', function () use ($arr) { }); -test('undefined item', function () use ($arr) { +test('insertBefore/After undefined key', function () use ($arr) { $dolly = $arr; Arrays::insertBefore($dolly, 'undefined', ['new' => 'value']); Assert::same([ @@ -80,7 +78,6 @@ test('undefined item', function () use ($arr) { 7 => 'fourth', ], $dolly); - $dolly = $arr; Arrays::insertAfter($dolly, 'undefined', ['new' => 'value']); Assert::same([ @@ -91,3 +88,149 @@ test('undefined item', function () use ($arr) { 'new' => 'value', ], $dolly); }); + + +test('insertBefore/After middle item', function () use ($arr) { + $dolly = $arr; + Arrays::insertBefore($dolly, 1, ['new' => 'value']); + Assert::same([ + '' => 'first', + 0 => 'second', + 'new' => 'value', + 1 => 'third', + 7 => 'fourth', + ], $dolly); + + $dolly = $arr; + Arrays::insertAfter($dolly, 0, ['new' => 'value']); + Assert::same([ + '' => 'first', + 0 => 'second', + 'new' => 'value', + 1 => 'third', + 7 => 'fourth', + ], $dolly); +}); + + +test('insertBefore/After with empty array', function () { + $arr = []; + Arrays::insertBefore($arr, null, ['new' => 'value']); + Assert::same(['new' => 'value'], $arr); + + $arr = []; + Arrays::insertAfter($arr, null, ['new' => 'value']); + Assert::same(['new' => 'value'], $arr); +}); + + +test('insertBefore/After with empty insertion', function () use ($arr) { + $dolly = $arr; + Arrays::insertBefore($dolly, 1, []); + Assert::same($arr, $dolly); + + $dolly = $arr; + Arrays::insertAfter($dolly, 1, []); + Assert::same($arr, $dolly); +}); + + +test('insertBefore/After multiple items', function () use ($arr) { + $dolly = $arr; + Arrays::insertBefore($dolly, 1, ['new1' => 'value1', 'new2' => 'value2', 'new3' => 'value3']); + Assert::same([ + '' => 'first', + 0 => 'second', + 'new1' => 'value1', + 'new2' => 'value2', + 'new3' => 'value3', + 1 => 'third', + 7 => 'fourth', + ], $dolly); + + $dolly = $arr; + Arrays::insertAfter($dolly, 0, ['new1' => 'value1', 'new2' => 'value2']); + Assert::same([ + '' => 'first', + 0 => 'second', + 'new1' => 'value1', + 'new2' => 'value2', + 1 => 'third', + 7 => 'fourth', + ], $dolly); +}); + + +test('insertBefore/After with numeric array', function () { + $arr = ['a', 'b', 'c', 'd']; + + $dolly = $arr; + Arrays::insertBefore($dolly, 2, [99 => 'x']); + Assert::same([0 => 'a', 1 => 'b', 99 => 'x', 2 => 'c', 3 => 'd'], $dolly); + + $dolly = $arr; + Arrays::insertAfter($dolly, 1, [99 => 'x']); + Assert::same([0 => 'a', 1 => 'b', 99 => 'x', 2 => 'c', 3 => 'd'], $dolly); +}); + + +test('insertBefore/After preserves key types', function () { + $arr = ['str' => 'string', 10 => 'int', '' => 'empty']; + + $dolly = $arr; + Arrays::insertBefore($dolly, 10, ['new' => 'value']); + Assert::same(['str' => 'string', 'new' => 'value', 10 => 'int', '' => 'empty'], $dolly); + + $dolly = $arr; + Arrays::insertAfter($dolly, 'str', [20 => 'numeric']); + Assert::same(['str' => 'string', 20 => 'numeric', 10 => 'int', '' => 'empty'], $dolly); +}); + + +test('insertBefore/After first item by key', function () use ($arr) { + $dolly = $arr; + Arrays::insertBefore($dolly, '', ['new' => 'value']); + Assert::same([ + 'new' => 'value', + '' => 'first', + 0 => 'second', + 1 => 'third', + 7 => 'fourth', + ], $dolly); + + $dolly = $arr; + Arrays::insertAfter($dolly, '', ['new' => 'value']); + Assert::same([ + '' => 'first', + 'new' => 'value', + 0 => 'second', + 1 => 'third', + 7 => 'fourth', + ], $dolly); +}); + + +test('insertBefore/After with single element array', function () { + $arr = ['only' => 'one']; + + $dolly = $arr; + Arrays::insertBefore($dolly, 'only', ['new' => 'value']); + Assert::same(['new' => 'value', 'only' => 'one'], $dolly); + + $dolly = $arr; + Arrays::insertAfter($dolly, 'only', ['new' => 'value']); + Assert::same(['only' => 'one', 'new' => 'value'], $dolly); +}); + + +test('insertBefore/After with duplicate values', function () { + $arr = ['a' => 'same', 'b' => 'same', 'c' => 'different']; + + $dolly = $arr; + Arrays::insertBefore($dolly, 'b', ['new' => 'value']); + Assert::same(['a' => 'same', 'new' => 'value', 'b' => 'same', 'c' => 'different'], $dolly); + + $dolly = $arr; + Arrays::insertAfter($dolly, 'a', ['new' => 'value']); + Assert::same(['a' => 'same', 'new' => 'value', 'b' => 'same', 'c' => 'different'], $dolly); +}); diff --git a/tests/Utils/Arrays.mergeTree().phpt b/tests/Utils/Arrays.mergeTree().phpt new file mode 100644 index 00000000..e58023f3 --- /dev/null +++ b/tests/Utils/Arrays.mergeTree().phpt @@ -0,0 +1,108 @@ + 1, 'b' => 2]; + $arr2 = ['c' => 3, 'd' => 4]; + Assert::same(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], Arrays::mergeTree($arr1, $arr2)); +}); + + +test('key collision - prefers value from first array', function () { + $arr1 = ['a' => 1, 'b' => 2]; + $arr2 = ['a' => 99, 'c' => 3]; + Assert::same(['a' => 1, 'b' => 2, 'c' => 3], Arrays::mergeTree($arr1, $arr2)); +}); + + +test('recursive merge of nested arrays', function () { + $arr1 = ['a' => ['b' => 1, 'c' => 2]]; + $arr2 = ['a' => ['d' => 3, 'e' => 4]]; + Assert::same(['a' => ['b' => 1, 'c' => 2, 'd' => 3, 'e' => 4]], Arrays::mergeTree($arr1, $arr2)); +}); + + +test('recursive merge with key collision in nested arrays', function () { + $arr1 = ['a' => ['b' => 1, 'c' => 2]]; + $arr2 = ['a' => ['b' => 99, 'd' => 3]]; + Assert::same(['a' => ['b' => 1, 'c' => 2, 'd' => 3]], Arrays::mergeTree($arr1, $arr2)); +}); + + +test('deep nesting - three levels', function () { + $arr1 = ['a' => ['b' => ['c' => 1, 'd' => 2]]]; + $arr2 = ['a' => ['b' => ['e' => 3]]]; + Assert::same(['a' => ['b' => ['c' => 1, 'd' => 2, 'e' => 3]]], Arrays::mergeTree($arr1, $arr2)); +}); + + +test('mix of array and scalar values - scalar in first array', function () { + $arr1 = ['a' => 1]; + $arr2 = ['a' => ['b' => 2]]; + Assert::same(['a' => 1], Arrays::mergeTree($arr1, $arr2)); +}); + + +test('mix of array and scalar values - scalar in second array', function () { + $arr1 = ['a' => ['b' => 1]]; + $arr2 = ['a' => 99]; + Assert::same(['a' => ['b' => 1]], Arrays::mergeTree($arr1, $arr2)); +}); + + +test('empty first array', function () { + $arr1 = []; + $arr2 = ['a' => 1, 'b' => 2]; + Assert::same(['a' => 1, 'b' => 2], Arrays::mergeTree($arr1, $arr2)); +}); + + +test('empty second array', function () { + $arr1 = ['a' => 1, 'b' => 2]; + $arr2 = []; + Assert::same(['a' => 1, 'b' => 2], Arrays::mergeTree($arr1, $arr2)); +}); + + +test('both arrays empty', function () { + Assert::same([], Arrays::mergeTree([], [])); +}); + + +test('numeric keys are preserved', function () { + $arr1 = [0 => 'a', 1 => 'b']; + $arr2 = [0 => 'x', 2 => 'c']; + Assert::same([0 => 'a', 1 => 'b', 2 => 'c'], Arrays::mergeTree($arr1, $arr2)); +}); + + +test('nested arrays with numeric keys', function () { + $arr1 = ['items' => [0 => 'a', 1 => 'b']]; + $arr2 = ['items' => [0 => 'x', 2 => 'c']]; + Assert::same(['items' => [0 => 'a', 1 => 'b', 2 => 'c']], Arrays::mergeTree($arr1, $arr2)); +}); + + +test('preserves null values', function () { + $arr1 = ['a' => null, 'b' => ['c' => null]]; + $arr2 = ['a' => 1, 'b' => ['c' => 2, 'd' => null]]; + Assert::same(['a' => null, 'b' => ['c' => null, 'd' => null]], Arrays::mergeTree($arr1, $arr2)); +}); + + +test('handles boolean and other scalar types', function () { + $arr1 = ['bool' => true, 'float' => 1.5, 'string' => 'test']; + $arr2 = ['bool' => false, 'int' => 42]; + Assert::same(['bool' => true, 'float' => 1.5, 'string' => 'test', 'int' => 42], Arrays::mergeTree($arr1, $arr2)); +}); diff --git a/tests/Utils/Arrays.normalize.phpt b/tests/Utils/Arrays.normalize.phpt index 578a548f..9101dd5c 100644 --- a/tests/Utils/Arrays.normalize.phpt +++ b/tests/Utils/Arrays.normalize.phpt @@ -1,7 +1,7 @@ null, - 'a' => 'second', - 'd' => ['third'], - 'fourth' => null, - ], - Arrays::normalize([ - 1 => 'first', - 'a' => 'second', - 'd' => ['third'], - 7 => 'fourth', - ]), -); - - -Assert::same( - [ - 'first' => true, - '' => 'second', - ], - Arrays::normalize([ - 1 => 'first', - '' => 'second', - ], filling: true), -); +test('normalizes numeric keys to their string values with null filling', function () { + Assert::same( + [ + 'first' => null, + 'a' => 'second', + 'd' => ['third'], + 'fourth' => null, + ], + Arrays::normalize([ + 1 => 'first', + 'a' => 'second', + 'd' => ['third'], + 7 => 'fourth', + ]), + ); +}); + + +test('uses custom filling value for normalized keys', function () { + Assert::same( + [ + 'first' => true, + '' => 'second', + ], + Arrays::normalize([ + 1 => 'first', + '' => 'second', + ], filling: true), + ); +}); + + +test('handles empty array', function () { + Assert::same([], Arrays::normalize([])); +}); + + +test('keeps associative array unchanged', function () { + Assert::same( + ['a' => 'x', 'b' => 'y'], + Arrays::normalize(['a' => 'x', 'b' => 'y']), + ); +}); + + +test('handles mixed numeric and string keys', function () { + Assert::same( + ['a' => 'x', 'b' => null, 'c' => 'z'], + Arrays::normalize(['a' => 'x', 0 => 'b', 'c' => 'z']), + ); +}); diff --git a/tests/Utils/Arrays.renameKey().phpt b/tests/Utils/Arrays.renameKey().phpt index 64f58ee3..9cab1c59 100644 --- a/tests/Utils/Arrays.renameKey().phpt +++ b/tests/Utils/Arrays.renameKey().phpt @@ -13,68 +13,194 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -$arr = [ - '' => 'first', - 0 => 'second', - 7 => 'fourth', - 1 => 'third', -]; - -Assert::true(Arrays::renameKey($arr, '1', 'new1')); -Assert::same([ - '' => 'first', - 0 => 'second', - 7 => 'fourth', - 'new1' => 'third', -], $arr); - -Arrays::renameKey($arr, 0, 'new2'); -Assert::same([ - '' => 'first', - 'new2' => 'second', - 7 => 'fourth', - 'new1' => 'third', -], $arr); - -Arrays::renameKey($arr, '', 'new3'); -Assert::same([ - 'new3' => 'first', - 'new2' => 'second', - 7 => 'fourth', - 'new1' => 'third', -], $arr); - -Arrays::renameKey($arr, '', 'new4'); -Assert::same([ - 'new3' => 'first', - 'new2' => 'second', - 7 => 'fourth', - 'new1' => 'third', -], $arr); - -Assert::false(Arrays::renameKey($arr, 'undefined', 'new5')); -Assert::same([ - 'new3' => 'first', - 'new2' => 'second', - 7 => 'fourth', - 'new1' => 'third', -], $arr); - -Arrays::renameKey($arr, 'new2', 'new3'); -Assert::same([ - 'new3' => 'second', - 7 => 'fourth', - 'new1' => 'third', -], $arr); - -Arrays::renameKey($arr, 'new3', 'new1'); -Assert::same([ - 'new1' => 'second', - 7 => 'fourth', -], $arr); - -Assert::true(Arrays::renameKey($arr, 'new1', 'new1')); -Assert::same([ - 'new1' => 'second', - 7 => 'fourth', -], $arr); +test('successfully renames existing key', function () { + $arr = [ + '' => 'first', + 0 => 'second', + 7 => 'fourth', + 1 => 'third', + ]; + + Assert::true(Arrays::renameKey($arr, '1', 'new1')); + Assert::same([ + '' => 'first', + 0 => 'second', + 7 => 'fourth', + 'new1' => 'third', + ], $arr); +}); + + +test('renames numeric key to string key', function () { + $arr = [ + '' => 'first', + 0 => 'second', + 7 => 'fourth', + 1 => 'third', + ]; + + Arrays::renameKey($arr, 0, 'new2'); + Assert::same([ + '' => 'first', + 'new2' => 'second', + 7 => 'fourth', + 1 => 'third', + ], $arr); +}); + + +test('renames empty string key', function () { + $arr = [ + '' => 'first', + 'a' => 'second', + ]; + + Arrays::renameKey($arr, '', 'new'); + Assert::same([ + 'new' => 'first', + 'a' => 'second', + ], $arr); +}); + + +test('returns false when key does not exist', function () { + $arr = ['a' => 'first', 'b' => 'second']; + + Assert::false(Arrays::renameKey($arr, 'nonexistent', 'new')); + Assert::same(['a' => 'first', 'b' => 'second'], $arr); +}); + + +test('renaming to existing key overwrites it', function () { + $arr = [ + 'new3' => 'first', + 'new2' => 'second', + 7 => 'fourth', + 'new1' => 'third', + ]; + + Arrays::renameKey($arr, 'new2', 'new3'); + Assert::same([ + 'new3' => 'second', + 7 => 'fourth', + 'new1' => 'third', + ], $arr); +}); + + +test('renaming to existing key - second case', function () { + $arr = [ + 'new3' => 'second', + 7 => 'fourth', + 'new1' => 'third', + ]; + + Arrays::renameKey($arr, 'new3', 'new1'); + Assert::same([ + 'new1' => 'second', + 7 => 'fourth', + ], $arr); +}); + + +test('renaming key to itself returns true and preserves array', function () { + $arr = ['key' => 'value', 'other' => 'data']; + + Assert::true(Arrays::renameKey($arr, 'key', 'key')); + Assert::same(['key' => 'value', 'other' => 'data'], $arr); +}); + + +test('preserves array order when renaming', function () { + $arr = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]; + + Arrays::renameKey($arr, 'b', 'new'); + Assert::same(['a' => 1, 'new' => 2, 'c' => 3, 'd' => 4], $arr); + + // Verify order is preserved + Assert::same(['a', 'new', 'c', 'd'], array_keys($arr)); +}); + + +test('works with single element array', function () { + $arr = ['only' => 'one']; + + Assert::true(Arrays::renameKey($arr, 'only', 'renamed')); + Assert::same(['renamed' => 'one'], $arr); +}); + + +test('handles numeric key conversions', function () { + $arr = [0 => 'zero', 1 => 'one', 2 => 'two']; + + Arrays::renameKey($arr, 0, 'first'); + Assert::same(['first' => 'zero', 1 => 'one', 2 => 'two'], $arr); + + Arrays::renameKey($arr, 'first', 10); + Assert::same([10 => 'zero', 1 => 'one', 2 => 'two'], $arr); +}); + + +test('handles mixed key types', function () { + $arr = ['str' => 'string', 5 => 'int', '' => 'empty']; + + Assert::true(Arrays::renameKey($arr, 5, 'five')); + Assert::same(['str' => 'string', 'five' => 'int', '' => 'empty'], $arr); + + Assert::true(Arrays::renameKey($arr, 'str', 0)); + Assert::same([0 => 'string', 'five' => 'int', '' => 'empty'], $arr); +}); + + +test('works with complex values', function () { + $obj = (object) ['prop' => 'value']; + $arr = ['a' => [1, 2, 3], 'b' => $obj, 'c' => null]; + + Arrays::renameKey($arr, 'a', 'array'); + Assert::same(['array' => [1, 2, 3], 'b' => $obj, 'c' => null], $arr); +}); + + +test('handles consecutive renames', function () { + $arr = ['original' => 'value']; + + Arrays::renameKey($arr, 'original', 'temp'); + Arrays::renameKey($arr, 'temp', 'final'); + + Assert::same(['final' => 'value'], $arr); +}); + + +test('returns false for empty array', function () { + $arr = []; + + Assert::false(Arrays::renameKey($arr, 'any', 'key')); + Assert::same([], $arr); +}); + + +test('handles string numeric keys correctly', function () { + $arr = ['1' => 'one', '2' => 'two', '10' => 'ten']; + + // String '1' should match numeric key 1 + Assert::true(Arrays::renameKey($arr, '1', 'first')); + Assert::same(['first' => 'one', '2' => 'two', '10' => 'ten'], $arr); +}); + + +test('renaming preserves position in associative array', function () { + $arr = [ + 'first' => 1, + 'second' => 2, + 'third' => 3, + 'fourth' => 4, + ]; + + Arrays::renameKey($arr, 'second', 'TWO'); + + $keys = array_keys($arr); + Assert::same('first', $keys[0]); + Assert::same('TWO', $keys[1]); + Assert::same('third', $keys[2]); + Assert::same('fourth', $keys[3]); +}); diff --git a/tests/Utils/Arrays.renameKey().ref.phpt b/tests/Utils/Arrays.renameKey().ref.phpt index ab2f3776..a67e887d 100644 --- a/tests/Utils/Arrays.renameKey().ref.phpt +++ b/tests/Utils/Arrays.renameKey().ref.phpt @@ -1,7 +1,7 @@ 'a', - 2 => 'b', -]; +test('preserves references when renaming key', function () { + $arr = [ + 1 => 'a', + 2 => 'b', + ]; -$arr2 = [ - 1 => &$arr[1], - 2 => &$arr[2], -]; + $arr2 = [ + 1 => &$arr[1], + 2 => &$arr[2], + ]; -Arrays::renameKey($arr, '1', 'new1'); + Arrays::renameKey($arr, '1', 'new1'); -$arr2[1] = 'A'; -$arr2[2] = 'B'; + // Modify via reference + $arr2[1] = 'A'; + $arr2[2] = 'B'; -Assert::same('A', $arr['new1']); -Assert::same('B', $arr[2]); + // Should reflect in renamed array + Assert::same('A', $arr['new1']); + Assert::same('B', $arr[2]); +}); -Arrays::renameKey($arr, 'new1', 2); +test('preserves references when renaming to existing numeric key', function () { + $arr = [ + 1 => 'a', + 2 => 'b', + ]; -$arr2[1] = 'AA'; -$arr2[2] = 'BB'; + $arr2 = [ + 1 => &$arr[1], + 2 => &$arr[2], + ]; -Assert::same('AA', $arr[2]); + Arrays::renameKey($arr, '1', 'new1'); + Arrays::renameKey($arr, 'new1', 2); + + // Modify via reference + $arr2[1] = 'AA'; + $arr2[2] = 'BB'; + + // The value at key 2 should now be the renamed value with preserved reference + Assert::same('AA', $arr[2]); +}); + + +test('maintains reference through multiple renames', function () { + $value = 'original'; + $arr = ['key' => &$value]; + + Arrays::renameKey($arr, 'key', 'temp'); + Arrays::renameKey($arr, 'temp', 'final'); + + // Modify original variable + $value = 'modified'; + + // Should reflect in array with renamed key + Assert::same('modified', $arr['final']); +}); + + +test('reference is preserved when renaming to same key', function () { + $value = 'test'; + $arr = ['key' => &$value]; + + Arrays::renameKey($arr, 'key', 'key'); + + $value = 'changed'; + + Assert::same('changed', $arr['key']); +}); + + +test('complex reference scenario with nested values', function () { + $shared = ['shared' => 'data']; + $arr = [ + 'a' => &$shared, + 'b' => ['nested' => 'value'], + ]; + + Arrays::renameKey($arr, 'a', 'shared_ref'); + + // Modify shared reference + $shared['shared'] = 'modified'; + + // Should be reflected in renamed key + Assert::same(['shared' => 'modified'], $arr['shared_ref']); +}); diff --git a/tests/Utils/Strings.compare().phpt b/tests/Utils/Strings.compare().phpt index 3aa78fb9..480924f5 100644 --- a/tests/Utils/Strings.compare().phpt +++ b/tests/Utils/Strings.compare().phpt @@ -14,19 +14,154 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -Assert::true(Strings::compare('', '')); -Assert::true(Strings::compare('', '', 0)); -Assert::true(Strings::compare('', '', 1)); -Assert::false(Strings::compare('xy', 'xx')); -Assert::true(Strings::compare('xy', 'xx', 0)); -Assert::true(Strings::compare('xy', 'xx', 1)); -Assert::false(Strings::compare('xy', 'yy', 1)); -Assert::true(Strings::compare('xy', 'yy', -1)); -Assert::true(Strings::compare('xy', 'yy', -1)); -Assert::true(Strings::compare("I\u{F1}t\u{EB}rn\u{E2}ti\u{F4}n\u{E0}liz\u{E6}ti\u{F8}n", "I\u{D1}T\u{CB}RN\u{C2}TI\u{D4}N\u{C0}LIZ\u{C6}TI\u{D8}N")); // Iñtërnâtiônàlizætiøn -Assert::true(Strings::compare("I\u{F1}t\u{EB}rn\u{E2}ti\u{F4}n\u{E0}liz\u{E6}ti\u{F8}n", "I\u{D1}T\u{CB}RN\u{C2}TI\u{D4}N\u{C0}LIZ\u{C6}TI\u{D8}N", 10)); - -if (class_exists('Normalizer')) { +test('compares empty strings', function () { + Assert::true(Strings::compare('', '')); + Assert::true(Strings::compare('', '', 0)); + Assert::true(Strings::compare('', '', 1)); + Assert::true(Strings::compare('', '', -1)); +}); + + +test('compares identical strings', function () { + Assert::true(Strings::compare('hello', 'hello')); + Assert::true(Strings::compare('test', 'test', 10)); +}); + + +test('case-insensitive comparison by default', function () { + Assert::true(Strings::compare('Hello', 'HELLO')); + Assert::true(Strings::compare('Test', 'test')); + Assert::true(Strings::compare('ABC', 'abc')); +}); + + +test('full string comparison', function () { + Assert::false(Strings::compare('xy', 'xx')); + Assert::false(Strings::compare('hello', 'world')); + Assert::false(Strings::compare('test', 'testing')); +}); + + +test('compares with length limit', function () { + Assert::true(Strings::compare('xy', 'xx', 0)); + Assert::true(Strings::compare('xy', 'xx', 1)); + Assert::false(Strings::compare('xy', 'yy', 1)); +}); + + +test('compares from end with negative length', function () { + Assert::true(Strings::compare('xy', 'yy', -1)); + Assert::true(Strings::compare('abc', 'xbc', -2)); + Assert::false(Strings::compare('abc', 'xyz', -2)); +}); + + +test('compares Unicode strings case-insensitively', function () { + // Iñtërnâtiônàlizætiøn + Assert::true( + Strings::compare( + "I\u{F1}t\u{EB}rn\u{E2}ti\u{F4}n\u{E0}liz\u{E6}ti\u{F8}n", + "I\u{D1}T\u{CB}RN\u{C2}TI\u{D4}N\u{C0}LIZ\u{C6}TI\u{D8}N", + ), + ); + + Assert::true( + Strings::compare( + "I\u{F1}t\u{EB}rn\u{E2}ti\u{F4}n\u{E0}liz\u{E6}ti\u{F8}n", + "I\u{D1}T\u{CB}RN\u{C2}TI\u{D4}N\u{C0}LIZ\u{C6}TI\u{D8}N", + 10, + ), + ); +}); + + +test('compares NFC and NFD Unicode normalization forms', function () { + if (!class_exists('Normalizer')) { + Tester\Environment::skip('Normalizer class not available'); + } + + // Å (U+00C5) vs A (U+0041) + ̊ (U+030A) Assert::true(Strings::compare("\xC3\x85", "A\xCC\x8A"), 'comparing NFC with NFD form'); Assert::true(Strings::compare("A\xCC\x8A", "\xC3\x85"), 'comparing NFD with NFC form'); -} +}); + + +test('compares with zero length', function () { + Assert::true(Strings::compare('completely', 'different', 0)); + Assert::true(Strings::compare('any', 'thing', 0)); +}); + + +test('handles Czech and other diacritics', function () { + Assert::true(Strings::compare('Příliš', 'PŘÍLIŠ')); + Assert::true(Strings::compare('žluťoučký', 'ŽLUŤOUČKÝ')); + Assert::true(Strings::compare('kůň', 'KŮŇ')); +}); + + +test('compares Cyrillic characters', function () { + Assert::true(Strings::compare('Привет', 'ПРИВЕТ')); + Assert::true(Strings::compare('мир', 'МИР')); +}); + + +test('partial comparison from beginning', function () { + Assert::true(Strings::compare('hello world', 'hello universe', 5)); + Assert::false(Strings::compare('hello world', 'hi world', 2)); +}); + + +test('partial comparison from end', function () { + Assert::true(Strings::compare('hello world', 'hey world', -5)); + Assert::false(Strings::compare('hello world', 'hello mars', -4)); +}); + + +test('handles strings with different lengths', function () { + Assert::false(Strings::compare('short', 'much longer string')); + Assert::true(Strings::compare('short', 'SHORE', 4)); + Assert::false(Strings::compare('short', 'SHORE', 5)); +}); + + +test('handles special characters', function () { + Assert::true(Strings::compare('test@example.com', 'TEST@EXAMPLE.COM')); + Assert::true(Strings::compare('path/to/file', 'PATH/TO/FILE')); + Assert::true(Strings::compare('hello-world', 'HELLO-WORLD')); +}); + + +test('compares with emoji and symbols', function () { + Assert::true(Strings::compare('hello 😀', 'hello 😀')); + Assert::false(Strings::compare('😀', '😁')); +}); + + +test('edge case with very long strings', function () { + $long1 = str_repeat('a', 1000); + $long2 = str_repeat('A', 1000); + + Assert::true(Strings::compare($long1, $long2)); + Assert::true(Strings::compare($long1, $long2, 500)); + Assert::true(Strings::compare($long1, $long2, -500)); +}); + + +test('handles null bytes in strings', function () { + Assert::true(Strings::compare("test\x00string", "TEST\x00STRING")); + Assert::false(Strings::compare("test\x00", "test\x01")); +}); + + +test('single character comparison', function () { + Assert::true(Strings::compare('a', 'A')); + Assert::false(Strings::compare('a', 'b')); + Assert::true(Strings::compare('a', 'b', 0)); +}); + + +test('comparison with whitespace', function () { + Assert::false(Strings::compare('hello', ' hello')); + Assert::false(Strings::compare('hello ', 'hello')); + Assert::true(Strings::compare(' ', ' ')); +}); diff --git a/tests/Utils/Strings.length().phpt b/tests/Utils/Strings.length().phpt index be2acb65..2dffed04 100644 --- a/tests/Utils/Strings.length().phpt +++ b/tests/Utils/Strings.length().phpt @@ -13,8 +13,44 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -Assert::same(0, Strings::length('')); -Assert::same(20, Strings::length("I\u{F1}t\u{EB}rn\u{E2}ti\u{F4}n\u{E0}liz\u{E6}ti\u{F8}n")); // Iñtërnâtiônàlizætiøn -Assert::same(1, Strings::length("\u{10000}")); // U+010000 -Assert::same(6, Strings::length("ma\u{F1}ana")); // mañana, U+00F1 -Assert::same(7, Strings::length("man\u{303}ana")); // mañana, U+006E + U+0303 (combining character) +test('returns zero for empty string', function () { + Assert::same(0, Strings::length('')); +}); + + +test('handles ASCII strings', function () { + Assert::same(5, Strings::length('hello')); + Assert::same(13, Strings::length('Hello, World!')); +}); + + +test('counts UTF-8 characters correctly', function () { + // Iñtërnâtiônàlizætiøn + Assert::same(20, Strings::length("I\u{F1}t\u{EB}rn\u{E2}ti\u{F4}n\u{E0}liz\u{E6}ti\u{F8}n")); + Assert::same(1, Strings::length("\u{10000}")); // U+010000 +}); + + +test('counts precomposed characters as single unit', function () { + Assert::same(6, Strings::length("ma\u{F1}ana")); // mañana, U+00F1 +}); + + +test('counts combining characters separately', function () { + // mañana, U+006E + U+0303 (combining character) + Assert::same(7, Strings::length("man\u{303}ana")); +}); + + +test('handles emoji and special symbols', function () { + // Emoji are counted as single code points (even if multiple bytes) + Assert::same(1, Strings::length('😀')); + Assert::same(6, Strings::length('Hello😀')); +}); + + +test('handles various Unicode ranges', function () { + Assert::same(4, Strings::length('中文字符')); // Chinese characters + Assert::same(6, Strings::length('Привет')); // Cyrillic + Assert::same(5, Strings::length('مرحبا')); // Arabic +}); diff --git a/tests/Utils/Strings.pad.phpt b/tests/Utils/Strings.pad.phpt index 86b3bc5e..83986e88 100644 --- a/tests/Utils/Strings.pad.phpt +++ b/tests/Utils/Strings.pad.phpt @@ -13,21 +13,129 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; -Assert::same('ŤOUŤOUŤŽLU', Strings::padLeft("\u{17D}LU", 10, "\u{164}OU")); -Assert::same('ŤOUŤOUŽLU', Strings::padLeft("\u{17D}LU", 9, "\u{164}OU")); -Assert::same('ŽLU', Strings::padLeft("\u{17D}LU", 3, "\u{164}OU")); -Assert::same('ŽLU', Strings::padLeft("\u{17D}LU", 0, "\u{164}OU")); -Assert::same('ŽLU', Strings::padLeft("\u{17D}LU", -1, "\u{164}OU")); -Assert::same('ŤŤŤŤŤŤŤŽLU', Strings::padLeft("\u{17D}LU", 10, "\u{164}")); -Assert::same('ŽLU', Strings::padLeft("\u{17D}LU", 3, "\u{164}")); -Assert::same(' ŽLU', Strings::padLeft("\u{17D}LU", 10)); - - -Assert::same('ŽLUŤOUŤOUŤ', Strings::padRight("\u{17D}LU", 10, "\u{164}OU")); -Assert::same('ŽLUŤOUŤOU', Strings::padRight("\u{17D}LU", 9, "\u{164}OU")); -Assert::same('ŽLU', Strings::padRight("\u{17D}LU", 3, "\u{164}OU")); -Assert::same('ŽLU', Strings::padRight("\u{17D}LU", 0, "\u{164}OU")); -Assert::same('ŽLU', Strings::padRight("\u{17D}LU", -1, "\u{164}OU")); -Assert::same('ŽLUŤŤŤŤŤŤŤ', Strings::padRight("\u{17D}LU", 10, "\u{164}")); -Assert::same('ŽLU', Strings::padRight("\u{17D}LU", 3, "\u{164}")); -Assert::same('ŽLU ', Strings::padRight("\u{17D}LU", 10)); +test('padLeft with ASCII strings', function () { + Assert::same('00042', Strings::padLeft('42', 5, '0')); + Assert::same('xxxhello', Strings::padLeft('hello', 8, 'x')); + Assert::same('abcabcabc123', Strings::padLeft('123', 12, 'abc')); +}); + + +test('padRight with ASCII strings', function () { + Assert::same('42000', Strings::padRight('42', 5, '0')); + Assert::same('helloxxx', Strings::padRight('hello', 8, 'x')); + Assert::same('123abcabcabc', Strings::padRight('123', 12, 'abc')); +}); + + +test('padLeft with empty string', function () { + Assert::same('-----', Strings::padLeft('', 5, '-')); + Assert::same(' ', Strings::padLeft('', 5)); + Assert::same('', Strings::padLeft('', 0, '-')); +}); + + +test('padRight with empty string', function () { + Assert::same('-----', Strings::padRight('', 5, '-')); + Assert::same(' ', Strings::padRight('', 5)); + Assert::same('', Strings::padRight('', 0, '-')); +}); + + +test('padLeft with exact length', function () { + Assert::same('hello', Strings::padLeft('hello', 5, 'x')); + Assert::same('test', Strings::padLeft('test', 4, '0')); +}); + + +test('padRight with exact length', function () { + Assert::same('hello', Strings::padRight('hello', 5, 'x')); + Assert::same('test', Strings::padRight('test', 4, '0')); +}); + + +test('padLeft with longer padding than needed', function () { + Assert::same('abchi', Strings::padLeft('hi', 5, 'abcdefgh')); + Assert::same('xyztest', Strings::padLeft('test', 7, 'xyzuvw')); +}); + + +test('padRight with longer padding than needed', function () { + Assert::same('hiabc', Strings::padRight('hi', 5, 'abcdefgh')); + Assert::same('testxyz', Strings::padRight('test', 7, 'xyzuvw')); +}); + + +test('padLeft with multi-byte padding string', function () { + // ŽLU padded with ŤOU + Assert::same('ŤOUŤOUŤŽLU', Strings::padLeft("\u{17D}LU", 10, "\u{164}OU")); + Assert::same('ŤOUŤOUŽLU', Strings::padLeft("\u{17D}LU", 9, "\u{164}OU")); +}); + + +test('padLeft returns original when length is reached', function () { + Assert::same('ŽLU', Strings::padLeft("\u{17D}LU", 3, "\u{164}OU")); + Assert::same('ŽLU', Strings::padLeft("\u{17D}LU", 0, "\u{164}OU")); + Assert::same('ŽLU', Strings::padLeft("\u{17D}LU", -1, "\u{164}OU")); +}); + + +test('padLeft with single multi-byte character', function () { + Assert::same('ŤŤŤŤŤŤŤŽLU', Strings::padLeft("\u{17D}LU", 10, "\u{164}")); + Assert::same('ŽLU', Strings::padLeft("\u{17D}LU", 3, "\u{164}")); +}); + + +test('padLeft with default space padding', function () { + Assert::same(' ŽLU', Strings::padLeft("\u{17D}LU", 10)); + Assert::same(' hello', Strings::padLeft('hello', 10)); +}); + + +test('padRight with multi-byte padding string', function () { + Assert::same('ŽLUŤOUŤOUŤ', Strings::padRight("\u{17D}LU", 10, "\u{164}OU")); + Assert::same('ŽLUŤOUŤOU', Strings::padRight("\u{17D}LU", 9, "\u{164}OU")); +}); + + +test('padRight returns original when length is reached', function () { + Assert::same('ŽLU', Strings::padRight("\u{17D}LU", 3, "\u{164}OU")); + Assert::same('ŽLU', Strings::padRight("\u{17D}LU", 0, "\u{164}OU")); + Assert::same('ŽLU', Strings::padRight("\u{17D}LU", -1, "\u{164}OU")); +}); + + +test('padRight with single multi-byte character', function () { + Assert::same('ŽLUŤŤŤŤŤŤŤ', Strings::padRight("\u{17D}LU", 10, "\u{164}")); + Assert::same('ŽLU', Strings::padRight("\u{17D}LU", 3, "\u{164}")); +}); + + +test('padRight with default space padding', function () { + Assert::same('ŽLU ', Strings::padRight("\u{17D}LU", 10)); + Assert::same('hello ', Strings::padRight('hello', 10)); +}); + + +test('padLeft with emoji', function () { + Assert::same('😀😀😀hi', Strings::padLeft('hi', 5, '😀')); + Assert::same('😀😀test', Strings::padLeft('test', 6, '😀')); +}); + + +test('padRight with emoji', function () { + Assert::same('hi😀😀😀', Strings::padRight('hi', 5, '😀')); + Assert::same('test😀😀', Strings::padRight('test', 6, '😀')); +}); + + +test('padLeft handles combining characters', function () { + // man + combining tilde = mañana + Assert::same('..man', Strings::padLeft('man', 5, '.')); + Assert::same("..man\u{303}", Strings::padLeft("man\u{303}", 6, '.')); +}); + + +test('padRight handles combining characters', function () { + Assert::same('man..', Strings::padRight('man', 5, '.')); + Assert::same("man\u{303}..", Strings::padRight("man\u{303}", 6, '.')); +}); diff --git a/tests/Utils/Strings.truncate().phpt b/tests/Utils/Strings.truncate().phpt index cdb4ccd8..82ea92f1 100644 --- a/tests/Utils/Strings.truncate().phpt +++ b/tests/Utils/Strings.truncate().phpt @@ -15,41 +15,114 @@ require __DIR__ . '/../bootstrap.php'; $s = "\u{158}ekn\u{11B}te, jak se (dnes) m\u{E1}te?"; // Řekněte, jak se (dnes) máte? -Assert::same('…', Strings::truncate($s, -1)); // length=-1 -Assert::same('…', Strings::truncate($s, 0)); // length=0 -Assert::same('…', Strings::truncate($s, 1)); // length=1 -Assert::same('Ř…', Strings::truncate($s, 2)); // length=2 -Assert::same('Ře…', Strings::truncate($s, 3)); // length=3 -Assert::same('Řek…', Strings::truncate($s, 4)); // length=4 -Assert::same('Řekn…', Strings::truncate($s, 5)); // length=5 -Assert::same('Řekně…', Strings::truncate($s, 6)); // length=6 -Assert::same('Řeknět…', Strings::truncate($s, 7)); // length=7 -Assert::same('Řekněte…', Strings::truncate($s, 8)); // length=8 -Assert::same('Řekněte,…', Strings::truncate($s, 9)); // length=9 -Assert::same('Řekněte,…', Strings::truncate($s, 10)); // length=10 -Assert::same('Řekněte,…', Strings::truncate($s, 11)); // length=11 -Assert::same('Řekněte,…', Strings::truncate($s, 12)); // length=12 -Assert::same('Řekněte, jak…', Strings::truncate($s, 13)); // length=13 -Assert::same('Řekněte, jak…', Strings::truncate($s, 14)); // length=14 -Assert::same('Řekněte, jak…', Strings::truncate($s, 15)); // length=15 -Assert::same('Řekněte, jak se…', Strings::truncate($s, 16)); // length=16 -Assert::same('Řekněte, jak se …', Strings::truncate($s, 17)); // length=17 -Assert::same('Řekněte, jak se …', Strings::truncate($s, 18)); // length=18 -Assert::same('Řekněte, jak se …', Strings::truncate($s, 19)); // length=19 -Assert::same('Řekněte, jak se …', Strings::truncate($s, 20)); // length=20 -Assert::same('Řekněte, jak se …', Strings::truncate($s, 21)); // length=21 -Assert::same('Řekněte, jak se (dnes…', Strings::truncate($s, 22)); // length=22 -Assert::same('Řekněte, jak se (dnes)…', Strings::truncate($s, 23)); // length=23 -Assert::same('Řekněte, jak se (dnes)…', Strings::truncate($s, 24)); // length=24 -Assert::same('Řekněte, jak se (dnes)…', Strings::truncate($s, 25)); // length=25 -Assert::same('Řekněte, jak se (dnes)…', Strings::truncate($s, 26)); // length=26 -Assert::same('Řekněte, jak se (dnes)…', Strings::truncate($s, 27)); // length=27 -Assert::same('Řekněte, jak se (dnes) máte?', Strings::truncate($s, 28)); // length=28 -Assert::same('Řekněte, jak se (dnes) máte?', Strings::truncate($s, 29)); // length=29 -Assert::same('Řekněte, jak se (dnes) máte?', Strings::truncate($s, 30)); // length=30 -Assert::same('Řekněte, jak se (dnes) máte?', Strings::truncate($s, 31)); // length=31 -Assert::same('Řekněte, jak se (dnes) máte?', Strings::truncate($s, 32)); // length=32 - -// mañana, U+006E + U+0303 (combining character) -Assert::same("man\u{303}", Strings::truncate("man\u{303}ana", 4, '')); -Assert::same('man', Strings::truncate("man\u{303}ana", 3, '')); + +test('truncates to ellipsis when length is too short', function () use ($s) { + Assert::same('…', Strings::truncate($s, -1)); + Assert::same('…', Strings::truncate($s, 0)); + Assert::same('…', Strings::truncate($s, 1)); +}); + + +test('cuts at character boundary when no word break available', function () use ($s) { + Assert::same('Ř…', Strings::truncate($s, 2)); + Assert::same('Ře…', Strings::truncate($s, 3)); + Assert::same('Řek…', Strings::truncate($s, 4)); +}); + + +test('preserves whole words when possible', function () use ($s) { + // At length 9, can't fit "jak" so keeps "Řekněte," + Assert::same('Řekněte,…', Strings::truncate($s, 9)); + Assert::same('Řekněte,…', Strings::truncate($s, 10)); + Assert::same('Řekněte,…', Strings::truncate($s, 11)); + + // At length 13, can fit "jak" + Assert::same('Řekněte, jak…', Strings::truncate($s, 13)); + + // At length 16, can fit "jak se" + Assert::same('Řekněte, jak se…', Strings::truncate($s, 16)); +}); + + +test('handles word breaks with spaces and punctuation', function () use ($s) { + // Breaks at space before parenthesis + Assert::same('Řekněte, jak se …', Strings::truncate($s, 17)); + Assert::same('Řekněte, jak se …', Strings::truncate($s, 20)); + + // Includes content in parentheses + Assert::same('Řekněte, jak se (dnes…', Strings::truncate($s, 22)); + Assert::same('Řekněte, jak se (dnes)…', Strings::truncate($s, 23)); +}); + + +test('returns original string when length is sufficient', function () use ($s) { + Assert::same('Řekněte, jak se (dnes) máte?', Strings::truncate($s, 28)); + Assert::same('Řekněte, jak se (dnes) máte?', Strings::truncate($s, 100)); +}); + + +test('handles combining characters', function () { + // mañana, U+006E + U+0303 (combining character) + // With length 4, keeps "man" + combining tilde + Assert::same("man\u{303}", Strings::truncate("man\u{303}ana", 4, '')); + + // With length 3, cuts before combining character + Assert::same('man', Strings::truncate("man\u{303}ana", 3, '')); +}); + + +test('uses custom append string', function () { + Assert::same('Hello...', Strings::truncate('Hello, World!', 8, '...')); + Assert::same('Hello [more]', Strings::truncate('Hello, World!', 12, ' [more]')); + Assert::same('Hello', Strings::truncate('Hello, World!', 5, '')); +}); + + +test('handles Unicode text with word boundaries', function () { + $czech = 'Příliš žluťoučký kůň úpěl ďábelské ódy'; + Assert::same('Příliš…', Strings::truncate($czech, 10)); + Assert::same('Příliš žluťoučký…', Strings::truncate($czech, 20)); + + $russian = 'Съешь же ещё этих мягких французских булок'; + Assert::same('Съешь же…', Strings::truncate($russian, 12)); +}); + + +test('handles text without spaces', function () { + Assert::same('abc…', Strings::truncate('abcdefghijk', 4)); + Assert::same('12345…', Strings::truncate('1234567890', 6)); +}); + + +test('handles empty string', function () { + Assert::same('', Strings::truncate('', 10)); + Assert::same('', Strings::truncate('', 0)); +}); + + +test('handles single word shorter than limit', function () { + Assert::same('Hello', Strings::truncate('Hello', 10)); + Assert::same('Test', Strings::truncate('Test', 100)); +}); + + +test('breaks at various punctuation marks', function () { + Assert::same('Hello,…', Strings::truncate('Hello, World!', 7)); + Assert::same('one-two…', Strings::truncate('one-two-three', 8)); + Assert::same('path…', Strings::truncate('path/to/file', 7)); +}); + + +test('handles emoji and multi-byte characters', function () { + Assert::same('Hello…', Strings::truncate('Hello 😀 World', 6)); + Assert::same('😀😀😀…', Strings::truncate('😀😀😀😀😀', 4)); +}); + + +test('handles grapheme clusters correctly', function () { + // Woman technologist emoji with skin tone modifier + $text = '👩‍💻 coding is fun'; + $truncated = Strings::truncate($text, 10); + // Should handle multi-codepoint grapheme clusters + Assert::type('string', $truncated); +}); diff --git a/tests/types/utils-types.php b/tests/types/utils-types.php new file mode 100644 index 00000000..e0c2285c --- /dev/null +++ b/tests/types/utils-types.php @@ -0,0 +1,105 @@ + $hash */ +function testArrayHash(ArrayHash $hash): void +{ + foreach ($hash as $key => $value) { + assertType('(int|string)', $key); + assertType('mixed', $value); + } + + assertType('mixed', $hash['key']); +} + + +function testHtml(Html $html): void +{ + foreach ($html as $key => $child) { + assertType('int', $key); + assertType('Nette\Utils\Html|string', $child); + } + + assertType('Nette\Utils\Html|string', $html[0]); +} + + +function testArraysSome(): void +{ + $result = Arrays::some([1, 2, 3], function ($value, $key) { + assertType('1|2|3', $value); + assertType('0|1|2', $key); + return $value > 2; + }); + assertType('bool', $result); +} + + +function testArraysEvery(): void +{ + $result = Arrays::every([1, 2, 3], function ($value, $key) { + assertType('1|2|3', $value); + assertType('0|1|2', $key); + return $value > 0; + }); + assertType('bool', $result); +} + + +function testArraysMap(): void +{ + $result = Arrays::map([1, 2, 3], function ($value) { + assertType('1|2|3', $value); + return $value * 2; + }); + assertType('array<0|1|2, float|int>', $result); +} + + +function testStringsSplit(): void +{ + $withoutOffset = Strings::split('a,b', '#(,)#'); + assertType('list', $withoutOffset); + + $withOffset = Strings::split('a,b', '#(,)#', captureOffset: true); + assertType('list', $withOffset); +} + + +function testStringsMatch(): void +{ + $withoutOffset = Strings::match('hello', '#l+#'); + assertType('array|null', $withoutOffset); + + $withOffset = Strings::match('hello', '#l+#', captureOffset: true); + assertType('array|null', $withOffset); +} + + +function testStringsMatchAll(): void +{ + $withoutOffset = Strings::matchAll('hello', '#l+#'); + assertType('list>', $withoutOffset); + + $withOffset = Strings::matchAll('hello', '#l+#', captureOffset: true); + assertType('list>', $withOffset); + + $lazy = Strings::matchAll('hello', '#l+#', lazy: true); + assertType('Generator, mixed, mixed>', $lazy); + + $lazyWithOffset = Strings::matchAll('hello', '#l+#', captureOffset: true, lazy: true); + assertType('Generator, mixed, mixed>', $lazyWithOffset); +} From 43af27ad16d8136f1d034aea695966508f2f3019 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 23 Jan 2026 00:10:07 +0100 Subject: [PATCH 6/8] fixed PHPStan errors --- composer.json | 2 +- phpstan.neon | 104 +++++++++++++++++++++++++++++++++++++++--- src/Utils/Finder.php | 2 +- src/Utils/Strings.php | 3 ++ 4 files changed, 102 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 6f6db4a4..8076d742 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "require-dev": { "nette/tester": "^2.5", "tracy/tracy": "^2.9", - "phpstan/phpstan-nette": "^2.0@stable", + "phpstan/phpstan": "^2.0@stable", "jetbrains/phpstorm-attributes": "^1.2" }, "conflict": { diff --git a/phpstan.neon b/phpstan.neon index f0d4d641..a484c4ed 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 5 + level: 6 paths: - src @@ -8,12 +8,102 @@ parameters: bootstrapFiles: - tests/phpstan-bootstrap.php + excludePaths: + - src/compatibility.php + - src/Iterators/Mapper.php + - src/Utils/ObjectHelpers.php + ignoreErrors: - # PHPStan does not support dynamic by reference return used by Nette\Utils\Strings::pcre() - - '#Undefined variable: \$m#' + # Intentional design pattern: new static() for inheritance support in fluent interfaces + - + identifier: new.static + paths: + - src/Utils/ArrayHash.php + - src/Utils/ArrayList.php + - src/Utils/DateTime.php + - src/Utils/Finder.php + - src/Utils/Html.php + - src/Utils/Image.php + + # Runtime validation: ArrayAccess methods receive mixed types, validation is necessary + - + identifier: function.alreadyNarrowedType + paths: + - src/Utils/ArrayHash.php + - src/Utils/ArrayList.php + + # Runtime validation: isList check validates input at runtime + - + identifier: staticMethod.alreadyNarrowedType + path: src/Utils/ArrayList.php + + # Runtime validation: is_callable check validates callback at runtime + - + identifier: function.alreadyNarrowedType + path: src/Utils/Strings.php + + # Intentional pattern: using && for short-circuit evaluation with side effects + - + identifier: booleanAnd.leftAlwaysTrue + path: src/Utils/DateTime.php + reportUnmatched: false + + # Intentional pattern: assignment in condition with && operator + - + identifier: booleanAnd.rightAlwaysTrue + path: src/Utils/Reflection.php + + # PHP 8.3+: getBytesFromString() doesn't exist on PHP 8.2 + - + identifier: method.notFound + path: src/Utils/Random.php + reportUnmatched: false + + # Intentional pattern: ??= for caching filter results, variable is declared via reference + - + identifier: nullCoalesce.variable + path: src/Utils/Finder.php + + # Type test files: assertType() is the side effect, comparison warnings are expected + - + identifier: greater.alwaysTrue + path: tests/types/utils-types.php + + # Image.php: Callback signature intentionally simplified (doesn't use int parameter) + - + identifier: argument.type + path: src/Utils/Image.php + count: 1 + + # Image.php: Defensive validation even though phpDoc specifies positive-int + - + identifiers: + - smaller.alwaysFalse + - booleanOr.alwaysFalse + path: src/Utils/Image.php + + # Image.php: isset() check for type safety even though offset always exists + - + identifier: isset.offset + path: src/Utils/Image.php + + # Image.php: Match arms document all supported types + - + identifier: match.alwaysTrue + path: src/Utils/Image.php + + # Image.php: Return type annotation conveys intent (ImageType constants are ints) + - + identifier: return.type + path: src/Utils/Image.php + count: 1 - # PHPStan does not support RecursiveIteratorIterator proxying unknown method calls to inner iterator - - '#RecursiveIteratorIterator::getSubPathName\(\)#' + # Image.php: By-ref parameter type narrowing in isPercent() + - + identifier: parameterByRef.type + path: src/Utils/Image.php - # static cannot be changed to maintain backward compatibility - - '#Unsafe usage of new static\(\)#' + # Iterables.php: Anonymous classes can't properly resolve template types from enclosing method + - + identifier: argument.templateType + path: src/Utils/Iterables.php diff --git a/src/Utils/Finder.php b/src/Utils/Finder.php index 365d95c0..1ca4ffb7 100644 --- a/src/Utils/Finder.php +++ b/src/Utils/Finder.php @@ -288,7 +288,7 @@ public function size(string $operator, ?int $size = null): static [, $operator, $size, $unit] = $matches; $units = ['' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9]; - $size *= $units[strtolower($unit)]; + $size = (float) $size * $units[strtolower($unit)]; $operator = $operator ?: '='; } diff --git a/src/Utils/Strings.php b/src/Utils/Strings.php index 2120e4c5..e5990a70 100644 --- a/src/Utils/Strings.php +++ b/src/Utils/Strings.php @@ -547,6 +547,7 @@ public static function match( $pattern .= 'u'; } + $m = []; if ($offset > strlen($subject)) { return null; } elseif (!self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset])) { @@ -588,6 +589,7 @@ public static function matchAll( $flags = PREG_OFFSET_CAPTURE | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0); return (function () use ($utf8, $captureOffset, $flags, $subject, $pattern, $offset) { $counter = 0; + $m = null; while ( $offset <= strlen($subject) - ($counter ? 1 : 0) && self::pcre('preg_match', [$pattern, $subject, &$m, $flags, $offset]) @@ -611,6 +613,7 @@ public static function matchAll( ? $captureOffset : ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0) | ($patternOrder ? PREG_PATTERN_ORDER : 0); + $m = []; self::pcre('preg_match_all', [ $pattern, $subject, &$m, ($flags & PREG_PATTERN_ORDER) ? $flags : ($flags | PREG_SET_ORDER), From f76b5dc3d6c6d3043c8d937df2698515b99cbaf5 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 23 Jan 2026 00:20:32 +0100 Subject: [PATCH 7/8] made static analysis mandatory --- .github/workflows/static-analysis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index be22f147..08bd0356 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -1,4 +1,4 @@ -name: Static Analysis (only informative) +name: Static Analysis on: [push, pull_request] @@ -15,4 +15,3 @@ jobs: - run: composer install --no-progress --prefer-dist - run: composer phpstan -- --no-progress - continue-on-error: true # is only informative From 187bd3a31b4bff05de3684217af8ca9131a2140f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0pa=C4=8Dek?= Date: Fri, 13 Feb 2026 00:52:50 +0100 Subject: [PATCH 8/8] Document Arrays::invoke() callbacks params --- src/Utils/Arrays.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Utils/Arrays.php b/src/Utils/Arrays.php index fe72796a..8d5958ba 100644 --- a/src/Utils/Arrays.php +++ b/src/Utils/Arrays.php @@ -496,7 +496,7 @@ public static function mapWithKeys(array $array, callable $transformer): array /** * Invokes all callbacks and returns array of results. - * @param iterable $callbacks + * @param iterable $callbacks * @param mixed ...$args * @return array */