Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/Type/Php/ArrayFirstDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;

#[AutowiredService]
final class ArrayFirstDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'array_first' && $functionReflection->isBuiltin();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is the correct way. Maybe we can check the PHP version. Because I thought there might be userland functions named array_first that would also be processed here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's okay like this, we don't check this anywhere for similar extensions. As a benefit it'd also work for polyfills running on lower PHP versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So should I remove the $functionReflection->isBuiltin() part?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes 😊

}

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
{
$args = $functionCall->getArgs();

if (count($args) < 1) {
return null;
}

$argType = $scope->getType($args[0]->value);
$iterableAtLeastOnce = $argType->isIterableAtLeastOnce();

if ($iterableAtLeastOnce->no()) {
return new NullType();
}

$valueType = $argType->getFirstIterableValueType();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I thought about the implementation, I'd do only getIterableValueType. The getFirstIterable*Type methods are highly misleading and we should get rid of it. Because array shapes do not enforce the order of the keys. Sorry to put more burden on you, but can you please deprecate Type::getFirstIterableKeyType, getLastIterableKeyType, getFirstIterableValueType, getLastIterableValueType and use getIterableKeyType and getIterableValueType in all places instead? And just update any test assertions because of that. Thank you!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright. I can do that. But we will lose the precise type for constant arrays that way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's okay. Ideally we would have two separate implementations for literal arrays and PHPDoc array shapes but we don't.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done with 73651b5 and 5f3237a

I didn't really check the assertions, just fixed whatever PHPStan said.


if ($iterableAtLeastOnce->yes()) {
return $valueType;
}

return TypeCombinator::union($valueType, new NullType());
}

}
48 changes: 48 additions & 0 deletions src/Type/Php/ArrayLastDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;

#[AutowiredService]
final class ArrayLastDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as this 2 extensions are very similar, you might consider using a single extension class for handling both

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the recent refactoring they are now merged into one

{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'array_last' && $functionReflection->isBuiltin();
}

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
{
$args = $functionCall->getArgs();

if (count($args) < 1) {
return null;
}

$argType = $scope->getType($args[0]->value);
$iterableAtLeastOnce = $argType->isIterableAtLeastOnce();

if ($iterableAtLeastOnce->no()) {
return new NullType();
}

$valueType = $argType->getLastIterableValueType();

if ($iterableAtLeastOnce->yes()) {
return $valueType;
}

return TypeCombinator::union($valueType, new NullType());
}

}
22 changes: 22 additions & 0 deletions tests/PHPStan/Analyser/nsrt/array_first_last.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php // lint >= 8.5

namespace ArrayFirstLast;

use function PHPStan\Testing\assertType;

/**
* @param string[] $stringArray
* @param non-empty-array<int, string> $nonEmptyArray
*/
function doFoo(array $stringArray, array $nonEmptyArray, $mixed): void
{
assertType("'a'", array_first([1 => 'a', 0 => 'b', 2 => 'c']));
assertType('string|null', array_first($stringArray));
assertType('string', array_first($nonEmptyArray));
assertType('mixed', array_first($mixed));

assertType("'c'", array_last([1 => 'a', 0 => 'b', 2 => 'c']));
assertType('string|null', array_last($stringArray));
assertType('string', array_last($nonEmptyArray));
assertType('mixed', array_last($mixed));
}
Loading