*/ final class UnsafeForeachArrayWithStringKeysRule implements Rule{ public function getNodeType() : string{ return Foreach_::class; } public function processNode(Node $node, Scope $scope) : array{ /** @var Foreach_ $node */ if($node->keyVar === null){ return []; } $iterableType = $scope->getType($node->expr); if($iterableType->isArray()->no()){ return []; } if($iterableType->isIterableAtLeastOnce()->no()){ return []; } $hasCastableKeyTypes = false; $expectsIntKeyTypes = false; $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; } if(!$type instanceof StringType){ return $traverse($type); } if($type->isNumericString()->no() || $type instanceof ClassStringType){ //class-string cannot be numeric, even if PHPStan thinks they can be return $type; } $hasCastableKeyTypes = true; return $type; }); if($hasCastableKeyTypes && !$expectsIntKeyTypes){ $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($tip)->identifier('pocketmine.foreach.stringKeys')->build() ]; } return []; } }