diff --git a/src/player/Player.php b/src/player/Player.php index 14fa687a4..97383d14d 100644 --- a/src/player/Player.php +++ b/src/player/Player.php @@ -691,8 +691,8 @@ class Player extends Human implements CommandSender, ChunkLoader, ChunkListener, /** * Resets the player's cooldown time for the given item back to the maximum. */ - public function resetItemCooldown(Item $item) : void{ - $ticks = $item->getCooldownTicks(); + public function resetItemCooldown(Item $item, ?int $ticks = null) : void{ + $ticks = $ticks ?? $item->getCooldownTicks(); if($ticks > 0){ $this->usedItemsCooldown[$item->getId()] = $this->server->getTick() + $ticks; } diff --git a/src/plugin/DiskResourceProvider.php b/src/plugin/DiskResourceProvider.php index 31016110f..9a748aefb 100644 --- a/src/plugin/DiskResourceProvider.php +++ b/src/plugin/DiskResourceProvider.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\plugin; +use pocketmine\utils\AssumptionFailedError; use function file_exists; use function fopen; use function is_dir; @@ -53,8 +54,10 @@ class DiskResourceProvider implements ResourceProvider{ */ public function getResource(string $filename){ $filename = rtrim(str_replace("\\", "/", $filename), "/"); - if(file_exists($this->file . "/" . $filename)){ - return fopen($this->file . "/" . $filename, "rb"); + if(file_exists($this->file . "resources/" . $filename)){ + $resource = fopen($this->file . "resources/" . $filename, "rb"); + if($resource === false) throw new AssumptionFailedError("fopen() should not fail on a file which exists"); + return $resource; } return null; diff --git a/src/plugin/PluginBase.php b/src/plugin/PluginBase.php index e4f25612c..63ddd10a7 100644 --- a/src/plugin/PluginBase.php +++ b/src/plugin/PluginBase.php @@ -30,6 +30,7 @@ use pocketmine\command\PluginCommand; use pocketmine\command\PluginIdentifiableCommand; use pocketmine\scheduler\TaskScheduler; use pocketmine\Server; +use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Config; use function count; use function dirname; @@ -275,7 +276,10 @@ abstract class PluginBase implements Plugin, CommandExecutor{ return false; } - $ret = stream_copy_to_stream($resource, $fp = fopen($out, "wb")) > 0; + $fp = fopen($out, "wb"); + if($fp === false) throw new AssumptionFailedError("fopen() should not fail with wb flags"); + + $ret = stream_copy_to_stream($resource, $fp) > 0; fclose($fp); fclose($resource); return $ret; diff --git a/src/plugin/PluginDescription.php b/src/plugin/PluginDescription.php index dd7a06835..cc57fbbb1 100644 --- a/src/plugin/PluginDescription.php +++ b/src/plugin/PluginDescription.php @@ -27,7 +27,6 @@ use pocketmine\permission\Permission; use pocketmine\permission\PermissionParser; use function array_map; use function array_values; -use function extension_loaded; use function is_array; use function phpversion; use function preg_match; @@ -229,11 +228,11 @@ class PluginDescription{ */ public function checkRequiredExtensions() : void{ foreach($this->extensions as $name => $versionConstrs){ - if(!extension_loaded($name)){ + $gotVersion = phpversion($name); + if($gotVersion === false){ throw new PluginException("Required extension $name not loaded"); } - $gotVersion = phpversion($name); foreach($versionConstrs as $constr){ // versionConstrs_loop if($constr === "*"){ continue; diff --git a/src/plugin/PluginManager.php b/src/plugin/PluginManager.php index ced8032b6..1d939bc3d 100644 --- a/src/plugin/PluginManager.php +++ b/src/plugin/PluginManager.php @@ -34,6 +34,7 @@ use pocketmine\network\mcpe\protocol\ProtocolInfo; use pocketmine\permission\PermissionManager; use pocketmine\Server; use pocketmine\timings\TimingsHandler; +use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Utils; use function array_intersect; use function array_map; @@ -208,6 +209,7 @@ class PluginManager{ shuffle($files); //this prevents plugins implicitly relying on the filesystem name order when they should be using dependency properties foreach($loaders as $loader){ foreach($files as $file){ + if(!is_string($file)) throw new AssumptionFailedError("FilesystemIterator current should be string when using CURRENT_AS_PATHNAME"); if(!$loader->canLoadPlugin($file)){ continue; } diff --git a/src/plugin/ScriptPluginLoader.php b/src/plugin/ScriptPluginLoader.php index 126f66de2..ea9d4c22d 100644 --- a/src/plugin/ScriptPluginLoader.php +++ b/src/plugin/ScriptPluginLoader.php @@ -55,7 +55,10 @@ class ScriptPluginLoader implements PluginLoader{ * Gets the PluginDescription from the file */ public function getPluginDescription(string $file) : ?PluginDescription{ - $content = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + $content = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if($content === false){ + return null; + } $data = []; diff --git a/src/scheduler/AsyncPool.php b/src/scheduler/AsyncPool.php index a33740bb3..1eedb3e2e 100644 --- a/src/scheduler/AsyncPool.php +++ b/src/scheduler/AsyncPool.php @@ -52,9 +52,15 @@ class AsyncPool{ /** @var \SplQueue[]|AsyncTask[][] */ private $taskQueues = []; - /** @var AsyncWorker[] */ + /** + * @var AsyncWorker[] + * @phpstan-var array + */ private $workers = []; - /** @var int[] */ + /** + * @var int[] + * @phpstan-var array + */ private $workerLastUsed = []; /** diff --git a/src/scheduler/AsyncTask.php b/src/scheduler/AsyncTask.php index c7d6fbb0d..89a410539 100644 --- a/src/scheduler/AsyncTask.php +++ b/src/scheduler/AsyncTask.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\scheduler; +use pocketmine\utils\AssumptionFailedError; use function is_scalar; use function serialize; use function spl_object_id; @@ -105,7 +106,11 @@ abstract class AsyncTask extends \Threaded{ * @return mixed */ public function getResult(){ - return $this->serialized ? unserialize($this->result) : $this->result; + if($this->serialized){ + if(!is_string($this->result)) throw new AssumptionFailedError("Result expected to be a serialized string"); + return unserialize($this->result); + } + return $this->result; } public function cancelRun() : void{ diff --git a/src/scheduler/SendUsageTask.php b/src/scheduler/SendUsageTask.php index c78d8b116..de1a6a0e2 100644 --- a/src/scheduler/SendUsageTask.php +++ b/src/scheduler/SendUsageTask.php @@ -26,6 +26,7 @@ namespace pocketmine\scheduler; use pocketmine\network\mcpe\protocol\ProtocolInfo; use pocketmine\player\Player; use pocketmine\Server; +use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Internet; use pocketmine\utils\Process; use pocketmine\utils\Utils; @@ -149,7 +150,9 @@ class SendUsageTask extends AsyncTask{ } $this->endpoint = $endpoint . "api/post"; - $this->data = json_encode($data/*, JSON_PRETTY_PRINT*/); + $data = json_encode($data/*, JSON_PRETTY_PRINT*/); + if($data === false) throw new AssumptionFailedError("Statistics JSON should never fail to encode: " . json_last_error_msg()); + $this->data = $data; } public function onRun() : void{ diff --git a/src/utils/Config.php b/src/utils/Config.php index dbaf18291..dfd41bda9 100644 --- a/src/utils/Config.php +++ b/src/utils/Config.php @@ -166,6 +166,9 @@ class Config{ $this->save(); }else{ $content = file_get_contents($this->file); + if($content === false){ + throw new \RuntimeException("Unable to load config file"); + } $config = null; switch($this->type){ case Config::PROPERTIES: @@ -539,7 +542,6 @@ class Config{ /** * @return mixed[] - * @phpstan-return array */ private function parseProperties(string $content) : array{ $result = []; diff --git a/src/utils/Filesystem.php b/src/utils/Filesystem.php index 0edc258c0..ab60becaa 100644 --- a/src/utils/Filesystem.php +++ b/src/utils/Filesystem.php @@ -58,6 +58,7 @@ final class Filesystem{ public static function recursiveUnlink(string $dir) : void{ if(is_dir($dir)){ $objects = scandir($dir, SCANDIR_SORT_NONE); + if($objects === false) throw new AssumptionFailedError("scandir() shouldn't return false when is_dir() returns true"); foreach($objects as $object){ if($object !== "." and $object !== ".."){ if(is_dir($dir . "/" . $object)){ diff --git a/src/utils/Internet.php b/src/utils/Internet.php index 8c2c21f5b..ccdfad21f 100644 --- a/src/utils/Internet.php +++ b/src/utils/Internet.php @@ -72,7 +72,7 @@ class Internet{ * * @param bool $force default false, force IP check even when cached * - * @return string|bool + * @return string|false */ public static function getIP(bool $force = false){ if(!self::$online){ @@ -116,7 +116,10 @@ class Internet{ * @throws InternetException */ public static function getInternalIP() : string{ - $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + $sock = @socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + if($sock === false){ + throw new InternetException("Failed to get internal IP: " . trim(socket_strerror(socket_last_error()))); + } try{ if(!@socket_connect($sock, "8.8.8.8", 65534)){ throw new InternetException("Failed to get internal IP: " . trim(socket_strerror(socket_last_error($sock)))); @@ -205,6 +208,9 @@ class Internet{ } $ch = curl_init($page); + if($ch === false){ + throw new InternetException("Unable to create new cURL session"); + } curl_setopt_array($ch, $extraOpts + [ CURLOPT_SSL_VERIFYPEER => false, @@ -221,10 +227,10 @@ class Internet{ ]); try{ $raw = curl_exec($ch); - $error = curl_error($ch); - if($error !== ""){ - throw new InternetException($error); + if($raw === false){ + throw new InternetException(curl_error($ch)); } + if(!is_string($raw)) throw new AssumptionFailedError("curl_exec() should return string|false when CURLOPT_RETURNTRANSFER is set"); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $rawHeaders = substr($raw, 0, $headerSize); diff --git a/src/utils/Process.php b/src/utils/Process.php index 1d26bee64..77e2c320e 100644 --- a/src/utils/Process.php +++ b/src/utils/Process.php @@ -55,13 +55,16 @@ final class Process{ $VmSize = null; $VmRSS = null; if(Utils::getOS() === "linux" or Utils::getOS() === "android"){ - $status = file_get_contents("/proc/self/status"); + $status = @file_get_contents("/proc/self/status"); + if($status === false) throw new AssumptionFailedError("/proc/self/status should always be accessible"); + + // the numbers found here should never be bigger than PHP_INT_MAX, so we expect them to always be castable to int if(preg_match("/VmRSS:[ \t]+([0-9]+) kB/", $status, $matches) > 0){ - $VmRSS = $matches[1] * 1024; + $VmRSS = ((int) $matches[1]) * 1024; } if(preg_match("/VmSize:[ \t]+([0-9]+) kB/", $status, $matches) > 0){ - $VmSize = $matches[1] * 1024; + $VmSize = ((int) $matches[1]) * 1024; } } @@ -90,13 +93,14 @@ final class Process{ $heap = 0; if(Utils::getOS() === "linux" or Utils::getOS() === "android"){ - $mappings = file("/proc/self/maps"); + $mappings = @file("/proc/self/maps"); + if($mappings === false) throw new AssumptionFailedError("/proc/self/maps should always be accessible"); foreach($mappings as $line){ if(preg_match("#([a-z0-9]+)\\-([a-z0-9]+) [rwxp\\-]{4} [a-z0-9]+ [^\\[]*\\[([a-zA-z0-9]+)\\]#", trim($line), $matches) > 0){ if(strpos($matches[3], "heap") === 0){ - $heap += hexdec($matches[2]) - hexdec($matches[1]); + $heap += (int) hexdec($matches[2]) - (int) hexdec($matches[1]); }elseif(strpos($matches[3], "stack") === 0){ - $stack += hexdec($matches[2]) - hexdec($matches[1]); + $stack += (int) hexdec($matches[2]) - (int) hexdec($matches[1]); } } } @@ -107,7 +111,9 @@ final class Process{ public static function getThreadCount() : int{ if(Utils::getOS() === "linux" or Utils::getOS() === "android"){ - if(preg_match("/Threads:[ \t]+([0-9]+)/", file_get_contents("/proc/self/status"), $matches) > 0){ + $status = @file_get_contents("/proc/self/status"); + if($status === false) throw new AssumptionFailedError("/proc/self/status should always be accessible"); + if(preg_match("/Threads:[ \t]+([0-9]+)/", $status, $matches) > 0){ return (int) $matches[1]; } } diff --git a/src/utils/Terminal.php b/src/utils/Terminal.php index 6b0b2509f..08bd2caff 100644 --- a/src/utils/Terminal.php +++ b/src/utils/Terminal.php @@ -91,6 +91,7 @@ abstract class Terminal{ private static function detectFormattingCodesSupport() : bool{ $stdout = fopen("php://stdout", "w"); + if($stdout === false) throw new AssumptionFailedError("Opening php://stdout should never fail"); $result = ( stream_isatty($stdout) and //STDOUT isn't being piped ( diff --git a/src/utils/TextFormat.php b/src/utils/TextFormat.php index d02973c9b..6d106db1c 100644 --- a/src/utils/TextFormat.php +++ b/src/utils/TextFormat.php @@ -66,13 +66,19 @@ abstract class TextFormat{ public const ITALIC = TextFormat::ESCAPE . "o"; public const RESET = TextFormat::ESCAPE . "r"; + private static function makePcreError(string $info) : \InvalidArgumentException{ + throw new \InvalidArgumentException("$info: Encountered PCRE error " . preg_last_error() . " during regex operation"); + } + /** * Splits the string by Format tokens * * @return string[] */ public static function tokenize(string $string) : array{ - return preg_split("/(" . TextFormat::ESCAPE . "[0-9a-fk-or])/u", $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + $result = preg_split("/(" . TextFormat::ESCAPE . "[0-9a-fk-or])/u", $string, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + if($result === false) throw self::makePcreError("Failed to tokenize string"); + return $result; } /** @@ -83,6 +89,7 @@ abstract class TextFormat{ public static function clean(string $string, bool $removeFormat = true) : string{ $string = mb_scrub($string, 'UTF-8'); $string = preg_replace("/[\x{E000}-\x{F8FF}]/u", "", $string); //remove unicode private-use-area characters (they might break the console) + if($string === null) throw self::makePcreError("Failed to strip private-area characters"); if($removeFormat){ $string = str_replace(TextFormat::ESCAPE, "", preg_replace("/" . TextFormat::ESCAPE . "[0-9a-fk-or]/u", "", $string)); } @@ -281,7 +288,11 @@ abstract class TextFormat{ } } - return json_encode($newString, JSON_UNESCAPED_SLASHES); + $result = json_encode($newString, JSON_UNESCAPED_SLASHES); + if($result === false){ + throw new \InvalidArgumentException("Failed to encode result JSON: " . json_last_error_msg()); + } + return $result; } /** diff --git a/src/utils/Timezone.php b/src/utils/Timezone.php index 8158956c1..4e6031c0a 100644 --- a/src/utils/Timezone.php +++ b/src/utils/Timezone.php @@ -48,7 +48,11 @@ use function trim; abstract class Timezone{ public static function get() : string{ - return ini_get('date.timezone'); + $tz = ini_get('date.timezone'); + if($tz === false){ + throw new AssumptionFailedError('date.timezone INI entry should always exist'); + } + return $tz; } public static function init() : void{ @@ -138,19 +142,16 @@ abstract class Timezone{ return self::parseOffset($offset); case 'linux': // Ubuntu / Debian. - if(file_exists('/etc/timezone')){ - $data = file_get_contents('/etc/timezone'); - if($data){ - return trim($data); - } + $data = @file_get_contents('/etc/timezone'); + if($data !== false){ + return trim($data); } + // RHEL / CentOS - if(file_exists('/etc/sysconfig/clock')){ - $data = parse_ini_file('/etc/sysconfig/clock'); - if(isset($data['ZONE']) and is_string($data['ZONE'])){ - return trim($data['ZONE']); - } + $data = @parse_ini_file('/etc/sysconfig/clock'); + if($data !== false and isset($data['ZONE']) and is_string($data['ZONE'])){ + return trim($data['ZONE']); } //Portable method for incompatible linux distributions. @@ -163,12 +164,10 @@ abstract class Timezone{ return self::parseOffset($offset); case 'mac': - if(is_link('/etc/localtime')){ - $filename = readlink('/etc/localtime'); - if(strpos($filename, '/usr/share/zoneinfo/') === 0){ - $timezone = substr($filename, 20); - return trim($timezone); - } + $filename = @readlink('/etc/localtime'); + if($filename !== false and strpos($filename, '/usr/share/zoneinfo/') === 0){ + $timezone = substr($filename, 20); + return trim($timezone); } return false; @@ -180,7 +179,7 @@ abstract class Timezone{ /** * @param string $offset In the format of +09:00, +02:00, -04:00 etc. * - * @return string|bool + * @return string|false */ private static function parseOffset($offset){ //Make signed offsets unsigned for date_parse diff --git a/src/utils/UUID.php b/src/utils/UUID.php index 90ce95154..05e0715e4 100644 --- a/src/utils/UUID.php +++ b/src/utils/UUID.php @@ -61,7 +61,12 @@ final class UUID{ * Creates an UUID from an hexadecimal representation */ public static function fromString(string $uuid, ?int $version = null) : UUID{ - return self::fromBinary(hex2bin(str_replace("-", "", trim($uuid))), $version); + //TODO: should we be stricter about the notation (8-4-4-4-12)? + $binary = @hex2bin(str_replace("-", "", trim($uuid))); + if($binary === false){ + throw new \InvalidArgumentException("Invalid hex string UUID representation"); + } + return self::fromBinary($binary, $version); } /** diff --git a/src/utils/Utils.php b/src/utils/Utils.php index 48f20e1af..6b2217555 100644 --- a/src/utils/Utils.php +++ b/src/utils/Utils.php @@ -114,7 +114,12 @@ class Utils{ //non-class function return $func->getName(); } - return "closure@" . Filesystem::cleanPath($func->getFileName()) . "#L" . $func->getStartLine(); + $filename = $func->getFileName(); + + return "closure@" . ($filename !== false ? + Filesystem::cleanPath($filename) . "#L" . $func->getStartLine() : + "internal" + ); } /** @@ -125,7 +130,12 @@ class Utils{ public static function getNiceClassName(object $obj) : string{ $reflect = new \ReflectionClass($obj); if($reflect->isAnonymous()){ - return "anonymous@" . Filesystem::cleanPath($reflect->getFileName()) . "#L" . $reflect->getStartLine(); + $filename = $reflect->getFileName(); + + return "anonymous@" . ($filename !== false ? + Filesystem::cleanPath($filename) . "#L" . $reflect->getStartLine() : + "internal" + ); } return $reflect->getName(); @@ -169,14 +179,14 @@ class Utils{ } $machine = php_uname("a"); - $machine .= file_exists("/proc/cpuinfo") ? implode(preg_grep("/(model name|Processor|Serial)/", file("/proc/cpuinfo"))) : ""; + $machine .= ($cpuinfo = @file("/proc/cpuinfo")) !== false ? implode(preg_grep("/(model name|Processor|Serial)/", $cpuinfo)) : ""; $machine .= sys_get_temp_dir(); $machine .= $extra; $os = Utils::getOS(); if($os === "win"){ @exec("ipconfig /ALL", $mac); $mac = implode("\n", $mac); - if(preg_match_all("#Physical Address[. ]{1,}: ([0-9A-F\\-]{17})#", $mac, $matches)){ + if(preg_match_all("#Physical Address[. ]{1,}: ([0-9A-F\\-]{17})#", $mac, $matches) > 0){ foreach($matches[1] as $i => $v){ if($v == "00-00-00-00-00-00"){ unset($matches[1][$i]); @@ -190,7 +200,7 @@ class Utils{ }else{ @exec("ifconfig 2>/dev/null", $mac); $mac = implode("\n", $mac); - if(preg_match_all("#HWaddr[ \t]{1,}([0-9a-f:]{17})#", $mac, $matches)){ + if(preg_match_all("#HWaddr[ \t]{1,}([0-9a-f:]{17})#", $mac, $matches) > 0){ foreach($matches[1] as $i => $v){ if($v == "00:00:00:00:00:00"){ unset($matches[1][$i]); @@ -270,14 +280,14 @@ class Utils{ switch(Utils::getOS()){ case "linux": case "android": - if(file_exists("/proc/cpuinfo")){ - foreach(file("/proc/cpuinfo") as $l){ + if(($cpuinfo = @file('/proc/cpuinfo')) !== false){ + foreach($cpuinfo as $l){ if(preg_match('/^processor[ \t]*:[ \t]*[0-9]+$/m', $l) > 0){ ++$processors; } } - }elseif(is_readable("/sys/devices/system/cpu/present")){ - if(preg_match("/^([0-9]+)\\-([0-9]+)$/", trim(file_get_contents("/sys/devices/system/cpu/present")), $matches) > 0){ + }elseif(($cpuPresent = @file_get_contents("/sys/devices/system/cpu/present")) !== false){ + if(preg_match("/^([0-9]+)\\-([0-9]+)$/", trim($cpuPresent), $matches) > 0){ $processors = (int) ($matches[2] - $matches[1]); } } @@ -382,7 +392,9 @@ class Utils{ public static function getReferenceCount($value, bool $includeCurrent = true) : int{ ob_start(); debug_zval_dump($value); - $ret = explode("\n", ob_get_contents()); + $contents = ob_get_contents(); + if($contents === false) throw new AssumptionFailedError("ob_get_contents() should never return false here"); + $ret = explode("\n", $contents); ob_end_clean(); if(count($ret) >= 1 and preg_match('/^.* refcount\\(([0-9]+)\\)\\{$/', trim($ret[0]), $m) > 0){ @@ -462,6 +474,10 @@ class Utils{ return array_combine($matches[1], $matches[2]); } + /** + * @phpstan-param class-string $className + * @phpstan-param class-string $baseName + */ public static function testValidInstance(string $className, string $baseName) : void{ try{ $base = new \ReflectionClass($baseName); diff --git a/src/world/format/io/region/RegionLoader.php b/src/world/format/io/region/RegionLoader.php index 974470571..19bd04819 100644 --- a/src/world/format/io/region/RegionLoader.php +++ b/src/world/format/io/region/RegionLoader.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\world\format\io\region; +use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Binary; use pocketmine\world\format\ChunkException; use pocketmine\world\format\io\exception\CorruptedChunkException; @@ -88,7 +89,9 @@ class RegionLoader{ throw new CorruptedRegionException("Region file should be padded to a multiple of 4KiB"); } - $this->filePointer = fopen($this->filePath, "r+b"); + $filePointer = fopen($this->filePath, "r+b"); + if($filePointer === false) throw new AssumptionFailedError("fopen() should not fail here"); + $this->filePointer = $filePointer; stream_set_read_buffer($this->filePointer, 1024 * 16); //16KB stream_set_write_buffer($this->filePointer, 1024 * 16); //16KB if(!$exists){ diff --git a/tests/phpstan/configs/phpstan-bugs.neon b/tests/phpstan/configs/phpstan-bugs.neon index 3b3f2db62..c070f2bfa 100644 --- a/tests/phpstan/configs/phpstan-bugs.neon +++ b/tests/phpstan/configs/phpstan-bugs.neon @@ -82,6 +82,12 @@ parameters: count: 1 path: ../../../src/network/mcpe/protocol/types/entity/EntityMetadataCollection.php + - + #readlink() can return false but phpstan doesn't know this + message: "#^Strict comparison using \\!\\=\\= between string and false will always evaluate to true\\.$#" + count: 1 + path: ../../../src/pocketmine/utils/Timezone.php + - #phpstan doesn't understand that SplFixedArray may contain null message: "#^Call to static method PHPUnit\\\\Framework\\\\Assert\\:\\:assertNotNull\\(\\) with int and string will always evaluate to true\\.$#"