From d42d358ba239add71e3cbff9e95ef6af7cb14d8e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Petr=20Mor=C3=A1vek?= <petr@pada.cz>
Date: Tue, 28 Mar 2023 17:54:07 +0200
Subject: [PATCH 1/4] improved type annotations (#290)

---
 src/Utils/ArrayHash.php  | 10 ++++++----
 src/Utils/ArrayList.php  |  4 +++-
 src/Utils/Image.php      | 10 +++++++---
 src/Utils/Paginator.php  | 25 +++++++++++++++++--------
 src/Utils/Reflection.php |  2 +-
 src/Utils/Strings.php    |  2 ++
 src/Utils/Validators.php |  1 +
 7 files changed, 37 insertions(+), 17 deletions(-)

diff --git a/src/Utils/ArrayHash.php b/src/Utils/ArrayHash.php
index 46cad2ba0..95bf73cef 100644
--- a/src/Utils/ArrayHash.php
+++ b/src/Utils/ArrayHash.php
@@ -15,6 +15,8 @@
 /**
  * Provides objects to work as array.
  * @template T
+ * @implements \RecursiveArrayIterator<array-key, T>
+ * @implements \ArrayAccess<array-key, T>
  */
 class ArrayHash extends \stdClass implements \ArrayAccess, \Countable, \IteratorAggregate
 {
@@ -57,7 +59,7 @@ public function count(): int
 
 	/**
 	 * Replaces or appends a item.
-	 * @param  string|int  $key
+	 * @param  array-key  $key
 	 * @param  T  $value
 	 */
 	public function offsetSet($key, $value): void
@@ -72,7 +74,7 @@ public function offsetSet($key, $value): void
 
 	/**
 	 * Returns a item.
-	 * @param  string|int  $key
+	 * @param  array-key  $key
 	 * @return T
 	 */
 	#[\ReturnTypeWillChange]
@@ -84,7 +86,7 @@ public function offsetGet($key)
 
 	/**
 	 * Determines whether a item exists.
-	 * @param  string|int  $key
+	 * @param  array-key  $key
 	 */
 	public function offsetExists($key): bool
 	{
@@ -94,7 +96,7 @@ public function offsetExists($key): bool
 
 	/**
 	 * Removes the element from this list.
-	 * @param  string|int  $key
+	 * @param  array-key  $key
 	 */
 	public function offsetUnset($key): void
 	{
diff --git a/src/Utils/ArrayList.php b/src/Utils/ArrayList.php
index de9220922..1c78ef74f 100644
--- a/src/Utils/ArrayList.php
+++ b/src/Utils/ArrayList.php
@@ -15,6 +15,8 @@
 /**
  * Provides the base class for a generic list (items can be accessed by index).
  * @template T
+ * @implements \IteratorAggregate<int, T>
+ * @implements \ArrayAccess<int, T>
  */
 class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
 {
@@ -26,7 +28,7 @@ class ArrayList implements \ArrayAccess, \Countable, \IteratorAggregate
 
 	/**
 	 * Transforms array to ArrayList.
-	 * @param  array<T>  $array
+	 * @param  list<T>  $array
 	 * @return static
 	 */
 	public static function from(array $array)
diff --git a/src/Utils/Image.php b/src/Utils/Image.php
index e2f714de5..b354c336b 100644
--- a/src/Utils/Image.php
+++ b/src/Utils/Image.php
@@ -90,8 +90,8 @@
  * @method void stringUp($font, $x, $y, string $s, $col)
  * @method void trueColorToPalette(bool $dither, $ncolors)
  * @method array ttfText($size, $angle, $x, $y, $color, string $fontfile, string $text)
- * @property-read int $width
- * @property-read int $height
+ * @property-read positive-int $width
+ * @property-read positive-int $height
  * @property-read resource|\GdImage $imageResource
  */
 class Image
@@ -205,6 +205,8 @@ 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
 	 * @return static
 	 * @throws Nette\NotSupportedException if gd extension is not loaded
 	 */
@@ -301,6 +303,7 @@ public function __construct($image)
 
 	/**
 	 * Returns image width.
+	 * @return positive-int
 	 */
 	public function getWidth(): int
 	{
@@ -310,6 +313,7 @@ public function getWidth(): int
 
 	/**
 	 * Returns image height.
+	 * @return positive-int
 	 */
 	public function getHeight(): int
 	{
@@ -536,7 +540,7 @@ public function sharpen()
 	 * Puts another image into this image.
 	 * @param  int|string  $left in pixels or percent
 	 * @param  int|string  $top in pixels or percent
-	 * @param  int  $opacity 0..100
+	 * @param  int<0, 100>  $opacity 0..100
 	 * @return static
 	 */
 	public function place(self $image, $left = 0, $top = 0, int $opacity = 100)
diff --git a/src/Utils/Paginator.php b/src/Utils/Paginator.php
index 4517a8cc3..ded2fcccc 100644
--- a/src/Utils/Paginator.php
+++ b/src/Utils/Paginator.php
@@ -18,17 +18,17 @@
  * @property   int $page
  * @property-read int $firstPage
  * @property-read int|null $lastPage
- * @property-read int $firstItemOnPage
- * @property-read int $lastItemOnPage
+ * @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|null $pageCount
- * @property   int $itemsPerPage
- * @property   int|null $itemCount
- * @property-read int $offset
- * @property-read int|null $countdownOffset
- * @property-read int $length
+ * @property-read int<0,max>|null $pageCount
+ * @property   positive-int $itemsPerPage
+ * @property   int<0,max>|null $itemCount
+ * @property-read int<0,max> $offset
+ * @property-read int<0,max>|null $countdownOffset
+ * @property-read int<0,max> $length
  */
 class Paginator
 {
@@ -89,6 +89,7 @@ public function getLastPage(): ?int
 
 	/**
 	 * Returns the sequence number of the first element on the page
+	 * @return int<0, max>
 	 */
 	public function getFirstItemOnPage(): int
 	{
@@ -100,6 +101,7 @@ public function getFirstItemOnPage(): int
 
 	/**
 	 * Returns the sequence number of the last element on the page
+	 * @return int<0, max>
 	 */
 	public function getLastItemOnPage(): int
 	{
@@ -129,6 +131,7 @@ public function getBase(): int
 
 	/**
 	 * Returns zero-based page number.
+	 * @return int<0, max>
 	 */
 	protected function getPageIndex(): int
 	{
@@ -161,6 +164,7 @@ public function isLast(): bool
 
 	/**
 	 * Returns the total number of pages.
+	 * @return int<0, max>|null
 	 */
 	public function getPageCount(): ?int
 	{
@@ -183,6 +187,7 @@ public function setItemsPerPage(int $itemsPerPage)
 
 	/**
 	 * Returns the number of items to display on a single page.
+	 * @return positive-int
 	 */
 	public function getItemsPerPage(): int
 	{
@@ -203,6 +208,7 @@ public function setItemCount(?int $itemCount = null)
 
 	/**
 	 * Returns the total number of items.
+	 * @return int<0, max>|null
 	 */
 	public function getItemCount(): ?int
 	{
@@ -212,6 +218,7 @@ public function getItemCount(): ?int
 
 	/**
 	 * Returns the absolute index of the first item on current page.
+	 * @return int<0, max>
 	 */
 	public function getOffset(): int
 	{
@@ -221,6 +228,7 @@ public function getOffset(): int
 
 	/**
 	 * Returns the absolute index of the first item on current page in countdown paging.
+	 * @return int<0, max>|null
 	 */
 	public function getCountdownOffset(): ?int
 	{
@@ -232,6 +240,7 @@ public function getCountdownOffset(): ?int
 
 	/**
 	 * Returns the number of items on current page.
+	 * @return int<0, max>
 	 */
 	public function getLength(): int
 	{
diff --git a/src/Utils/Reflection.php b/src/Utils/Reflection.php
index dffd87ffa..275775830 100644
--- a/src/Utils/Reflection.php
+++ b/src/Utils/Reflection.php
@@ -283,7 +283,7 @@ public static function expandClassName(string $name, \ReflectionClass $context):
 	}
 
 
-	/** @return array of [alias => class] */
+	/** @return array<string, class-string> of [alias => class] */
 	public static function getUseStatements(\ReflectionClass $class): array
 	{
 		if ($class->isAnonymous()) {
diff --git a/src/Utils/Strings.php b/src/Utils/Strings.php
index 15c7a147f..302b3f0e0 100644
--- a/src/Utils/Strings.php
+++ b/src/Utils/Strings.php
@@ -376,6 +376,7 @@ public static function trim(string $s, string $charlist = self::TRIM_CHARACTERS)
 
 	/**
 	 * Pads a UTF-8 string to given length by prepending the $pad string to the beginning.
+	 * @param  non-empty-string  $pad
 	 */
 	public static function padLeft(string $s, int $length, string $pad = ' '): string
 	{
@@ -387,6 +388,7 @@ public static function padLeft(string $s, int $length, string $pad = ' '): strin
 
 	/**
 	 * Pads UTF-8 string to given length by appending the $pad string to the end.
+	 * @param  non-empty-string  $pad
 	 */
 	public static function padRight(string $s, int $length, string $pad = ' '): string
 	{
diff --git a/src/Utils/Validators.php b/src/Utils/Validators.php
index c71bedc80..a39a8cc52 100644
--- a/src/Utils/Validators.php
+++ b/src/Utils/Validators.php
@@ -284,6 +284,7 @@ public static function isMixed(): bool
 	 * Checks if a variable is a zero-based integer indexed array.
 	 * @param  mixed  $value
 	 * @deprecated  use Nette\Utils\Arrays::isList
+	 * @return ($value is list ? true : false)
 	 */
 	public static function isList($value): bool
 	{

From 07c000693d08bb34b79dfc8f75c02fb888db6546 Mon Sep 17 00:00:00 2001
From: David Grudl <david@grudl.com>
Date: Sun, 30 Jul 2023 14:31:35 +0200
Subject: [PATCH 2/4] Callback::unwrap() returns correct class name for private
 methods

---
 src/Utils/Callback.php            | 5 +++--
 tests/Utils/Callback.closure.phpt | 2 ++
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/src/Utils/Callback.php b/src/Utils/Callback.php
index 04e886a6f..1833fa4a1 100644
--- a/src/Utils/Callback.php
+++ b/src/Utils/Callback.php
@@ -168,13 +168,14 @@ public static function isStatic(callable $callable): bool
 	public static function unwrap(\Closure $closure)
 	{
 		$r = new \ReflectionFunction($closure);
+		$class = $r->getClosureScopeClass();
 		if (substr($r->name, -1) === '}') {
 			return $closure;
 
-		} elseif ($obj = $r->getClosureThis()) {
+		} elseif (($obj = $r->getClosureThis()) && $class && get_class($obj) === $class->name) {
 			return [$obj, $r->name];
 
-		} elseif ($class = $r->getClosureScopeClass()) {
+		} elseif ($class) {
 			return [$class->name, $r->name];
 
 		} else {
diff --git a/tests/Utils/Callback.closure.phpt b/tests/Utils/Callback.closure.phpt
index 1495b970c..937dabcbc 100644
--- a/tests/Utils/Callback.closure.phpt
+++ b/tests/Utils/Callback.closure.phpt
@@ -156,6 +156,8 @@ test('object methods', function () {
 
 	Assert::same('Test::privateFun', getName(Callback::toReflection([$test, 'privateFun'])));
 	Assert::same('Test::privateFun', getName(Callback::toReflection($test->createPrivateClosure())));
+
+	Assert::same(['Test', 'privateFun'], Callback::unwrap((new TestChild)->createPrivateClosure()));
 	Assert::same('Test::privateFun', getName(Callback::toReflection((new TestChild)->createPrivateClosure())));
 
 	Assert::same('Test::privateFun*', $test->createPrivateClosure()->__invoke('*'));

From a4175c62652f2300c8017fb7e640f9ccb11648d2 Mon Sep 17 00:00:00 2001
From: David Grudl <david@grudl.com>
Date: Sun, 30 Jul 2023 17:29:59 +0200
Subject: [PATCH 3/4] support for PHP 8.3

---
 .github/workflows/tests.yml | 2 +-
 composer.json               | 2 +-
 readme.md                   | 2 +-
 src/Utils/Reflection.php    | 5 +++--
 4 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 0cf0e238c..5dd7d837d 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -8,7 +8,7 @@ jobs:
         strategy:
             matrix:
                 os: [ubuntu-latest, windows-latest]
-                php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
+                php: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3']
 
             fail-fast: false
 
diff --git a/composer.json b/composer.json
index ec4efb0e4..7e881052c 100644
--- a/composer.json
+++ b/composer.json
@@ -15,7 +15,7 @@
 		}
 	],
 	"require": {
-		"php": ">=7.2 <8.3"
+		"php": ">=7.2 <8.4"
 	},
 	"require-dev": {
 		"nette/tester": "~2.0",
diff --git a/readme.md b/readme.md
index 430cc7359..5317b8e65 100644
--- a/readme.md
+++ b/readme.md
@@ -39,7 +39,7 @@ The recommended way to install is via Composer:
 composer require nette/utils
 ```
 
-- Nette Utils 3.2 is compatible with PHP 7.2 to 8.2
+- Nette Utils 3.2 is compatible with PHP 7.2 to 8.3
 - Nette Utils 3.1 is compatible with PHP 7.1 to 8.0
 - Nette Utils 3.0 is compatible with PHP 7.1 to 8.0
 - Nette Utils 2.5 is compatible with PHP 5.6 to 8.0
diff --git a/src/Utils/Reflection.php b/src/Utils/Reflection.php
index 275775830..1ee207a4c 100644
--- a/src/Utils/Reflection.php
+++ b/src/Utils/Reflection.php
@@ -316,7 +316,8 @@ private static function parseUseStatements(string $code, ?string $forClass = nul
 			$tokens = [];
 		}
 
-		$namespace = $class = $classLevel = $level = null;
+		$namespace = $class = null;
+		$classLevel = $level = 0;
 		$res = $uses = [];
 
 		$nameTokens = PHP_VERSION_ID < 80000
@@ -387,7 +388,7 @@ private static function parseUseStatements(string $code, ?string $forClass = nul
 
 				case '}':
 					if ($level === $classLevel) {
-						$class = $classLevel = null;
+						$class = $classLevel = 0;
 					}
 
 					$level--;

From b433959cb129b9d4f15d26b9138c7d2b62a3d757 Mon Sep 17 00:00:00 2001
From: David Grudl <david@grudl.com>
Date: Tue, 29 Aug 2023 23:55:41 +0200
Subject: [PATCH 4/4] StaticClass: constructor is private [Closes nette/di#292]

- ReflectionClass::isInstance() returns false
- it is marked as an error in the IDE
---
 src/StaticClass.php          | 6 ++----
 tests/Utils/StaticClass.phpt | 2 +-
 2 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/StaticClass.php b/src/StaticClass.php
index 8fed0ef31..3f48c0c92 100644
--- a/src/StaticClass.php
+++ b/src/StaticClass.php
@@ -16,12 +16,10 @@
 trait StaticClass
 {
 	/**
-	 * @return never
-	 * @throws \Error
+	 * Class is static and cannot be instantiated.
 	 */
-	final public function __construct()
+	private function __construct()
 	{
-		throw new \Error('Class ' . static::class . ' is static and cannot be instantiated.');
 	}
 
 
diff --git a/tests/Utils/StaticClass.phpt b/tests/Utils/StaticClass.phpt
index ef119ee29..bcab6f540 100644
--- a/tests/Utils/StaticClass.phpt
+++ b/tests/Utils/StaticClass.phpt
@@ -22,7 +22,7 @@ class TestClass
 
 Assert::exception(function () {
 	new TestClass;
-}, Error::class, 'Class TestClass is static and cannot be instantiated.');
+}, Error::class, 'Call to private TestClass::__construct() %a%');
 
 Assert::exception(function () {
 	TestClass::methodA();