diff --git a/resources/functionMap.php b/resources/functionMap.php index 9d23945857..4c5654b880 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -2932,7 +2932,7 @@ 'ffmpeg_movie::hasAudio' => ['bool'], 'ffmpeg_movie::hasVideo' => ['bool'], 'fgetc' => ['string|false', 'fp'=>'resource'], -'fgetcsv' => ['list|array{0: null}|false|null', 'fp'=>'resource', 'length='=>'0|positive-int|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'fgetcsv' => ['list|list{null}|false|null', 'fp'=>'resource', 'length='=>'0|positive-int|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'fgets' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int'], 'fgetss' => ['string|false', 'fp'=>'resource', 'length='=>'0|positive-int', 'allowable_tags='=>'string'], 'file' => ['list|false', 'filename'=>'string', 'flags='=>'int-mask', 'context='=>'resource'], @@ -4534,8 +4534,8 @@ 'image_type_to_mime_type' => ['string', 'imagetype'=>'int'], 'imageaffine' => ['resource|false', 'src'=>'resource', 'affine'=>'array', 'clip='=>'array'], 'imageaffineconcat' => ['array', 'm1'=>'array', 'm2'=>'array'], -'imageaffinematrixconcat' => ['array{0:float,1:float,2:float,3:float,4:float,5:float}|false', 'm1'=>'array', 'm2'=>'array'], -'imageaffinematrixget' => ['array{0:float,1:float,2:float,3:float,4:float,5:float}|false', 'type'=>'int', 'options'=>'array|float'], +'imageaffinematrixconcat' => ['list{float,float,float,float,float,float}|false', 'm1'=>'array', 'm2'=>'array'], +'imageaffinematrixget' => ['list{float,float,float,float,float,float}|false', 'type'=>'int', 'options'=>'array|float'], 'imagealphablending' => ['bool', 'im'=>'resource', 'on'=>'bool'], 'imageantialias' => ['bool', 'im'=>'resource', 'on'=>'bool'], 'imagearc' => ['bool', 'im'=>'resource', 'cx'=>'int', 'cy'=>'int', 'w'=>'int', 'h'=>'int', 's'=>'int', 'e'=>'int', 'col'=>'int'], @@ -4682,9 +4682,9 @@ 'Imagick::colorMatrixImage' => ['bool', 'color_matrix'=>'array'], 'Imagick::combineImages' => ['Imagick', 'channeltype'=>'Imagick::CHANNEL_*'], 'Imagick::commentImage' => ['bool', 'comment'=>'string'], -'Imagick::compareImageChannels' => ['array{Imagick,float}', 'image'=>'imagick', 'channeltype'=>'Imagick::CHANNEL_*', 'metrictype'=>'Imagick::METRIC_*'], +'Imagick::compareImageChannels' => ['list{Imagick,float}', 'image'=>'imagick', 'channeltype'=>'Imagick::CHANNEL_*', 'metrictype'=>'Imagick::METRIC_*'], 'Imagick::compareImageLayers' => ['Imagick', 'method'=>'Imagick::LAYERMETHOD_*'], -'Imagick::compareImages' => ['array{Imagick,float}', 'compare'=>'imagick', 'metric'=>'Imagick::METRIC_*'], +'Imagick::compareImages' => ['list{Imagick,float}', 'compare'=>'imagick', 'metric'=>'Imagick::METRIC_*'], 'Imagick::compositeImage' => ['bool', 'composite_object'=>'imagick', 'composite'=>'Imagick::COMPOSITE_*', 'x'=>'int', 'y'=>'int', 'channel='=>'Imagick::CHANNEL_*'], 'Imagick::compositeImageGravity' => ['bool', 'imagick'=>'Imagick', 'COMPOSITE_CONSTANT'=>'int', 'GRAVITY_CONSTANT'=>'int'], 'Imagick::contrastImage' => ['bool', 'sharpen'=>'bool'], @@ -9543,7 +9543,7 @@ 'Redis::geoadd' => ['__benevolent', 'key'=>'string', 'lng'=>'float', 'lat'=>'float', 'member'=>'string', '...other_triples_and_options='=>'mixed'], 'Redis::geodist' => ['__benevolent', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], 'Redis::geohash' => ['__benevolent|false>', 'key'=>'string', 'member'=>'string', '...other_members='=>'string'], -'Redis::geopos' => ['__benevolent|false>', 'key'=>'string', 'member'=>'string', '...other_members'=>'string'], +'Redis::geopos' => ['__benevolent|false>', 'key'=>'string', 'member'=>'string', '...other_members'=>'string'], 'Redis::georadius' => ['__benevolent>', 'key'=>'string', 'lng'=>'float', 'lat'=>'float', 'radius'=>'float', 'unit'=>'string', 'options='=>'array'], 'Redis::georadiusbymember' => ['__benevolent>', 'key'=>'string', 'lng'=>'float', 'lat'=>'float', 'radius'=>'float', 'unit'=>'string', 'options='=>'array'], 'Redis::georadiusbymember_ro' => ['__benevolent>', 'key'=>'string', 'lng'=>'float', 'lat'=>'float', 'radius'=>'float', 'unit'=>'string', 'options='=>'array'], @@ -9567,7 +9567,7 @@ 'Redis::getReadTimeout' => ['float'], 'Redis::getset' => ['__benevolent', 'key'=>'string', 'value'=>'mixed'], 'Redis::getTimeout' => ['float|false'], -'Redis::getTransferredBytes' => ['array'], +'Redis::getTransferredBytes' => ['array'], 'Redis::hDel' => ['__benevolent', 'key'=>'string', 'field'=>'string', '...other_fields='=>'string'], 'Redis::hExists' => ['__benevolent', 'key'=>'string', 'field'=>'string'], 'Redis::hGet' => ['__benevolent', 'key'=>'string', 'member'=>'string'], @@ -9693,7 +9693,7 @@ 'Redis::sUnionStore' => ['__benevolent', 'dst'=>'string', 'key'=>'string', '...other_keys='=>'string'], 'Redis::sunsubscribe' => ['__benevolent', 'channels'=>'string[]'], 'Redis::swapdb' => ['__benevolent', 'src'=>'int', 'dst'=>'int'], -'Redis::time' => ['__benevolent'], +'Redis::time' => ['__benevolent'], 'Redis::ttl' => ['__benevolent', 'key'=>'string'], 'Redis::type' => ['__benevolent', 'key'=>'string'], 'Redis::unlink' => ['__benevolent', 'key'=>'string[]|string', '...other_keys'=>'string'], @@ -11575,7 +11575,7 @@ 'SplFileObject::fflush' => ['bool'], 'SplFileObject::fgetc' => ['string|false'], // Do not believe https://www.php.net/manual/en/splfileobject.fgetcsv#refsect1-splfileobject.fgetcsv-returnvalues -'SplFileObject::fgetcsv' => ['list|array{0: null}|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], +'SplFileObject::fgetcsv' => ['list|list{null}|false|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'SplFileObject::fgets' => ['string'], 'SplFileObject::fgetss' => ['string|false', 'allowable_tags='=>'string'], 'SplFileObject::flock' => ['bool', 'operation'=>'int-mask', '&w_wouldblock='=>'0|1'], @@ -12602,7 +12602,7 @@ 'timezone_transitions_get' => ['list|false', 'object'=>'DateTimeZone', 'timestamp_begin='=>'int', 'timestamp_end='=>'int'], 'timezone_version_get' => ['string'], 'tmpfile' => ['__benevolent'], -'token_get_all' => ['list', 'source'=>'string', 'flags='=>'int'], +'token_get_all' => ['list', 'source'=>'string', 'flags='=>'int'], 'token_name' => ['non-falsy-string', 'type'=>'int'], 'TokyoTyrant::__construct' => ['void', 'host='=>'string', 'port='=>'int', 'options='=>'array'], 'TokyoTyrant::add' => ['int|float', 'key'=>'string', 'increment'=>'float', 'type='=>'int'], diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php index bca5fef36b..13d40f9fd8 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -46,7 +46,7 @@ 'error_log' => ['bool', 'message'=>'string', 'message_type='=>'0|1|3|4', 'destination='=>'string', 'extra_headers='=>'string'], 'explode' => ['list', 'separator'=>'non-empty-string', 'str'=>'string', 'limit='=>'int'], 'fdiv' => ['float', 'dividend'=>'float', 'divisor'=>'float'], - 'fgetcsv' => ['list|array{0: null}|false', 'fp'=>'resource', 'length='=>'0|positive-int|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], + 'fgetcsv' => ['list|list{null}|false', 'fp'=>'resource', 'length='=>'0|positive-int|null', 'delimiter='=>'string', 'enclosure='=>'string', 'escape='=>'string'], 'filter_input' => ['mixed', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'variable_name'=>'string', 'filter='=>'int', 'options='=>'array|int'], 'filter_input_array' => ['array|false|null', 'type'=>'INPUT_GET|INPUT_POST|INPUT_COOKIE|INPUT_SERVER|INPUT_ENV', 'definition='=>'int|array', 'add_empty='=>'bool'], 'floor' => ['float', 'number'=>'float'], diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index 6abb943d73..7297dc6518 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -1042,7 +1042,21 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $nameScope): Type { + $isListShape = in_array($typeNode->kind, [ + ArrayShapeNode::KIND_LIST, + ArrayShapeNode::KIND_NON_EMPTY_LIST, + ], true); + $builder = ConstantArrayTypeBuilder::createEmpty(); + if (!$isListShape) { + foreach ($typeNode->items as $itemNode) { + if ($itemNode->keyName !== null) { + $builder = ConstantArrayTypeBuilder::createEmptyIndeterminate(); + break; + } + } + } + if (count($typeNode->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) { $builder->degradeToGeneralArray(true); } @@ -1062,10 +1076,7 @@ private function resolveArrayShapeNode(ArrayShapeNode $typeNode, NameScope $name } $arrayType = $builder->getArray(); - if (in_array($typeNode->kind, [ - ArrayShapeNode::KIND_LIST, - ArrayShapeNode::KIND_NON_EMPTY_LIST, - ], true)) { + if ($isListShape) { $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType()); } diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index a639bf6c0e..c922cd056c 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -55,6 +55,11 @@ public static function createEmpty(): self return new self([], [], [0], [], TrinaryLogic::createYes()); } + public static function createEmptyIndeterminate(): self + { + return new self([], [], [0], [], TrinaryLogic::createMaybe()); + } + public static function createFromConstantArray(ConstantArrayType $startArrayType): self { $builder = new self( diff --git a/tests/PHPStan/Analyser/nsrt/list-shapes.php b/tests/PHPStan/Analyser/nsrt/list-shapes.php index 62313ca8e7..87800183f6 100644 --- a/tests/PHPStan/Analyser/nsrt/list-shapes.php +++ b/tests/PHPStan/Analyser/nsrt/list-shapes.php @@ -15,12 +15,11 @@ class Foo */ public function bar($l1, $l2, $l3, $l4, $l5, $l6): void { - // TODO: list{} - assertType('array{}', $l1); - assertType("array{'a'}", $l2); - assertType("array{'a'}", $l3); - assertType("array{'a', 'b'}", $l4); - assertType("array{0: 'a', 1?: 'b'}", $l5); - assertType("array{'a', 'b'}", $l6); + assertType('list{}', $l1); + assertType("list{'a'}", $l2); + assertType("list{'a'}", $l3); + assertType("list{'a', 'b'}", $l4); + assertType("list{0: 'a', 1?: 'b'}", $l5); + assertType("list{'a', 'b'}", $l6); } }