From 5325ecee373bbba5fb9e4a42ffbe38adb8655e6c Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Mon, 25 Nov 2024 14:30:58 +0000 Subject: [PATCH 1/2] Deal with a whole lot of PHPStan suppressed key casting errors closes #6534 --- build/generate-biome-ids.php | 2 +- ...enerate-pocketmine-yml-property-consts.php | 2 +- build/generate-registry-annotations.php | 4 ++- src/MemoryManager.php | 10 +++---- src/Server.php | 13 +++++++-- src/block/RuntimeBlockStateRegistry.php | 1 + src/block/inventory/EnchantInventory.php | 5 +++- src/block/utils/SignText.php | 7 ++++- src/command/Command.php | 13 +++++++-- src/command/SimpleCommandMap.php | 11 ++++++-- src/crafting/CraftingManager.php | 6 ++-- .../CraftingManagerFromDataHelper.php | 2 +- src/crafting/CraftingRecipe.php | 2 ++ src/crafting/ShapedRecipe.php | 28 ++++++++++++++----- src/crafting/ShapelessRecipe.php | 17 +++++++---- src/crash/CrashDumpData.php | 10 +++++-- src/data/bedrock/ItemTagToIdMap.php | 2 +- src/data/bedrock/LegacyToStringIdMap.php | 3 +- .../item/upgrade/R12ItemIdToBlockIdMap.php | 2 +- src/entity/Entity.php | 5 +++- src/event/HandlerList.php | 5 +++- src/inventory/BaseInventory.php | 7 +++-- src/inventory/CreativeInventory.php | 6 +++- .../transaction/CraftingTransaction.php | 19 +++++++++++-- .../transaction/InventoryTransaction.php | 23 +++++++++------ src/item/ItemEnchantmentHandlingTrait.php | 6 +++- src/item/LegacyStringToItemParser.php | 3 +- src/lang/Language.php | 4 +-- src/lang/Translatable.php | 4 ++- src/network/NetworkSessionManager.php | 10 +++++-- src/network/mcpe/InventoryManager.php | 1 + src/network/mcpe/NetworkSession.php | 2 +- .../mcpe/StandardPacketBroadcaster.php | 1 - src/network/mcpe/cache/ChunkCache.php | 5 +++- .../convert/BlockStateDictionaryEntry.php | 5 +++- .../ItemTypeDictionaryFromDataHelper.php | 3 +- .../mcpe/handler/InGamePacketHandler.php | 2 +- .../mcpe/handler/ItemStackRequestExecutor.php | 3 +- .../mcpe/handler/LoginPacketHandler.php | 2 +- src/permission/BanList.php | 8 ++++-- src/permission/PermissionAttachment.php | 10 +++++-- src/permission/PermissionManager.php | 13 +++++++-- src/plugin/ApiVersion.php | 4 +-- src/plugin/PluginDescription.php | 5 ++-- src/plugin/PluginGraylist.php | 3 +- src/plugin/PluginManager.php | 13 +++++++-- src/promise/Promise.php | 3 +- src/resourcepacks/ResourcePackManager.php | 3 +- src/utils/Utils.php | 15 +++++++++- src/world/BlockTransaction.php | 5 +++- src/world/WorldManager.php | 6 +++- src/world/format/SubChunk.php | 10 ++++--- src/world/format/io/BaseWorldProvider.php | 2 ++ src/world/format/io/ChunkData.php | 15 ++++++++-- .../format/io/region/RegionGarbageMap.php | 10 +++---- src/world/format/io/region/RegionLoader.php | 8 ++++-- .../format/io/region/RegionWorldProvider.php | 5 +++- src/world/generator/PopulationTask.php | 6 ++-- tests/phpstan/configs/phpstan-bugs.neon | 5 ++++ .../rules/UnsafeForeachArrayOfStringRule.php | 26 ++++++++++++++--- tests/phpunit/block/BlockTest.php | 2 +- tests/phpunit/item/ItemTest.php | 1 - .../utils/CloningRegistryTraitTest.php | 2 +- tests/phpunit/utils/TestCloningRegistry.php | 6 +++- tools/compact-regions.php | 6 ++-- tools/generate-bedrock-data-from-packets.php | 14 +++++----- 66 files changed, 338 insertions(+), 124 deletions(-) diff --git a/build/generate-biome-ids.php b/build/generate-biome-ids.php index f36591fe4..56f871f43 100644 --- a/build/generate-biome-ids.php +++ b/build/generate-biome-ids.php @@ -122,7 +122,7 @@ if(!is_array($ids)){ throw new \RuntimeException("Invalid biome ID map, expected array for root JSON object"); } $cleanedIds = []; -foreach($ids as $name => $id){ +foreach(Utils::promoteKeys($ids) as $name => $id){ if(!is_string($name) || !is_int($id)){ throw new \RuntimeException("Invalid biome ID map, expected string => int map"); } diff --git a/build/generate-pocketmine-yml-property-consts.php b/build/generate-pocketmine-yml-property-consts.php index dcc574f8a..f90f3fc66 100644 --- a/build/generate-pocketmine-yml-property-consts.php +++ b/build/generate-pocketmine-yml-property-consts.php @@ -42,7 +42,7 @@ $constants = []; * @phpstan-param-out array $constants */ function collectProperties(string $prefix, array $properties, array &$constants) : void{ - foreach($properties as $propertyName => $property){ + foreach(Utils::promoteKeys($properties) as $propertyName => $property){ $fullPropertyName = ($prefix !== "" ? $prefix . "." : "") . $propertyName; $constName = str_replace([".", "-"], "_", strtoupper($fullPropertyName)); diff --git a/build/generate-registry-annotations.php b/build/generate-registry-annotations.php index bcdaee4eb..35d089395 100644 --- a/build/generate-registry-annotations.php +++ b/build/generate-registry-annotations.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\build\update_registry_annotations; +use pocketmine\utils\Utils; use function basename; use function class_exists; use function count; @@ -48,6 +49,7 @@ if(count($argv) !== 2){ /** * @param object[] $members + * @phpstan-param array $members */ function generateMethodAnnotations(string $namespaceName, array $members) : string{ $selfName = basename(__FILE__); @@ -60,7 +62,7 @@ function generateMethodAnnotations(string $namespaceName, array $members) : stri static $lineTmpl = " * @method static %2\$s %s()"; $memberLines = []; - foreach($members as $name => $member){ + foreach(Utils::stringifyKeys($members) as $name => $member){ $reflect = new \ReflectionClass($member); while($reflect !== false && $reflect->isAnonymous()){ $reflect = $reflect->getParentClass(); diff --git a/src/MemoryManager.php b/src/MemoryManager.php index 96e3bf49c..4308167d3 100644 --- a/src/MemoryManager.php +++ b/src/MemoryManager.php @@ -332,7 +332,7 @@ class MemoryManager{ continue; } $methodStatics = []; - foreach($method->getStaticVariables() as $name => $variable){ + foreach(Utils::promoteKeys($method->getStaticVariables()) as $name => $variable){ $methodStatics[$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize); } if(count($methodStatics) > 0){ @@ -360,7 +360,7 @@ class MemoryManager{ '_SESSION' => true ]; - foreach($GLOBALS as $varName => $value){ + foreach(Utils::promoteKeys($GLOBALS) as $varName => $value){ if(isset($ignoredGlobals[$varName])){ continue; } @@ -376,7 +376,7 @@ class MemoryManager{ $reflect = new \ReflectionFunction($function); $vars = []; - foreach($reflect->getStaticVariables() as $varName => $variable){ + foreach(Utils::promoteKeys($reflect->getStaticVariables()) as $varName => $variable){ $vars[$varName] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize); } if(count($vars) > 0){ @@ -416,7 +416,7 @@ class MemoryManager{ $info["this"] = self::continueDump($closureThis, $objects, $refCounts, 0, $maxNesting, $maxStringSize); } - foreach($reflect->getStaticVariables() as $name => $variable){ + foreach(Utils::promoteKeys($reflect->getStaticVariables()) as $name => $variable){ $info["referencedVars"][$name] = self::continueDump($variable, $objects, $refCounts, 0, $maxNesting, $maxStringSize); } }else{ @@ -499,7 +499,7 @@ class MemoryManager{ } $data = []; $numeric = 0; - foreach($from as $key => $value){ + foreach(Utils::promoteKeys($from) as $key => $value){ $data[$numeric] = [ "k" => self::continueDump($key, $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize), "v" => self::continueDump($value, $objects, $refCounts, $recursion + 1, $maxNesting, $maxStringSize), diff --git a/src/Server.php b/src/Server.php index a34349bb5..074088068 100644 --- a/src/Server.php +++ b/src/Server.php @@ -736,12 +736,15 @@ class Server{ /** * @return string[][] + * @phpstan-return array> */ public function getCommandAliases() : array{ $section = $this->configGroup->getProperty(Yml::ALIASES); $result = []; if(is_array($section)){ - foreach($section as $key => $value){ + foreach(Utils::promoteKeys($section) as $key => $value){ + //TODO: more validation needed here + //key might not be a string, value might not be list $commands = []; if(is_array($value)){ $commands = $value; @@ -749,7 +752,7 @@ class Server{ $commands[] = (string) $value; } - $result[$key] = $commands; + $result[(string) $key] = $commands; } } @@ -1094,7 +1097,11 @@ class Server{ $anyWorldFailedToLoad = false; - foreach((array) $this->configGroup->getProperty(Yml::WORLDS, []) as $name => $options){ + foreach(Utils::promoteKeys((array) $this->configGroup->getProperty(Yml::WORLDS, [])) as $name => $options){ + if(!is_string($name)){ + //TODO: this probably should be an error + continue; + } if($options === null){ $options = []; }elseif(!is_array($options)){ diff --git a/src/block/RuntimeBlockStateRegistry.php b/src/block/RuntimeBlockStateRegistry.php index 78be658bc..a8ece7722 100644 --- a/src/block/RuntimeBlockStateRegistry.php +++ b/src/block/RuntimeBlockStateRegistry.php @@ -132,6 +132,7 @@ class RuntimeBlockStateRegistry{ /** * @return Block[] + * @phpstan-return array */ public function getAllKnownStates() : array{ return $this->fullList; diff --git a/src/block/inventory/EnchantInventory.php b/src/block/inventory/EnchantInventory.php index b726dbedf..6dffbad32 100644 --- a/src/block/inventory/EnchantInventory.php +++ b/src/block/inventory/EnchantInventory.php @@ -39,7 +39,10 @@ class EnchantInventory extends SimpleInventory implements BlockInventory, Tempor public const SLOT_INPUT = 0; public const SLOT_LAPIS = 1; - /** @var EnchantingOption[] $options */ + /** + * @var EnchantingOption[] $options + * @phpstan-var list + */ private array $options = []; public function __construct(Position $holder){ diff --git a/src/block/utils/SignText.php b/src/block/utils/SignText.php index 51285d267..a7e8759b8 100644 --- a/src/block/utils/SignText.php +++ b/src/block/utils/SignText.php @@ -36,13 +36,17 @@ use function str_contains; class SignText{ public const LINE_COUNT = 4; - /** @var string[] */ + /** + * @var string[] + * @phpstan-var array{0: string, 1: string, 2: string, 3: string} + */ private array $lines; private Color $baseColor; private bool $glowing; /** * @param string[]|null $lines index-sensitive; keys 0-3 will be used, regardless of array order + * @phpstan-param array{0?: string, 1?: string, 2?: string, 3?: string}|null $lines * * @throws \InvalidArgumentException if the array size is greater than 4 * @throws \InvalidArgumentException if invalid keys (out of bounds or string) are found in the array @@ -82,6 +86,7 @@ class SignText{ * Returns an array of lines currently on the sign. * * @return string[] + * @phpstan-return array{0: string, 1: string, 2: string, 3: string} */ public function getLines() : array{ return $this->lines; diff --git a/src/command/Command.php b/src/command/Command.php index 02bae60d9..4c2c6815b 100644 --- a/src/command/Command.php +++ b/src/command/Command.php @@ -44,10 +44,16 @@ abstract class Command{ private string $nextLabel; private string $label; - /** @var string[] */ + /** + * @var string[] + * @phpstan-var list + */ private array $aliases = []; - /** @var string[] */ + /** + * @var string[] + * @phpstan-var list + */ private array $activeAliases = []; private ?CommandMap $commandMap = null; @@ -62,6 +68,7 @@ abstract class Command{ /** * @param string[] $aliases + * @phpstan-param list $aliases */ public function __construct(string $name, Translatable|string $description = "", Translatable|string|null $usageMessage = null, array $aliases = []){ $this->name = $name; @@ -182,6 +189,7 @@ abstract class Command{ /** * @return string[] + * @phpstan-return list */ public function getAliases() : array{ return $this->activeAliases; @@ -201,6 +209,7 @@ abstract class Command{ /** * @param string[] $aliases + * @phpstan-param list $aliases */ public function setAliases(array $aliases) : void{ $this->aliases = $aliases; diff --git a/src/command/SimpleCommandMap.php b/src/command/SimpleCommandMap.php index 1268e715b..06367994a 100644 --- a/src/command/SimpleCommandMap.php +++ b/src/command/SimpleCommandMap.php @@ -70,6 +70,7 @@ use pocketmine\lang\KnownTranslationFactory; use pocketmine\Server; use pocketmine\timings\Timings; use pocketmine\utils\TextFormat; +use pocketmine\utils\Utils; use function array_shift; use function count; use function implode; @@ -80,7 +81,10 @@ use function trim; class SimpleCommandMap implements CommandMap{ - /** @var Command[] */ + /** + * @var Command[] + * @phpstan-var array + */ protected array $knownCommands = []; public function __construct(private Server $server){ @@ -169,7 +173,7 @@ class SimpleCommandMap implements CommandMap{ } public function unregister(Command $command) : bool{ - foreach($this->knownCommands as $lbl => $cmd){ + foreach(Utils::promoteKeys($this->knownCommands) as $lbl => $cmd){ if($cmd === $command){ unset($this->knownCommands[$lbl]); } @@ -237,6 +241,7 @@ class SimpleCommandMap implements CommandMap{ /** * @return Command[] + * @phpstan-return array */ public function getCommands() : array{ return $this->knownCommands; @@ -245,7 +250,7 @@ class SimpleCommandMap implements CommandMap{ public function registerServerAliases() : void{ $values = $this->server->getCommandAliases(); - foreach($values as $alias => $commandStrings){ + foreach(Utils::stringifyKeys($values) as $alias => $commandStrings){ if(str_contains($alias, ":")){ $this->server->getLogger()->warning($this->server->getLanguage()->translate(KnownTranslationFactory::pocketmine_command_alias_illegal($alias))); continue; diff --git a/src/crafting/CraftingManager.php b/src/crafting/CraftingManager.php index c7c0b10c6..ff2be6926 100644 --- a/src/crafting/CraftingManager.php +++ b/src/crafting/CraftingManager.php @@ -110,14 +110,15 @@ class CraftingManager{ /** * @param Item[] $items + * @phpstan-param list $items * * @return Item[] + * @phpstan-return list */ private static function pack(array $items) : array{ - /** @var Item[] $result */ $result = []; - foreach($items as $i => $item){ + foreach($items as $item){ foreach($result as $otherItem){ if($item->canStackWith($otherItem)){ $otherItem->setCount($otherItem->getCount() + $item->getCount()); @@ -134,6 +135,7 @@ class CraftingManager{ /** * @param Item[] $outputs + * @phpstan-param list $outputs */ private static function hashOutputs(array $outputs) : string{ $outputs = self::pack($outputs); diff --git a/src/crafting/CraftingManagerFromDataHelper.php b/src/crafting/CraftingManagerFromDataHelper.php index 616c2a4bd..3df6bfb62 100644 --- a/src/crafting/CraftingManagerFromDataHelper.php +++ b/src/crafting/CraftingManagerFromDataHelper.php @@ -193,7 +193,7 @@ final class CraftingManagerFromDataHelper{ */ private static function loadJsonObjectListIntoModel(\JsonMapper $mapper, string $modelClass, array $data) : array{ $result = []; - foreach($data as $i => $item){ + foreach(Utils::promoteKeys($data) as $i => $item){ if(!is_object($item)){ throw new SavedDataLoadingException("Invalid entry at index $i: expected object, got " . get_debug_type($item)); } diff --git a/src/crafting/CraftingRecipe.php b/src/crafting/CraftingRecipe.php index 02e6fce04..d7342aae3 100644 --- a/src/crafting/CraftingRecipe.php +++ b/src/crafting/CraftingRecipe.php @@ -30,6 +30,7 @@ interface CraftingRecipe{ * Returns a list of items needed to craft this recipe. This MUST NOT include Air items or items with a zero count. * * @return RecipeIngredient[] + * @phpstan-return list */ public function getIngredientList() : array; @@ -37,6 +38,7 @@ interface CraftingRecipe{ * Returns a list of results this recipe will produce when the inputs in the given crafting grid are consumed. * * @return Item[] + * @phpstan-return list */ public function getResultsFor(CraftingGrid $grid) : array; diff --git a/src/crafting/ShapedRecipe.php b/src/crafting/ShapedRecipe.php index f13ba3ae7..4c40eb0f5 100644 --- a/src/crafting/ShapedRecipe.php +++ b/src/crafting/ShapedRecipe.php @@ -32,11 +32,20 @@ use function str_contains; use function strlen; class ShapedRecipe implements CraftingRecipe{ - /** @var string[] */ + /** + * @var string[] + * @phpstan-var list + */ private array $shape = []; - /** @var RecipeIngredient[] char => RecipeIngredient map */ + /** + * @var RecipeIngredient[] char => RecipeIngredient map + * @phpstan-var array + */ private array $ingredientList = []; - /** @var Item[] */ + /** + * @var Item[] + * @phpstan-var list + */ private array $results = []; private int $height; @@ -56,6 +65,10 @@ class ShapedRecipe implements CraftingRecipe{ * @param Item[] $results List of items that this recipe produces when crafted. * * Note: Recipes **do not** need to be square. Do NOT add padding for empty rows/columns. + * + * @phpstan-param list $shape + * @phpstan-param array $ingredients + * @phpstan-param list $results */ public function __construct(array $shape, array $ingredients, array $results){ $this->height = count($shape); @@ -84,7 +97,7 @@ class ShapedRecipe implements CraftingRecipe{ $this->shape = $shape; - foreach($ingredients as $char => $i){ + foreach(Utils::stringifyKeys($ingredients) as $char => $i){ if(!str_contains(implode($this->shape), $char)){ throw new \InvalidArgumentException("Symbol '$char' does not appear in the recipe shape"); } @@ -105,6 +118,7 @@ class ShapedRecipe implements CraftingRecipe{ /** * @return Item[] + * @phpstan-return list */ public function getResults() : array{ return Utils::cloneObjectArray($this->results); @@ -112,6 +126,7 @@ class ShapedRecipe implements CraftingRecipe{ /** * @return Item[] + * @phpstan-return list */ public function getResultsFor(CraftingGrid $grid) : array{ return $this->getResults(); @@ -119,6 +134,7 @@ class ShapedRecipe implements CraftingRecipe{ /** * @return (RecipeIngredient|null)[][] + * @phpstan-return list> */ public function getIngredientMap() : array{ $ingredients = []; @@ -132,9 +148,6 @@ class ShapedRecipe implements CraftingRecipe{ return $ingredients; } - /** - * @return RecipeIngredient[] - */ public function getIngredientList() : array{ $ingredients = []; @@ -157,6 +170,7 @@ class ShapedRecipe implements CraftingRecipe{ /** * Returns an array of strings containing characters representing the recipe's shape. * @return string[] + * @phpstan-return list */ public function getShape() : array{ return $this->shape; diff --git a/src/crafting/ShapelessRecipe.php b/src/crafting/ShapelessRecipe.php index a9e1f023c..7a4a22fda 100644 --- a/src/crafting/ShapelessRecipe.php +++ b/src/crafting/ShapelessRecipe.php @@ -28,15 +28,24 @@ use pocketmine\utils\Utils; use function count; class ShapelessRecipe implements CraftingRecipe{ - /** @var RecipeIngredient[] */ + /** + * @var RecipeIngredient[] + * @phpstan-var list + */ private array $ingredients = []; - /** @var Item[] */ + /** + * @var Item[] + * @phpstan-var list + */ private array $results; private ShapelessRecipeType $type; /** * @param RecipeIngredient[] $ingredients No more than 9 total. This applies to sum of item stack counts, not count of array. * @param Item[] $results List of result items created by this recipe. + * + * @phpstan-param list $ingredients + * @phpstan-param list $results */ public function __construct(array $ingredients, array $results, ShapelessRecipeType $type){ $this->type = $type; @@ -50,6 +59,7 @@ class ShapelessRecipe implements CraftingRecipe{ /** * @return Item[] + * @phpstan-return list */ public function getResults() : array{ return Utils::cloneObjectArray($this->results); @@ -63,9 +73,6 @@ class ShapelessRecipe implements CraftingRecipe{ return $this->type; } - /** - * @return RecipeIngredient[] - */ public function getIngredientList() : array{ return $this->ingredients; } diff --git a/src/crash/CrashDumpData.php b/src/crash/CrashDumpData.php index b71e6f405..f8a20abd5 100644 --- a/src/crash/CrashDumpData.php +++ b/src/crash/CrashDumpData.php @@ -43,7 +43,10 @@ final class CrashDumpData implements \JsonSerializable{ public string $plugin = ""; - /** @var string[] */ + /** + * @var string[] + * @phpstan-var array + */ public array $code = []; /** @var string[] */ @@ -55,7 +58,10 @@ final class CrashDumpData implements \JsonSerializable{ */ public array $plugins = []; - /** @var string[] */ + /** + * @var string[] + * @phpstan-var list + */ public array $parameters = []; public string $serverDotProperties = ""; diff --git a/src/data/bedrock/ItemTagToIdMap.php b/src/data/bedrock/ItemTagToIdMap.php index 84f2ff451..af9cb3ce5 100644 --- a/src/data/bedrock/ItemTagToIdMap.php +++ b/src/data/bedrock/ItemTagToIdMap.php @@ -48,7 +48,7 @@ final class ItemTagToIdMap{ throw new AssumptionFailedError("Invalid item tag map, expected array"); } $cleanMap = []; - foreach($map as $tagName => $ids){ + foreach(Utils::promoteKeys($map) as $tagName => $ids){ if(!is_string($tagName)){ throw new AssumptionFailedError("Invalid item tag name $tagName, expected string as key"); } diff --git a/src/data/bedrock/LegacyToStringIdMap.php b/src/data/bedrock/LegacyToStringIdMap.php index 2b5375db2..1a92f2b6b 100644 --- a/src/data/bedrock/LegacyToStringIdMap.php +++ b/src/data/bedrock/LegacyToStringIdMap.php @@ -25,6 +25,7 @@ namespace pocketmine\data\bedrock; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Filesystem; +use pocketmine\utils\Utils; use function is_array; use function is_int; use function is_string; @@ -43,7 +44,7 @@ abstract class LegacyToStringIdMap{ if(!is_array($stringToLegacyId)){ throw new AssumptionFailedError("Invalid format of ID map"); } - foreach($stringToLegacyId as $stringId => $legacyId){ + foreach(Utils::promoteKeys($stringToLegacyId) as $stringId => $legacyId){ if(!is_string($stringId) || !is_int($legacyId)){ throw new AssumptionFailedError("ID map should have string keys and int values"); } diff --git a/src/data/bedrock/item/upgrade/R12ItemIdToBlockIdMap.php b/src/data/bedrock/item/upgrade/R12ItemIdToBlockIdMap.php index 2aac8de64..19cfe6c78 100644 --- a/src/data/bedrock/item/upgrade/R12ItemIdToBlockIdMap.php +++ b/src/data/bedrock/item/upgrade/R12ItemIdToBlockIdMap.php @@ -56,7 +56,7 @@ final class R12ItemIdToBlockIdMap{ } $builtMap = []; - foreach($map as $itemId => $blockId){ + foreach(Utils::promoteKeys($map) as $itemId => $blockId){ if(!is_string($itemId)){ throw new AssumptionFailedError("Invalid blockitem ID mapping table, expected string as key"); } diff --git a/src/entity/Entity.php b/src/entity/Entity.php index c55a8716c..3d791ebca 100644 --- a/src/entity/Entity.php +++ b/src/entity/Entity.php @@ -102,7 +102,10 @@ abstract class Entity{ return self::$entityCount++; } - /** @var Player[] */ + /** + * @var Player[] + * @phpstan-var array + */ protected array $hasSpawned = []; protected int $id; diff --git a/src/event/HandlerList.php b/src/event/HandlerList.php index 2072cd522..e7d229a3f 100644 --- a/src/event/HandlerList.php +++ b/src/event/HandlerList.php @@ -30,7 +30,10 @@ use function spl_object_id; use const SORT_NUMERIC; class HandlerList{ - /** @var RegisteredListener[][] */ + /** + * @var RegisteredListener[][] + * @phpstan-var array> + */ private array $handlerSlots = []; /** @var RegisteredListenerCache[] */ diff --git a/src/inventory/BaseInventory.php b/src/inventory/BaseInventory.php index 522c827a4..0d5d1ffe6 100644 --- a/src/inventory/BaseInventory.php +++ b/src/inventory/BaseInventory.php @@ -41,7 +41,10 @@ use function spl_object_id; */ abstract class BaseInventory implements Inventory, SlotValidatedInventory{ protected int $maxStackSize = Inventory::MAX_STACK; - /** @var Player[] */ + /** + * @var Player[] + * @phpstan-var array + */ protected array $viewers = []; /** * @var InventoryListener[]|ObjectSet @@ -286,8 +289,6 @@ abstract class BaseInventory implements Inventory, SlotValidatedInventory{ } public function removeItem(Item ...$slots) : array{ - /** @var Item[] $searchItems */ - /** @var Item[] $slots */ $searchItems = []; foreach($slots as $slot){ if(!$slot->isNull()){ diff --git a/src/inventory/CreativeInventory.php b/src/inventory/CreativeInventory.php index 7502a6785..57e5cbb4e 100644 --- a/src/inventory/CreativeInventory.php +++ b/src/inventory/CreativeInventory.php @@ -36,7 +36,10 @@ final class CreativeInventory{ use SingletonTrait; use DestructorCallbackTrait; - /** @var Item[] */ + /** + * @var Item[] + * @phpstan-var array + */ private array $creative = []; /** @phpstan-var ObjectSet<\Closure() : void> */ @@ -69,6 +72,7 @@ final class CreativeInventory{ /** * @return Item[] + * @phpstan-return array */ public function getAll() : array{ return Utils::cloneObjectArray($this->creative); diff --git a/src/inventory/transaction/CraftingTransaction.php b/src/inventory/transaction/CraftingTransaction.php index 2ae231b6e..9e3c50552 100644 --- a/src/inventory/transaction/CraftingTransaction.php +++ b/src/inventory/transaction/CraftingTransaction.php @@ -57,9 +57,15 @@ class CraftingTransaction extends InventoryTransaction{ protected ?CraftingRecipe $recipe = null; protected ?int $repetitions = null; - /** @var Item[] */ + /** + * @var Item[] + * @phpstan-var list + */ protected array $inputs = []; - /** @var Item[] */ + /** + * @var Item[] + * @phpstan-var list + */ protected array $outputs = []; private CraftingManager $craftingManager; @@ -74,6 +80,9 @@ class CraftingTransaction extends InventoryTransaction{ /** * @param Item[] $providedItems * @return Item[] + * + * @phpstan-param list $providedItems + * @phpstan-return list */ private static function packItems(array $providedItems) : array{ $packedProvidedItems = []; @@ -94,6 +103,9 @@ class CraftingTransaction extends InventoryTransaction{ /** * @param Item[] $providedItems * @param RecipeIngredient[] $recipeIngredients + * + * @phpstan-param list $providedItems + * @phpstan-param list $recipeIngredients */ public static function matchIngredients(array $providedItems, array $recipeIngredients, int $expectedIterations) : void{ if(count($recipeIngredients) === 0){ @@ -172,6 +184,9 @@ class CraftingTransaction extends InventoryTransaction{ * @param Item[] $txItems * @param Item[] $recipeItems * + * @phpstan-param list $txItems + * @phpstan-param list $recipeItems + * * @throws TransactionValidationException */ protected function matchOutputs(array $txItems, array $recipeItems) : int{ diff --git a/src/inventory/transaction/InventoryTransaction.php b/src/inventory/transaction/InventoryTransaction.php index 2ca00f910..b3465a8df 100644 --- a/src/inventory/transaction/InventoryTransaction.php +++ b/src/inventory/transaction/InventoryTransaction.php @@ -29,6 +29,7 @@ use pocketmine\inventory\transaction\action\InventoryAction; use pocketmine\inventory\transaction\action\SlotChangeAction; use pocketmine\item\Item; use pocketmine\player\Player; +use pocketmine\utils\Utils; use function array_keys; use function array_values; use function assert; @@ -57,10 +58,16 @@ use function spl_object_id; class InventoryTransaction{ protected bool $hasExecuted = false; - /** @var Inventory[] */ + /** + * @var Inventory[] + * @phpstan-var array + */ protected array $inventories = []; - /** @var InventoryAction[] */ + /** + * @var InventoryAction[] + * @phpstan-var array + */ protected array $actions = []; /** @@ -81,6 +88,7 @@ class InventoryTransaction{ /** * @return Inventory[] + * @phpstan-return array */ public function getInventories() : array{ return $this->inventories; @@ -93,6 +101,7 @@ class InventoryTransaction{ * significance and should not be relied on. * * @return InventoryAction[] + * @phpstan-return array */ public function getActions() : array{ return $this->actions; @@ -133,8 +142,8 @@ class InventoryTransaction{ /** * @param Item[] $needItems * @param Item[] $haveItems - * @phpstan-param-out Item[] $needItems - * @phpstan-param-out Item[] $haveItems + * @phpstan-param-out list $needItems + * @phpstan-param-out list $haveItems * * @throws TransactionValidationException */ @@ -188,11 +197,8 @@ class InventoryTransaction{ * wrong order), so this method also tries to chain them into order. */ protected function squashDuplicateSlotChanges() : void{ - /** @var SlotChangeAction[][] $slotChanges */ $slotChanges = []; - /** @var Inventory[] $inventories */ $inventories = []; - /** @var int[] $slots */ $slots = []; foreach($this->actions as $key => $action){ @@ -203,7 +209,7 @@ class InventoryTransaction{ } } - foreach($slotChanges as $hash => $list){ + foreach(Utils::stringifyKeys($slotChanges) as $hash => $list){ if(count($list) === 1){ //No need to compact slot changes if there is only one on this slot continue; } @@ -233,6 +239,7 @@ class InventoryTransaction{ /** * @param SlotChangeAction[] $possibleActions + * @phpstan-param list $possibleActions */ protected function findResultItem(Item $needOrigin, array $possibleActions) : ?Item{ assert(count($possibleActions) > 0); diff --git a/src/item/ItemEnchantmentHandlingTrait.php b/src/item/ItemEnchantmentHandlingTrait.php index d8302e7b3..10da7dd76 100644 --- a/src/item/ItemEnchantmentHandlingTrait.php +++ b/src/item/ItemEnchantmentHandlingTrait.php @@ -33,7 +33,10 @@ use function spl_object_id; * The primary purpose of this trait is providing scope isolation for the methods it contains. */ trait ItemEnchantmentHandlingTrait{ - /** @var EnchantmentInstance[] */ + /** + * @var EnchantmentInstance[] + * @phpstan-var array + */ protected array $enchantments = []; public function hasEnchantments() : bool{ @@ -79,6 +82,7 @@ trait ItemEnchantmentHandlingTrait{ /** * @return EnchantmentInstance[] + * @phpstan-return array */ public function getEnchantments() : array{ return $this->enchantments; diff --git a/src/item/LegacyStringToItemParser.php b/src/item/LegacyStringToItemParser.php index a52b6c716..6969190d5 100644 --- a/src/item/LegacyStringToItemParser.php +++ b/src/item/LegacyStringToItemParser.php @@ -29,6 +29,7 @@ use pocketmine\data\bedrock\item\upgrade\ItemDataUpgrader; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\Filesystem; use pocketmine\utils\SingletonTrait; +use pocketmine\utils\Utils; use pocketmine\world\format\io\GlobalItemDataHandlers; use Symfony\Component\Filesystem\Path; use function explode; @@ -67,7 +68,7 @@ final class LegacyStringToItemParser{ $mappings = json_decode($mappingsRaw, true); if(!is_array($mappings)) throw new AssumptionFailedError("Invalid mappings format, expected array"); - foreach($mappings as $name => $id){ + foreach(Utils::promoteKeys($mappings) as $name => $id){ if(!is_string($id)) throw new AssumptionFailedError("Invalid mappings format, expected string values"); $result->addMapping((string) $name, $id); } diff --git a/src/lang/Language.php b/src/lang/Language.php index a871c820a..29f28917d 100644 --- a/src/lang/Language.php +++ b/src/lang/Language.php @@ -147,7 +147,7 @@ class Language{ $baseText = $this->parseTranslation($str, $onlyPrefix); } - foreach($params as $i => $p){ + foreach(Utils::promoteKeys($params) as $i => $p){ $replacement = $p instanceof Translatable ? $this->translate($p) : (string) $p; $baseText = str_replace("{%$i}", $replacement, $baseText); } @@ -161,7 +161,7 @@ class Language{ $baseText = $this->parseTranslation($c->getText()); } - foreach($c->getParameters() as $i => $p){ + foreach(Utils::promoteKeys($c->getParameters()) as $i => $p){ $replacement = $p instanceof Translatable ? $this->translate($p) : $p; $baseText = str_replace("{%$i}", $replacement, $baseText); } diff --git a/src/lang/Translatable.php b/src/lang/Translatable.php index 827a11657..8dee8f477 100644 --- a/src/lang/Translatable.php +++ b/src/lang/Translatable.php @@ -23,6 +23,8 @@ declare(strict_types=1); namespace pocketmine\lang; +use pocketmine\utils\Utils; + final class Translatable{ /** @var string[]|Translatable[] $params */ protected array $params = []; @@ -34,7 +36,7 @@ final class Translatable{ protected string $text, array $params = [] ){ - foreach($params as $k => $param){ + foreach(Utils::promoteKeys($params) as $k => $param){ if(!($param instanceof Translatable)){ $this->params[$k] = (string) $param; }else{ diff --git a/src/network/NetworkSessionManager.php b/src/network/NetworkSessionManager.php index d8ff7fe03..aecbc646d 100644 --- a/src/network/NetworkSessionManager.php +++ b/src/network/NetworkSessionManager.php @@ -30,10 +30,16 @@ use function spl_object_id; class NetworkSessionManager{ - /** @var NetworkSession[] */ + /** + * @var NetworkSession[] + * @phpstan-var array + */ private array $sessions = []; - /** @var NetworkSession[] */ + /** + * @var NetworkSession[] + * @phpstan-var array + */ private array $pendingLoginSessions = []; /** diff --git a/src/network/mcpe/InventoryManager.php b/src/network/mcpe/InventoryManager.php index 70c427aa1..7df8c734b 100644 --- a/src/network/mcpe/InventoryManager.php +++ b/src/network/mcpe/InventoryManager.php @@ -695,6 +695,7 @@ class InventoryManager{ /** * @param EnchantingOption[] $options + * @phpstan-param list $options */ public function syncEnchantingTableOptions(array $options) : void{ $protocolOptions = []; diff --git a/src/network/mcpe/NetworkSession.php b/src/network/mcpe/NetworkSession.php index 0eee71e0e..78b11e27e 100644 --- a/src/network/mcpe/NetworkSession.php +++ b/src/network/mcpe/NetworkSession.php @@ -1092,7 +1092,7 @@ class NetworkSession{ public function syncAvailableCommands() : void{ $commandData = []; - foreach($this->server->getCommandMap()->getCommands() as $name => $command){ + foreach($this->server->getCommandMap()->getCommands() as $command){ if(isset($commandData[$command->getLabel()]) || $command->getLabel() === "help" || !$command->testPermissionSilent($this->player)){ continue; } diff --git a/src/network/mcpe/StandardPacketBroadcaster.php b/src/network/mcpe/StandardPacketBroadcaster.php index 32afdeeb7..7a91b397b 100644 --- a/src/network/mcpe/StandardPacketBroadcaster.php +++ b/src/network/mcpe/StandardPacketBroadcaster.php @@ -53,7 +53,6 @@ final class StandardPacketBroadcaster implements PacketBroadcaster{ $compressors = []; - /** @var NetworkSession[][] $targetsByCompressor */ $targetsByCompressor = []; foreach($recipients as $recipient){ //TODO: different compressors might be compatible, it might not be necessary to split them up by object diff --git a/src/network/mcpe/cache/ChunkCache.php b/src/network/mcpe/cache/ChunkCache.php index 6d8020085..12e769776 100644 --- a/src/network/mcpe/cache/ChunkCache.php +++ b/src/network/mcpe/cache/ChunkCache.php @@ -78,7 +78,10 @@ class ChunkCache implements ChunkListener{ } } - /** @var CompressBatchPromise[] */ + /** + * @var CompressBatchPromise[] + * @phpstan-var array + */ private array $caches = []; private int $hits = 0; diff --git a/src/network/mcpe/convert/BlockStateDictionaryEntry.php b/src/network/mcpe/convert/BlockStateDictionaryEntry.php index 8c6244da5..28cc3960d 100644 --- a/src/network/mcpe/convert/BlockStateDictionaryEntry.php +++ b/src/network/mcpe/convert/BlockStateDictionaryEntry.php @@ -28,6 +28,7 @@ use pocketmine\nbt\LittleEndianNbtSerializer; use pocketmine\nbt\tag\CompoundTag; use pocketmine\nbt\tag\Tag; use pocketmine\nbt\TreeRoot; +use pocketmine\utils\Utils; use function count; use function ksort; use const SORT_STRING; @@ -43,6 +44,7 @@ final class BlockStateDictionaryEntry{ /** * @param Tag[] $stateProperties + * @phpstan-param array $stateProperties */ public function __construct( private string $stateName, @@ -79,6 +81,7 @@ final class BlockStateDictionaryEntry{ /** * @param Tag[] $properties + * @phpstan-param array $properties */ public static function encodeStateProperties(array $properties) : string{ if(count($properties) === 0){ @@ -87,7 +90,7 @@ final class BlockStateDictionaryEntry{ //TODO: make a more efficient encoding - NBT will do for now, but it's not very compact ksort($properties, SORT_STRING); $tag = new CompoundTag(); - foreach($properties as $k => $v){ + foreach(Utils::stringifyKeys($properties) as $k => $v){ $tag->setTag($k, $v); } return (new LittleEndianNbtSerializer())->write(new TreeRoot($tag)); diff --git a/src/network/mcpe/convert/ItemTypeDictionaryFromDataHelper.php b/src/network/mcpe/convert/ItemTypeDictionaryFromDataHelper.php index 5d06758ef..d962063d3 100644 --- a/src/network/mcpe/convert/ItemTypeDictionaryFromDataHelper.php +++ b/src/network/mcpe/convert/ItemTypeDictionaryFromDataHelper.php @@ -26,6 +26,7 @@ namespace pocketmine\network\mcpe\convert; use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary; use pocketmine\network\mcpe\protocol\types\ItemTypeEntry; use pocketmine\utils\AssumptionFailedError; +use pocketmine\utils\Utils; use function is_array; use function is_bool; use function is_int; @@ -41,7 +42,7 @@ final class ItemTypeDictionaryFromDataHelper{ } $params = []; - foreach($table as $name => $entry){ + foreach(Utils::promoteKeys($table) as $name => $entry){ if(!is_array($entry) || !is_string($name) || !isset($entry["component_based"], $entry["runtime_id"]) || !is_bool($entry["component_based"]) || !is_int($entry["runtime_id"])){ throw new AssumptionFailedError("Invalid item list format"); } diff --git a/src/network/mcpe/handler/InGamePacketHandler.php b/src/network/mcpe/handler/InGamePacketHandler.php index c92db3133..cc1f3eab4 100644 --- a/src/network/mcpe/handler/InGamePacketHandler.php +++ b/src/network/mcpe/handler/InGamePacketHandler.php @@ -251,7 +251,7 @@ class InGamePacketHandler extends PacketHandler{ if(count($blockActions) > 100){ throw new PacketHandlingException("Too many block actions in PlayerAuthInputPacket"); } - foreach($blockActions as $k => $blockAction){ + foreach(Utils::promoteKeys($blockActions) as $k => $blockAction){ $actionHandled = false; if($blockAction instanceof PlayerBlockActionStopBreak){ $actionHandled = $this->handlePlayerActionFromData($blockAction->getActionType(), new BlockPosition(0, 0, 0), Facing::DOWN); diff --git a/src/network/mcpe/handler/ItemStackRequestExecutor.php b/src/network/mcpe/handler/ItemStackRequestExecutor.php index 54a192590..6db8f1e12 100644 --- a/src/network/mcpe/handler/ItemStackRequestExecutor.php +++ b/src/network/mcpe/handler/ItemStackRequestExecutor.php @@ -54,6 +54,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\stackresponse\ItemStackResp use pocketmine\network\mcpe\protocol\types\inventory\UIInventorySlotOffset; use pocketmine\player\Player; use pocketmine\utils\AssumptionFailedError; +use pocketmine\utils\Utils; use function array_key_first; use function count; use function spl_object_id; @@ -370,7 +371,7 @@ class ItemStackRequestExecutor{ * @throws ItemStackRequestProcessException */ public function generateInventoryTransaction() : InventoryTransaction{ - foreach($this->request->getActions() as $k => $action){ + foreach(Utils::promoteKeys($this->request->getActions()) as $k => $action){ try{ $this->processItemStackRequestAction($action); }catch(ItemStackRequestProcessException $e){ diff --git a/src/network/mcpe/handler/LoginPacketHandler.php b/src/network/mcpe/handler/LoginPacketHandler.php index 2e3a51519..c15753dad 100644 --- a/src/network/mcpe/handler/LoginPacketHandler.php +++ b/src/network/mcpe/handler/LoginPacketHandler.php @@ -150,7 +150,7 @@ class LoginPacketHandler extends PacketHandler{ protected function fetchAuthData(JwtChain $chain) : AuthenticationData{ /** @var AuthenticationData|null $extraData */ $extraData = null; - foreach($chain->chain as $k => $jwt){ + foreach($chain->chain as $jwt){ //validate every chain element try{ [, $claims, ] = JwtUtils::parse($jwt); diff --git a/src/permission/BanList.php b/src/permission/BanList.php index b09cd98c9..36826ed66 100644 --- a/src/permission/BanList.php +++ b/src/permission/BanList.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\permission; +use pocketmine\utils\Utils; use function fclose; use function fgets; use function fopen; @@ -32,7 +33,10 @@ use function strtolower; use function trim; class BanList{ - /** @var BanEntry[] */ + /** + * @var BanEntry[] + * @phpstan-var array + */ private array $list = []; private bool $enabled = true; @@ -101,7 +105,7 @@ class BanList{ } public function removeExpired() : void{ - foreach($this->list as $name => $entry){ + foreach(Utils::promoteKeys($this->list) as $name => $entry){ if($entry->hasExpired()){ unset($this->list[$name]); } diff --git a/src/permission/PermissionAttachment.php b/src/permission/PermissionAttachment.php index b01fba307..79c66ba8a 100644 --- a/src/permission/PermissionAttachment.php +++ b/src/permission/PermissionAttachment.php @@ -25,10 +25,14 @@ namespace pocketmine\permission; use pocketmine\plugin\Plugin; use pocketmine\plugin\PluginException; +use pocketmine\utils\Utils; use function spl_object_id; class PermissionAttachment{ - /** @var bool[] */ + /** + * @var bool[] + * @phpstan-var array + */ private array $permissions = []; /** @@ -60,6 +64,7 @@ class PermissionAttachment{ /** * @return bool[] + * @phpstan-return array */ public function getPermissions() : array{ return $this->permissions; @@ -78,9 +83,10 @@ class PermissionAttachment{ /** * @param bool[] $permissions + * @phpstan-param array $permissions */ public function setPermissions(array $permissions) : void{ - foreach($permissions as $key => $value){ + foreach(Utils::stringifyKeys($permissions) as $key => $value){ $this->permissions[$key] = $value; } $this->recalculatePermissibles(); diff --git a/src/permission/PermissionManager.php b/src/permission/PermissionManager.php index 1291ba86b..f2b02e8e5 100644 --- a/src/permission/PermissionManager.php +++ b/src/permission/PermissionManager.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\permission; +use pocketmine\utils\Utils; use function count; use function spl_object_id; @@ -37,9 +38,15 @@ class PermissionManager{ return self::$instance; } - /** @var Permission[] */ + /** + * @var Permission[] + * @phpstan-var array + */ protected array $permissions = []; - /** @var PermissibleInternal[][] */ + /** + * @var PermissibleInternal[][] + * @phpstan-var array> + */ protected array $permSubs = []; public function getPermission(string $name) : ?Permission{ @@ -82,7 +89,7 @@ class PermissionManager{ } public function unsubscribeFromAllPermissions(PermissibleInternal $permissible) : void{ - foreach($this->permSubs as $permission => $subs){ + foreach(Utils::promoteKeys($this->permSubs) as $permission => $subs){ if(count($subs) === 1 && isset($subs[spl_object_id($permissible)])){ unset($this->permSubs[$permission]); }else{ diff --git a/src/plugin/ApiVersion.php b/src/plugin/ApiVersion.php index bcf7e59a0..d95b66576 100644 --- a/src/plugin/ApiVersion.php +++ b/src/plugin/ApiVersion.php @@ -70,7 +70,6 @@ final class ApiVersion{ * @return string[] */ public static function checkAmbiguousVersions(array $versions) : array{ - /** @var VersionString[][] $indexedVersions */ $indexedVersions = []; foreach($versions as $str){ @@ -85,9 +84,8 @@ final class ApiVersion{ } } - /** @var VersionString[] $result */ $result = []; - foreach($indexedVersions as $major => $list){ + foreach($indexedVersions as $list){ if(count($list) > 1){ array_push($result, ...$list); } diff --git a/src/plugin/PluginDescription.php b/src/plugin/PluginDescription.php index 72f0add7f..35ae2ba32 100644 --- a/src/plugin/PluginDescription.php +++ b/src/plugin/PluginDescription.php @@ -26,6 +26,7 @@ namespace pocketmine\plugin; use pocketmine\permission\Permission; use pocketmine\permission\PermissionParser; use pocketmine\permission\PermissionParserException; +use pocketmine\utils\Utils; use function array_map; use function array_values; use function get_debug_type; @@ -151,7 +152,7 @@ class PluginDescription{ $this->compatibleOperatingSystems = array_map("\strval", (array) ($plugin[self::KEY_OS] ?? [])); if(isset($plugin[self::KEY_COMMANDS]) && is_array($plugin[self::KEY_COMMANDS])){ - foreach($plugin[self::KEY_COMMANDS] as $commandName => $commandData){ + foreach(Utils::promoteKeys($plugin[self::KEY_COMMANDS]) as $commandName => $commandData){ if(!is_string($commandName)){ throw new PluginDescriptionParseException("Invalid Plugin commands, key must be the name of the command"); } @@ -177,7 +178,7 @@ class PluginDescription{ if(isset($plugin[self::KEY_EXTENSIONS])){ $extensions = (array) $plugin[self::KEY_EXTENSIONS]; $isLinear = $extensions === array_values($extensions); - foreach($extensions as $k => $v){ + foreach(Utils::promoteKeys($extensions) as $k => $v){ if($isLinear){ $k = $v; $v = "*"; diff --git a/src/plugin/PluginGraylist.php b/src/plugin/PluginGraylist.php index 84aa3f792..ff9d71832 100644 --- a/src/plugin/PluginGraylist.php +++ b/src/plugin/PluginGraylist.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\plugin; +use pocketmine\utils\Utils; use function array_flip; use function is_array; use function is_float; @@ -77,7 +78,7 @@ class PluginGraylist{ if(!is_array($array["plugins"])){ throw new \InvalidArgumentException("\"plugins\" must be an array"); } - foreach($array["plugins"] as $k => $v){ + foreach(Utils::promoteKeys($array["plugins"]) as $k => $v){ if(!is_string($v) && !is_int($v) && !is_float($v)){ throw new \InvalidArgumentException("\"plugins\" contains invalid element at position $k"); } diff --git a/src/plugin/PluginManager.php b/src/plugin/PluginManager.php index 198e4e893..f84698aa0 100644 --- a/src/plugin/PluginManager.php +++ b/src/plugin/PluginManager.php @@ -69,10 +69,16 @@ use function strtolower; * Manages all the plugins */ class PluginManager{ - /** @var Plugin[] */ + /** + * @var Plugin[] + * @phpstan-var array + */ protected array $plugins = []; - /** @var Plugin[] */ + /** + * @var Plugin[] + * @phpstan-var array + */ protected array $enabledPlugins = []; /** @var array> */ @@ -114,6 +120,7 @@ class PluginManager{ /** * @return Plugin[] + * @phpstan-return array */ public function getPlugins() : array{ return $this->plugins; @@ -526,7 +533,7 @@ class PluginManager{ } public function tickSchedulers(int $currentTick) : void{ - foreach($this->enabledPlugins as $pluginName => $p){ + foreach(Utils::promoteKeys($this->enabledPlugins) as $pluginName => $p){ if(isset($this->enabledPlugins[$pluginName])){ //the plugin may have been disabled as a result of updating other plugins' schedulers, and therefore //removed from enabledPlugins; however, foreach will still see it due to copy-on-write diff --git a/src/promise/Promise.php b/src/promise/Promise.php index 0def7e605..cd8fcc4f1 100644 --- a/src/promise/Promise.php +++ b/src/promise/Promise.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\promise; +use pocketmine\utils\Utils; use function count; use function spl_object_id; @@ -83,7 +84,7 @@ final class Promise{ $toResolve = count($promises); $continue = true; - foreach($promises as $key => $promise){ + foreach(Utils::promoteKeys($promises) as $key => $promise){ $promise->onCompletion( function(mixed $value) use ($resolver, $key, $toResolve, &$values) : void{ $values[$key] = $value; diff --git a/src/resourcepacks/ResourcePackManager.php b/src/resourcepacks/ResourcePackManager.php index 2df6750de..baf16ca20 100644 --- a/src/resourcepacks/ResourcePackManager.php +++ b/src/resourcepacks/ResourcePackManager.php @@ -25,6 +25,7 @@ namespace pocketmine\resourcepacks; use pocketmine\utils\Config; use pocketmine\utils\Filesystem; +use pocketmine\utils\Utils; use Symfony\Component\Filesystem\Path; use function array_keys; use function copy; @@ -87,7 +88,7 @@ class ResourcePackManager{ throw new \InvalidArgumentException("\"resource_stack\" key should contain a list of pack names"); } - foreach($resourceStack as $pos => $pack){ + foreach(Utils::promoteKeys($resourceStack) as $pos => $pack){ if(!is_string($pack) && !is_int($pack) && !is_float($pack)){ $logger->critical("Found invalid entry in resource pack list at offset $pos of type " . gettype($pack)); continue; diff --git a/src/utils/Utils.php b/src/utils/Utils.php index ef3f2d249..551e6ae44 100644 --- a/src/utils/Utils.php +++ b/src/utils/Utils.php @@ -445,6 +445,7 @@ final class Utils{ * @phpstan-param list> $trace * * @return string[] + * @phpstan-return list */ public static function printableTrace(array $trace, int $maxStringLength = 80) : array{ $messages = []; @@ -456,7 +457,7 @@ final class Utils{ }else{ $args = $trace[$i]["params"]; } - /** @var mixed[] $args */ + /** @phpstan-var array $args */ $paramsList = []; $offset = 0; @@ -608,6 +609,18 @@ final class Utils{ } } + /** + * Gets rid of PHPStan BenevolentUnionType on array keys, so that wrong type errors get reported properly + * Use this if you don't care what the key type is and just want proper PHPStan error reporting + * + * @phpstan-template TValueType + * @phpstan-param array $array + * @phpstan-return array + */ + public static function promoteKeys(array $array) : array{ + return $array; + } + public static function checkUTF8(string $string) : void{ if(!mb_check_encoding($string, 'UTF-8')){ throw new \InvalidArgumentException("Text must be valid UTF-8"); diff --git a/src/world/BlockTransaction.php b/src/world/BlockTransaction.php index 5f7e9f9fa..46cbc7903 100644 --- a/src/world/BlockTransaction.php +++ b/src/world/BlockTransaction.php @@ -28,7 +28,10 @@ use pocketmine\math\Vector3; use pocketmine\utils\Utils; class BlockTransaction{ - /** @var Block[][][] */ + /** + * @var Block[][][] + * @phpstan-var array>> + */ private array $blocks = []; /** diff --git a/src/world/WorldManager.php b/src/world/WorldManager.php index ff603a2df..7ff4ed6e0 100644 --- a/src/world/WorldManager.php +++ b/src/world/WorldManager.php @@ -56,7 +56,10 @@ use function trim; class WorldManager{ public const TICKS_PER_AUTOSAVE = 300 * Server::TARGET_TICKS_PER_SECOND; - /** @var World[] */ + /** + * @var World[] + * @phpstan-var array + */ private array $worlds = []; private ?World $defaultWorld = null; @@ -76,6 +79,7 @@ class WorldManager{ /** * @return World[] + * @phpstan-return array */ public function getWorlds() : array{ return $this->worlds; diff --git a/src/world/format/SubChunk.php b/src/world/format/SubChunk.php index 3f7e943e3..d8546e7e9 100644 --- a/src/world/format/SubChunk.php +++ b/src/world/format/SubChunk.php @@ -24,7 +24,6 @@ declare(strict_types=1); namespace pocketmine\world\format; use function array_map; -use function array_values; use function count; class SubChunk{ @@ -36,6 +35,7 @@ class SubChunk{ * SubChunk constructor. * * @param PalettedBlockArray[] $blockLayers + * @phpstan-param list $blockLayers */ public function __construct( private int $emptyBlockId, @@ -85,6 +85,7 @@ class SubChunk{ /** * @return PalettedBlockArray[] + * @phpstan-return list */ public function getBlockLayers() : array{ return $this->blockLayers; @@ -129,17 +130,18 @@ class SubChunk{ } public function collectGarbage() : void{ - foreach($this->blockLayers as $k => $layer){ + $cleanedLayers = []; + foreach($this->blockLayers as $layer){ $layer->collectGarbage(); foreach($layer->getPalette() as $p){ if($p !== $this->emptyBlockId){ + $cleanedLayers[] = $layer; continue 2; } } - unset($this->blockLayers[$k]); } - $this->blockLayers = array_values($this->blockLayers); + $this->blockLayers = $cleanedLayers; $this->biomes->collectGarbage(); if($this->skyLight !== null && $this->skyLight->isUniform(0)){ diff --git a/src/world/format/io/BaseWorldProvider.php b/src/world/format/io/BaseWorldProvider.php index f863fdf74..79f6875a4 100644 --- a/src/world/format/io/BaseWorldProvider.php +++ b/src/world/format/io/BaseWorldProvider.php @@ -65,6 +65,8 @@ abstract class BaseWorldProvider implements WorldProvider{ abstract protected function loadLevelData() : WorldData; private function translatePalette(PalettedBlockArray $blockArray, \Logger $logger) : PalettedBlockArray{ + //TODO: missing type info in stubs + /** @phpstan-var list $palette */ $palette = $blockArray->getPalette(); $newPalette = []; diff --git a/src/world/format/io/ChunkData.php b/src/world/format/io/ChunkData.php index 458e00196..235ac07bf 100644 --- a/src/world/format/io/ChunkData.php +++ b/src/world/format/io/ChunkData.php @@ -32,6 +32,10 @@ final class ChunkData{ * @param SubChunk[] $subChunks * @param CompoundTag[] $entityNBT * @param CompoundTag[] $tileNBT + * + * @phpstan-param array $subChunks + * @phpstan-param list $entityNBT + * @phpstan-param list $tileNBT */ public function __construct( private array $subChunks, @@ -42,14 +46,21 @@ final class ChunkData{ /** * @return SubChunk[] + * @phpstan-return array */ public function getSubChunks() : array{ return $this->subChunks; } public function isPopulated() : bool{ return $this->populated; } - /** @return CompoundTag[] */ + /** + * @return CompoundTag[] + * @phpstan-return list + */ public function getEntityNBT() : array{ return $this->entityNBT; } - /** @return CompoundTag[] */ + /** + * @return CompoundTag[] + * @phpstan-return list + */ public function getTileNBT() : array{ return $this->tileNBT; } } diff --git a/src/world/format/io/region/RegionGarbageMap.php b/src/world/format/io/region/RegionGarbageMap.php index d1e950452..9e4e232ee 100644 --- a/src/world/format/io/region/RegionGarbageMap.php +++ b/src/world/format/io/region/RegionGarbageMap.php @@ -31,7 +31,10 @@ use const SORT_NUMERIC; final class RegionGarbageMap{ - /** @var RegionLocationTableEntry[] */ + /** + * @var RegionLocationTableEntry[] + * @phpstan-var array + */ private array $entries = []; private bool $clean = false; @@ -48,7 +51,6 @@ final class RegionGarbageMap{ * @param RegionLocationTableEntry[]|null[] $locationTable */ public static function buildFromLocationTable(array $locationTable) : self{ - /** @var RegionLocationTableEntry[] $usedMap */ $usedMap = []; foreach($locationTable as $entry){ if($entry === null){ @@ -62,12 +64,10 @@ final class RegionGarbageMap{ ksort($usedMap, SORT_NUMERIC); - /** @var RegionLocationTableEntry[] $garbageMap */ $garbageMap = []; - /** @var RegionLocationTableEntry|null $prevEntry */ $prevEntry = null; - foreach($usedMap as $firstSector => $entry){ + foreach($usedMap as $entry){ $prevEndPlusOne = ($prevEntry !== null ? $prevEntry->getLastSector() + 1 : RegionLoader::FIRST_SECTOR); $currentStart = $entry->getFirstSector(); if($prevEndPlusOne < $currentStart){ diff --git a/src/world/format/io/region/RegionLoader.php b/src/world/format/io/region/RegionLoader.php index f040b8a47..b65f5346c 100644 --- a/src/world/format/io/region/RegionLoader.php +++ b/src/world/format/io/region/RegionLoader.php @@ -67,7 +67,10 @@ class RegionLoader{ /** @var resource */ protected $filePointer; protected int $nextSector = self::FIRST_SECTOR; - /** @var RegionLocationTableEntry[]|null[] */ + /** + * @var RegionLocationTableEntry[]|null[] + * @phpstan-var list + */ protected array $locationTable = []; protected RegionGarbageMap $garbageTable; public int $lastUsed; @@ -327,7 +330,6 @@ class RegionLoader{ * @throws CorruptedRegionException */ private function checkLocationTableValidity() : void{ - /** @var int[] $usedOffsets */ $usedOffsets = []; $fileSize = filesize($this->filePath); @@ -355,7 +357,7 @@ class RegionLoader{ } ksort($usedOffsets, SORT_NUMERIC); $prevLocationIndex = null; - foreach($usedOffsets as $startOffset => $locationTableIndex){ + foreach($usedOffsets as $locationTableIndex){ if($this->locationTable[$locationTableIndex] === null){ continue; } diff --git a/src/world/format/io/region/RegionWorldProvider.php b/src/world/format/io/region/RegionWorldProvider.php index 1feca61be..75fcfd083 100644 --- a/src/world/format/io/region/RegionWorldProvider.php +++ b/src/world/format/io/region/RegionWorldProvider.php @@ -72,7 +72,10 @@ abstract class RegionWorldProvider extends BaseWorldProvider{ return false; } - /** @var RegionLoader[] */ + /** + * @var RegionLoader[] + * @phpstan-var array + */ protected array $regions = []; protected function loadLevelData() : WorldData{ diff --git a/src/world/generator/PopulationTask.php b/src/world/generator/PopulationTask.php index 8479a79fa..bad134324 100644 --- a/src/world/generator/PopulationTask.php +++ b/src/world/generator/PopulationTask.php @@ -76,7 +76,10 @@ class PopulationTask extends AsyncTask{ $chunk = $this->chunk !== null ? FastChunkSerializer::deserializeTerrain($this->chunk) : null; - /** @var string[] $serialChunks */ + /** + * @var string[] $serialChunks + * @phpstan-var array $serialChunks + */ $serialChunks = igbinary_unserialize($this->adjacentChunks); $chunks = array_map( function(?string $serialized) : ?Chunk{ @@ -92,7 +95,6 @@ class PopulationTask extends AsyncTask{ self::setOrGenerateChunk($manager, $generator, $this->chunkX, $this->chunkZ, $chunk); - /** @var Chunk[] $resultChunks */ $resultChunks = []; //this is just to keep phpstan's type inference happy foreach($chunks as $relativeChunkHash => $c){ World::getXZ($relativeChunkHash, $relativeX, $relativeZ); diff --git a/tests/phpstan/configs/phpstan-bugs.neon b/tests/phpstan/configs/phpstan-bugs.neon index 0fc3defda..02c94d2d6 100644 --- a/tests/phpstan/configs/phpstan-bugs.neon +++ b/tests/phpstan/configs/phpstan-bugs.neon @@ -60,3 +60,8 @@ parameters: count: 2 path: ../../phpunit/promise/PromiseTest.php + - + message: "#^Strict comparison using \\=\\=\\= between 0 and 0 will always evaluate to true\\.$#" + count: 1 + path: ../rules/UnsafeForeachArrayOfStringRule.php + diff --git a/tests/phpstan/rules/UnsafeForeachArrayOfStringRule.php b/tests/phpstan/rules/UnsafeForeachArrayOfStringRule.php index e42d32927..745cf2109 100644 --- a/tests/phpstan/rules/UnsafeForeachArrayOfStringRule.php +++ b/tests/phpstan/rules/UnsafeForeachArrayOfStringRule.php @@ -28,6 +28,7 @@ use PhpParser\Node\Stmt\Foreach_; use PHPStan\Analyser\Scope; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\BenevolentUnionType; use PHPStan\Type\ClassStringType; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; @@ -62,8 +63,17 @@ final class UnsafeForeachArrayOfStringRule implements Rule{ $hasCastableKeyTypes = false; $expectsIntKeyTypes = false; - TypeTraverser::map($iterableType->getIterableKeyType(), function(Type $type, callable $traverse) use (&$hasCastableKeyTypes, &$expectsIntKeyTypes) : Type{ - if($type instanceof IntegerType){ + $implicitType = false; + $benevolentUnionDepth = 0; + TypeTraverser::map($iterableType->getIterableKeyType(), function(Type $type, callable $traverse) use (&$hasCastableKeyTypes, &$expectsIntKeyTypes, &$benevolentUnionDepth, &$implicitType) : Type{ + if($type instanceof BenevolentUnionType){ + $implicitType = true; + $benevolentUnionDepth++; + $result = $traverse($type); + $benevolentUnionDepth--; + return $result; + } + if($type instanceof IntegerType && $benevolentUnionDepth === 0){ $expectsIntKeyTypes = true; return $type; } @@ -78,12 +88,20 @@ final class UnsafeForeachArrayOfStringRule implements Rule{ return $type; }); if($hasCastableKeyTypes && !$expectsIntKeyTypes){ - $func = Utils::stringifyKeys(...); + $tip = $implicitType ? + sprintf( + "Declare a key type using @phpstan-var or @phpstan-param, or use %s() to promote the key type to get proper error reporting", + Utils::getNiceClosureName(Utils::promoteKeys(...)) + ) : + sprintf( + "Use %s() to get a \Generator that will force the keys to string", + Utils::getNiceClosureName(Utils::stringifyKeys(...)), + ); return [ RuleErrorBuilder::message(sprintf( "Unsafe foreach on array with key type %s (they might be casted to int).", $iterableType->getIterableKeyType()->describe(VerbosityLevel::value()) - ))->tip(sprintf("Use %s() for a safe Generator-based iterator.", Utils::getNiceClosureName($func)))->build() + ))->tip($tip)->build() ]; } return []; diff --git a/tests/phpunit/block/BlockTest.php b/tests/phpunit/block/BlockTest.php index 6ade2bffe..841917787 100644 --- a/tests/phpunit/block/BlockTest.php +++ b/tests/phpunit/block/BlockTest.php @@ -135,7 +135,7 @@ class BlockTest extends TestCase{ } $errors = []; - foreach($expected as $typeName => $numStates){ + foreach(Utils::promoteKeys($expected) as $typeName => $numStates){ if(!is_string($typeName) || !is_int($numStates)){ throw new AssumptionFailedError("Old table should be array"); } diff --git a/tests/phpunit/item/ItemTest.php b/tests/phpunit/item/ItemTest.php index 05cb48b30..36ca5c5ff 100644 --- a/tests/phpunit/item/ItemTest.php +++ b/tests/phpunit/item/ItemTest.php @@ -89,7 +89,6 @@ class ItemTest extends TestCase{ } public function testGetEnchantments() : void{ - /** @var EnchantmentInstance[] $enchantments */ $enchantments = [ new EnchantmentInstance(VanillaEnchantments::EFFICIENCY(), 5), new EnchantmentInstance(VanillaEnchantments::SHARPNESS(), 1) diff --git a/tests/phpunit/utils/CloningRegistryTraitTest.php b/tests/phpunit/utils/CloningRegistryTraitTest.php index 7f8298ff9..e3b53ecb5 100644 --- a/tests/phpunit/utils/CloningRegistryTraitTest.php +++ b/tests/phpunit/utils/CloningRegistryTraitTest.php @@ -47,7 +47,7 @@ final class CloningRegistryTraitTest extends TestCase{ public function testGetAllClone() : void{ $list1 = TestCloningRegistry::getAll(); $list2 = TestCloningRegistry::getAll(); - foreach($list1 as $k => $member){ + foreach(Utils::promoteKeys($list1) as $k => $member){ self::assertNotSame($member, $list2[$k], "VanillaBlocks ought to clone its members"); } } diff --git a/tests/phpunit/utils/TestCloningRegistry.php b/tests/phpunit/utils/TestCloningRegistry.php index ade94d461..d65b8abaa 100644 --- a/tests/phpunit/utils/TestCloningRegistry.php +++ b/tests/phpunit/utils/TestCloningRegistry.php @@ -37,9 +37,13 @@ final class TestCloningRegistry{ /** * @return \stdClass[] + * @phpstan-return array */ public static function getAll() : array{ - /** @var \stdClass[] $result */ + /** + * @var \stdClass[] $result + * @phpstan-var array $result + */ $result = self::_registryGetAll(); return $result; } diff --git a/tools/compact-regions.php b/tools/compact-regions.php index 6959c82fe..04ac3f0c9 100644 --- a/tools/compact-regions.php +++ b/tools/compact-regions.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace pocketmine\tools\compact_regions; +use pocketmine\utils\Utils; use pocketmine\world\format\io\exception\CorruptedChunkException; use pocketmine\world\format\io\region\CorruptedRegionException; use pocketmine\world\format\io\region\RegionLoader; @@ -59,6 +60,7 @@ const SUPPORTED_EXTENSIONS = [ /** * @param int[] $files * @phpstan-param array $files + * @phpstan-param-out array $files */ function find_regions_recursive(string $dir, array &$files) : void{ $dirFiles = scandir($dir, SCANDIR_SORT_NONE); @@ -112,7 +114,7 @@ function main(array $argv) : int{ $corruptedFiles = []; $doneCount = 0; $totalCount = count($files); - foreach($files as $file => $size){ + foreach(Utils::stringifyKeys($files) as $file => $size){ try{ $oldRegion = RegionLoader::loadExisting($file); }catch(CorruptedRegionException $e){ @@ -162,7 +164,7 @@ function main(array $argv) : int{ clearstatcache(); $newSize = 0; - foreach($files as $file => $oldSize){ + foreach(Utils::stringifyKeys($files) as $file => $oldSize){ $newSize += file_exists($file) ? filesize($file) : 0; } $diff = $currentSize - $newSize; diff --git a/tools/generate-bedrock-data-from-packets.php b/tools/generate-bedrock-data-from-packets.php index 0cb5ac366..2c20e6099 100644 --- a/tools/generate-bedrock-data-from-packets.php +++ b/tools/generate-bedrock-data-from-packets.php @@ -198,12 +198,12 @@ class ParserPacketHandler extends PacketHandler{ $result = (array) ($object instanceof \JsonSerializable ? $object->jsonSerialize() : $object); ksort($result, SORT_STRING); - foreach($result as $property => $value){ + foreach(Utils::promoteKeys($result) as $property => $value){ if(is_object($value)){ $result[$property] = self::objectToOrderedArray($value); }elseif(is_array($value)){ $array = []; - foreach($value as $k => $v){ + foreach(Utils::promoteKeys($value) as $k => $v){ if(is_object($v)){ $array[$k] = self::objectToOrderedArray($v); }else{ @@ -224,7 +224,7 @@ class ParserPacketHandler extends PacketHandler{ } if(is_array($object)){ $result = []; - foreach($object as $k => $v){ + foreach(Utils::promoteKeys($object) as $k => $v){ $result[$k] = self::sort($v); } return $result; @@ -247,7 +247,7 @@ class ParserPacketHandler extends PacketHandler{ ksort($table, SORT_STRING); file_put_contents($this->bedrockDataPath . '/required_item_list.json', json_encode($table, JSON_PRETTY_PRINT) . "\n"); - foreach($packet->levelSettings->experiments->getExperiments() as $name => $experiment){ + foreach(Utils::promoteKeys($packet->levelSettings->experiments->getExperiments()) as $name => $experiment){ echo "Experiment \"$name\" is " . ($experiment ? "" : "not ") . "active\n"; } return true; @@ -317,8 +317,8 @@ class ParserPacketHandler extends PacketHandler{ $char = ord("A"); $outputsByKey = []; - foreach($entry->getInput() as $x => $row){ - foreach($row as $y => $ingredient){ + foreach(Utils::promoteKeys($entry->getInput()) as $x => $row){ + foreach(Utils::promoteKeys($row) as $y => $ingredient){ if($ingredient->getDescriptor() === null){ $shape[$x][$y] = " "; }else{ @@ -337,7 +337,7 @@ class ParserPacketHandler extends PacketHandler{ } $unlockingIngredients = $entry->getUnlockingRequirement()->getUnlockingIngredients(); return new ShapedRecipeData( - array_map(fn(array $array) => implode('', $array), $shape), + array_map(fn(array $array) => implode('', array_values($array)), array_values($shape)), $outputsByKey, array_map(fn(ItemStack $output) => $this->itemStackToJson($output), $entry->getOutput()), $entry->getBlockName(), From a9787f0d993ab2f9e6bac29649f732fe933099ce Mon Sep 17 00:00:00 2001 From: "Dylan K. Taylor" Date: Mon, 25 Nov 2024 14:32:17 +0000 Subject: [PATCH 2/2] Fix PHPStan error --- src/utils/Utils.php | 2 +- tests/phpstan/configs/actual-problems.neon | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/utils/Utils.php b/src/utils/Utils.php index 551e6ae44..38f523ad4 100644 --- a/src/utils/Utils.php +++ b/src/utils/Utils.php @@ -403,7 +403,7 @@ final class Utils{ } /** - * @param mixed[] $trace + * @param mixed[][] $trace * @return string[] */ public static function printableExceptionInfo(\Throwable $e, $trace = null) : array{ diff --git a/tests/phpstan/configs/actual-problems.neon b/tests/phpstan/configs/actual-problems.neon index e778cf004..54028f0dd 100644 --- a/tests/phpstan/configs/actual-problems.neon +++ b/tests/phpstan/configs/actual-problems.neon @@ -840,11 +840,6 @@ parameters: count: 1 path: ../../../src/utils/Utils.php - - - message: "#^Parameter \\#1 \\$trace of static method pocketmine\\\\utils\\\\Utils\\:\\:printableTrace\\(\\) expects array\\\\>, array given\\.$#" - count: 1 - path: ../../../src/utils/Utils.php - - message: "#^Parameter \\#2 \\$file of class pocketmine\\\\thread\\\\ThreadCrashInfoFrame constructor expects string\\|null, mixed given\\.$#" count: 1