Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
17 changes: 17 additions & 0 deletions src/Analyser/ArgumentsNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,23 @@ public static function reorderNewArguments(
);
}

/**
* The argument bound to the parameter at $position named $name: the
* positional argument at that index or the one passed by that name.
*
* @param Arg[] $args
*/
public static function getArgByPositionOrName(array $args, int $position, string $name): ?Arg

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we miss some unit tests for this method

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I added the unit tests for ArgumentsNormalizer::getArgByPositionOrName(). Running them with the turbo extension loaded turned up a difference between the PHP method and its C++ mirror, which I fixed. Both commits are pushed.

Tests (6f45d0882, tests/PHPStan/Analyser/ArgumentsNormalizerTest.php): testGetArgByPositionOrName takes its cases from a data provider:

  • no arguments;
  • a positional argument at the position, and too few positional arguments;
  • a named argument in signature order, and in another order (callback:, array: and mode:, callback:, array:);
  • positional arguments followed by the named one;
  • a named argument sitting at the position but belonging to a different parameter, which must return null;
  • argument names are case-sensitive;
  • a negative position.

Fix (c2e266143, turbo-ext/src/ArgumentsNormalizer.cpp): with a negative position, the native binding returned null straight away. The PHP version still finds an argument passed by name in that case, for example ([x, array: y], -1, 'array') returns y. I removed the early return. The C++ now compares the array key the same way PHP's $i === $position does, so both give the same result.

Checks:

  • With the extension loaded, the new tests now pass, as do tests/smoke.php and tests/signature-parity.php.
  • make tests passes (23417 tests, 107 skipped), and so do make phpstan and make cs.

On the broader point about repeating the parameter-position and argument handling from FunctionCallParametersCheck: I left that alone. Merging it into one shared helper would be a refactor beyond this PR.

{
foreach ($args as $i => $arg) {
if ($arg->name === null ? $i === $position : $arg->name->toString() === $name) {
return $arg;
}
}

return null;
}

/**
* @param Arg[] $callArgs
* @return ?list<Arg>
Expand Down
7 changes: 4 additions & 3 deletions src/Parser/ArrayFilterArgVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Override;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Turbo\ShadowedByTurboExtension;

Expand All @@ -21,9 +22,9 @@ public function enterNode(Node $node): ?Node
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) {
$functionName = $node->name->toLowerString();
if ($functionName === 'array_filter') {
$args = $node->getArgs();
if (isset($args[0])) {
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
$arg = ArgumentsNormalizer::getArgByPositionOrName($node->getArgs(), 0, 'array');
if ($arg !== null) {
$arg->setAttribute(self::ATTRIBUTE_NAME, true);
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/Parser/ArrayFindArgVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Override;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Turbo\ShadowedByTurboExtension;
use function in_array;
Expand All @@ -22,9 +23,9 @@ public function enterNode(Node $node): ?Node
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) {
$functionName = $node->name->toLowerString();
if (in_array($functionName, ['array_all', 'array_any', 'array_find', 'array_find_key'], true)) {
$args = $node->getArgs();
if (isset($args[0])) {
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
$arg = ArgumentsNormalizer::getArgByPositionOrName($node->getArgs(), 0, 'array');
if ($arg !== null) {
$arg->setAttribute(self::ATTRIBUTE_NAME, true);
}
}
}
Expand Down
10 changes: 4 additions & 6 deletions src/Parser/ArrayMapArgVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
use Override;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Turbo\ShadowedByTurboExtension;
use function array_slice;
use function count;

#[AutowiredService]
Expand Down Expand Up @@ -35,11 +35,9 @@ public function enterNode(Node $node): ?Node

$arrayArgs[] = $arg;
}
if (isset($args[0])) {
$slicedArgs = array_slice($args, 1);
if (count($slicedArgs) > 0) {
$args[0]->value->setAttribute(self::ATTRIBUTE_NAME, $arrayArgs);
}
$callbackArg = ArgumentsNormalizer::getArgByPositionOrName($args, 0, 'callback');
if ($callbackArg !== null && count($arrayArgs) > 0) {
$callbackArg->value->setAttribute(self::ATTRIBUTE_NAME, $arrayArgs);
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/Parser/ArrayWalkArgVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Override;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Turbo\ShadowedByTurboExtension;

Expand All @@ -21,9 +22,9 @@ public function enterNode(Node $node): ?Node
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) {
$functionName = $node->name->toLowerString();
if ($functionName === 'array_walk') {
$args = $node->getArgs();
if (isset($args[0])) {
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
$arg = ArgumentsNormalizer::getArgByPositionOrName($node->getArgs(), 0, 'array');
if ($arg !== null) {
$arg->setAttribute(self::ATTRIBUTE_NAME, true);
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/Parser/CurlSetOptArgVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Override;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Turbo\ShadowedByTurboExtension;

Expand All @@ -21,9 +22,9 @@ public function enterNode(Node $node): ?Node
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) {
$functionName = $node->name->toLowerString();
if ($functionName === 'curl_setopt') {
$args = $node->getArgs();
if (isset($args[0])) {
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
$arg = ArgumentsNormalizer::getArgByPositionOrName($node->getArgs(), 0, 'handle');
if ($arg !== null) {
$arg->setAttribute(self::ATTRIBUTE_NAME, true);
}
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/Parser/CurlSetOptArrayArgVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Override;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Turbo\ShadowedByTurboExtension;

Expand All @@ -21,9 +22,9 @@ public function enterNode(Node $node): ?Node
if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) {
$functionName = $node->name->toLowerString();
if ($functionName === 'curl_setopt_array') {
$args = $node->getArgs();
if (isset($args[1])) {
$args[1]->setAttribute(self::ATTRIBUTE_NAME, true);
$arg = ArgumentsNormalizer::getArgByPositionOrName($node->getArgs(), 1, 'options');
if ($arg !== null) {
$arg->setAttribute(self::ATTRIBUTE_NAME, true);
}
}
}
Expand Down
76 changes: 51 additions & 25 deletions src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,31 @@ public static function selectFromArgs(
return self::selectFromTypes($types, $parametersAcceptors, $unpack);
}

/**
* The intrinsic overrides read sibling arguments by their parameter
* position, so named arguments are put in signature order first.
*
* @param Node\Arg[] $args
* @return Node\Arg[]
*/
private static function reorderIntrinsicArgs(array $args, ParametersAcceptor $parametersAcceptor): array
{
foreach ($args as $arg) {
if ($arg->name === null) {
continue;
}

$reorderedArgs = ArgumentsNormalizer::reorderArgs($parametersAcceptor, array_values($args));
if ($reorderedArgs === null || count($reorderedArgs) === 0) {
return $args;
}

return $reorderedArgs;
}

return $args;
}

/**
* @internal
* @param Node\Arg[] $args
Expand All @@ -204,7 +229,8 @@ public static function applyIntrinsicArgOverrides(
count($args) > 0
&& count($parametersAcceptors) > 0
) {
$arrayMapArgs = $args[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME);
$positionalArgs = self::reorderIntrinsicArgs($args, $namedArgumentsVariants[0] ?? $parametersAcceptors[0]);
$arrayMapArgs = $positionalArgs[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME);
if ($arrayMapArgs !== null) {
$callbackParameters = [];
$nativeCallbackParameters = [];
Expand Down Expand Up @@ -252,8 +278,8 @@ public static function applyIntrinsicArgOverrides(
}
}

if (count($args) >= 3 && (bool) $args[0]->getAttribute(CurlSetOptArgVisitor::ATTRIBUTE_NAME)) {
$optType = ($typeGetter)($args[1]->value);
if (count($positionalArgs) >= 3 && (bool) $positionalArgs[0]->getAttribute(CurlSetOptArgVisitor::ATTRIBUTE_NAME)) {
$optType = ($typeGetter)($positionalArgs[1]->value);

$valueTypes = [];
foreach ($optType->getConstantScalarValues() as $scalarValue) {
Expand Down Expand Up @@ -296,8 +322,8 @@ public static function applyIntrinsicArgOverrides(
}
}

if (count($args) >= 2 && (bool) $args[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME)) {
$optArrayType = ($typeGetter)($args[1]->value);
if (count($positionalArgs) >= 2 && (bool) $positionalArgs[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME)) {
$optArrayType = ($typeGetter)($positionalArgs[1]->value);

$hasTypes = false;
$builder = ConstantArrayTypeBuilder::createEmpty();
Expand Down Expand Up @@ -348,27 +374,27 @@ public static function applyIntrinsicArgOverrides(
}
}

if ((bool) $args[0]->getAttribute(ArrayFilterArgVisitor::ATTRIBUTE_NAME)) {
if ((bool) $positionalArgs[0]->getAttribute(ArrayFilterArgVisitor::ATTRIBUTE_NAME)) {
$arrayFilterParameters = null;
$nativeArrayFilterParameters = null;
if (isset($args[2])) {
$mode = ($typeGetter)($args[2]->value);
if (isset($positionalArgs[2])) {
$mode = ($typeGetter)($positionalArgs[2]->value);
if ($mode instanceof ConstantIntegerType) {
if ($mode->getValue() === ARRAY_FILTER_USE_KEY) {
$arrayFilterParameters = [
new DummyParameter('key', ($iterableKeyTypeGetter)(($typeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
new DummyParameter('key', ($iterableKeyTypeGetter)(($typeGetter)($positionalArgs[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
];
$nativeArrayFilterParameters = [
new DummyParameter('key', ($iterableKeyTypeGetter)(($nativeTypeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
new DummyParameter('key', ($iterableKeyTypeGetter)(($nativeTypeGetter)($positionalArgs[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
];
} elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) {
$arrayFilterParameters = [
new DummyParameter('item', ($iterableValueTypeGetter)(($typeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
new DummyParameter('key', ($iterableKeyTypeGetter)(($typeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
new DummyParameter('item', ($iterableValueTypeGetter)(($typeGetter)($positionalArgs[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
new DummyParameter('key', ($iterableKeyTypeGetter)(($typeGetter)($positionalArgs[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
];
$nativeArrayFilterParameters = [
new DummyParameter('item', ($iterableValueTypeGetter)(($nativeTypeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
new DummyParameter('key', ($iterableKeyTypeGetter)(($nativeTypeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
new DummyParameter('item', ($iterableValueTypeGetter)(($nativeTypeGetter)($positionalArgs[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
new DummyParameter('key', ($iterableKeyTypeGetter)(($nativeTypeGetter)($positionalArgs[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
];
}
}
Expand All @@ -377,7 +403,7 @@ public static function applyIntrinsicArgOverrides(
$acceptor = $parametersAcceptors[0];
$parameters = $acceptor->getParameters();
if (isset($parameters[1])) {
$arrayArgType = ($typeGetter)($args[0]->value);
$arrayArgType = ($typeGetter)($positionalArgs[0]->value);
$callableType = new UnionType([
new CallableType(
$arrayFilterParameters ?? [
Expand All @@ -388,7 +414,7 @@ public static function applyIntrinsicArgOverrides(
),
new NullType(),
]);
$nativeArrayArgType = ($nativeTypeGetter)($args[0]->value);
$nativeArrayArgType = ($nativeTypeGetter)($positionalArgs[0]->value);
$nativeCallableType = new UnionType([
new CallableType(
$nativeArrayFilterParameters ?? [
Expand Down Expand Up @@ -433,9 +459,9 @@ public static function applyIntrinsicArgOverrides(
];
}

if ((bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) {
$arrayArgType = ($typeGetter)($args[0]->value);
$nativeArrayArgType = ($nativeTypeGetter)($args[0]->value);
if ((bool) $positionalArgs[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) {
$arrayArgType = ($typeGetter)($positionalArgs[0]->value);
$nativeArrayArgType = ($nativeTypeGetter)($positionalArgs[0]->value);
$arrayWalkParameters = [
new DummyParameter('item', ($iterableValueTypeGetter)($arrayArgType), optional: false, passedByReference: PassedByReference::createReadsArgument(), variadic: false, defaultValue: null),
new DummyParameter('key', ($iterableKeyTypeGetter)($arrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
Expand All @@ -444,9 +470,9 @@ public static function applyIntrinsicArgOverrides(
new DummyParameter('item', ($iterableValueTypeGetter)($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createReadsArgument(), variadic: false, defaultValue: null),
new DummyParameter('key', ($iterableKeyTypeGetter)($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
];
if (isset($args[2])) {
$arrayWalkParameters[] = new DummyParameter('arg', ($typeGetter)($args[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
$nativeArrayWalkParameters[] = new DummyParameter('arg', ($nativeTypeGetter)($args[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
if (isset($positionalArgs[2])) {
$arrayWalkParameters[] = new DummyParameter('arg', ($typeGetter)($positionalArgs[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
$nativeArrayWalkParameters[] = new DummyParameter('arg', ($nativeTypeGetter)($positionalArgs[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
}

$acceptor = $parametersAcceptors[0];
Expand All @@ -459,11 +485,11 @@ public static function applyIntrinsicArgOverrides(
}
}

if ((bool) $args[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) {
if ((bool) $positionalArgs[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) {
$acceptor = $parametersAcceptors[0];
$parameters = $acceptor->getParameters();
if (isset($parameters[1])) {
$argType = ($typeGetter)($args[0]->value);
$argType = ($typeGetter)($positionalArgs[0]->value);
$callableType = new CallableType(
[
new DummyParameter('value', ($iterableValueTypeGetter)($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
Expand All @@ -472,7 +498,7 @@ public static function applyIntrinsicArgOverrides(
new BooleanType(),
false,
);
$nativeArgType = ($nativeTypeGetter)($args[0]->value);
$nativeArgType = ($nativeTypeGetter)($positionalArgs[0]->value);
$nativeCallableType = new CallableType(
[
new DummyParameter('value', ($iterableValueTypeGetter)($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null),
Expand Down
3 changes: 2 additions & 1 deletion src/Type/Php/ArrayFilterParameterClosureTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\FunctionReflection;
Expand All @@ -28,7 +29,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Para

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type
{
$args = $functionCall->getArgs();
$args = ArgumentsNormalizer::reorderArgs($functionReflection->getNamedArgumentsVariants()[0] ?? $functionReflection->getVariants()[0], $functionCall->getArgs()) ?? [];
if (!isset($args[0])) {
return null;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Type/Php/ArrayFindParameterClosureTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\FunctionReflection;
Expand All @@ -27,7 +28,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Para

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type
{
$args = $functionCall->getArgs();
$args = ArgumentsNormalizer::reorderArgs($functionReflection->getNamedArgumentsVariants()[0] ?? $functionReflection->getVariants()[0], $functionCall->getArgs()) ?? [];
if (!isset($args[0])) {
return null;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Type/Php/ArrayMapParameterClosureTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\FunctionReflection;
Expand All @@ -26,7 +27,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Para

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type
{
$args = $functionCall->getArgs();
$args = ArgumentsNormalizer::reorderArgs($functionReflection->getNamedArgumentsVariants()[0] ?? $functionReflection->getVariants()[0], $functionCall->getArgs()) ?? [];
if (count($args) < 2) {
return null;
}
Expand Down
3 changes: 2 additions & 1 deletion src/Type/Php/ArrayWalkParameterClosureTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\FunctionReflection;
Expand All @@ -25,7 +26,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Para

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, ParameterReflection $parameter, Scope $scope): ?Type
{
$args = $functionCall->getArgs();
$args = ArgumentsNormalizer::reorderArgs($functionReflection->getNamedArgumentsVariants()[0] ?? $functionReflection->getVariants()[0], $functionCall->getArgs()) ?? [];
if (!isset($args[0])) {
return null;
}
Expand Down
Loading
Loading