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
65 changes: 57 additions & 8 deletions src/Analyser/ArgumentsNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use function array_is_list;
use function array_key_exists;
use function array_keys;
use function array_search;
use function array_values;
use function count;
use function is_string;
Expand Down Expand Up @@ -301,14 +302,7 @@ public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array
return [];
}

$hasNamedArgs = false;
foreach ($callArgs as $arg) {
if ($arg->name !== null) {
$hasNamedArgs = true;
break;
}
}
if (!$hasNamedArgs) {
if (!self::hasNamedArgs($callArgs)) {
return array_values($callArgs);
}

Expand Down Expand Up @@ -441,6 +435,61 @@ public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array
return $reorderedArgs;
}

/**
* Maps the arguments of a call onto the positions of the callee's parameters,
* leaving out named arguments that don't match any of $parameterNames.
*
* Unlike reorderArgs() this doesn't need a ParametersAcceptor - the caller
* spells out the parameter names it knows about, so it also works in the
* parser visitors, which run before any reflection is available. It also
* returns the original Arg objects instead of copies, which is what makes
* the attributes those visitors set visible on the analysed AST.
*
* @param Arg[] $args
* @param list<string> $parameterNames parameter names in signature order
* @return array<int, Arg>
*/
public static function getArgsByPosition(array $args, array $parameterNames): array
{
if (!self::hasNamedArgs($args)) {
return $args;
}

$argsByPosition = [];
foreach ($args as $i => $arg) {
if ($arg->name === null) {
// positional arguments always precede named ones
$argsByPosition[$i] = $arg;
continue;
}

$position = array_search($arg->name->toString(), $parameterNames, true);
if ($position === false) {
continue;
}

$argsByPosition[$position] = $arg;
}

return $argsByPosition;
}

/**
* @param Arg[] $args
*/
private static function hasNamedArgs(array $args): bool
{
foreach ($args as $arg) {
if ($arg->name === null) {
continue;
}

return true;
}

return false;
}

/**
* The printed form of an expression is derived from its own arguments, so
* it must not travel to a node whose arguments were just reordered — the
Expand Down
5 changes: 4 additions & 1 deletion 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;

#[AutowiredService]
Expand All @@ -13,13 +14,15 @@ final class ArrayFilterArgVisitor extends NodeVisitorAbstract

public const ATTRIBUTE_NAME = 'isArrayFilterArg';

public const PARAMETER_NAMES = ['array', 'callback', 'mode'];

#[Override]
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();
$args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES);
if (isset($args[0])) {
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
}
Expand Down
5 changes: 4 additions & 1 deletion 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 function in_array;

Expand All @@ -14,13 +15,15 @@ final class ArrayFindArgVisitor extends NodeVisitorAbstract

public const ATTRIBUTE_NAME = 'isArrayFindArg';

public const PARAMETER_NAMES = ['array', 'callback'];

#[Override]
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();
$args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES);
if (isset($args[0])) {
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
}
Expand Down
24 changes: 13 additions & 11 deletions src/Parser/ArrayMapArgVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
use Override;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\DependencyInjection\AutowiredService;
use function array_slice;
use function count;

#[AutowiredService]
Expand All @@ -15,29 +15,31 @@ final class ArrayMapArgVisitor extends NodeVisitorAbstract

public const ATTRIBUTE_NAME = 'arrayMapArgs';

public const PARAMETER_NAMES = ['callback', 'array', 'arrays'];

#[Override]
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_map') {
$args = $node->getArgs();
$callbackArg = ArgumentsNormalizer::getArgsByPosition($args, self::PARAMETER_NAMES)[0] ?? null;
if ($callbackArg === null) {
return null;
}

$arrayArgs = [];
foreach ($args as $i => $arg) {
if ($arg->name === null && $i === 0) {
continue;
}
if ($arg->name !== null && $arg->name->toString() === 'callback') {
foreach ($args as $arg) {
if ($arg === $callbackArg) {
continue;
}

$arrayArgs[] = $arg;
}
if (isset($args[0])) {
$slicedArgs = array_slice($args, 1);
if (count($slicedArgs) > 0) {
$args[0]->value->setAttribute(self::ATTRIBUTE_NAME, $arrayArgs);
}

if (count($arrayArgs) > 0) {
$callbackArg->value->setAttribute(self::ATTRIBUTE_NAME, $arrayArgs);
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion 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;

#[AutowiredService]
Expand All @@ -13,13 +14,15 @@ final class ArrayWalkArgVisitor extends NodeVisitorAbstract

public const ATTRIBUTE_NAME = 'isArrayWalkArg';

public const PARAMETER_NAMES = ['array', 'callback', 'arg'];

#[Override]
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();
$args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES);
if (isset($args[0])) {
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
}
Expand Down
8 changes: 7 additions & 1 deletion src/Parser/ClosureBindArgVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\DependencyInjection\AutowiredService;
use function count;

Expand All @@ -15,6 +16,8 @@ final class ClosureBindArgVisitor extends NodeVisitorAbstract

public const ATTRIBUTE_NAME = 'closureBindArg';

public const PARAMETER_NAMES = ['closure', 'newThis', 'newScope'];

#[Override]
public function enterNode(Node $node): ?Node
{
Expand All @@ -28,7 +31,10 @@ public function enterNode(Node $node): ?Node
) {
$args = $node->getArgs();
if (count($args) > 1) {
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
$args = ArgumentsNormalizer::getArgsByPosition($args, self::PARAMETER_NAMES);
if (isset($args[0])) {
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
}
}
}
return null;
Expand Down
5 changes: 4 additions & 1 deletion src/Parser/ClosureBindToVarVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\DependencyInjection\AutowiredService;

#[AutowiredService]
Expand All @@ -14,6 +15,8 @@ final class ClosureBindToVarVisitor extends NodeVisitorAbstract

public const ATTRIBUTE_NAME = 'closureBindToVar';

public const PARAMETER_NAMES = ['newThis', 'newScope'];

#[Override]
public function enterNode(Node $node): ?Node
{
Expand All @@ -23,7 +26,7 @@ public function enterNode(Node $node): ?Node
&& $node->name->toLowerString() === 'bindto'
&& !$node->isFirstClassCallable()
) {
$args = $node->getArgs();
$args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES);
if (isset($args[0])) {
$args[0]->setAttribute(self::ATTRIBUTE_NAME, $node->var);
}
Expand Down
5 changes: 4 additions & 1 deletion 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;

#[AutowiredService]
Expand All @@ -13,13 +14,15 @@ final class CurlSetOptArgVisitor extends NodeVisitorAbstract

public const ATTRIBUTE_NAME = 'isCurlSetOptArg';

public const PARAMETER_NAMES = ['handle', 'option', 'value'];

#[Override]
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();
$args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES);
if (isset($args[0])) {
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
}
Expand Down
5 changes: 4 additions & 1 deletion 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;

#[AutowiredService]
Expand All @@ -13,13 +14,15 @@ final class CurlSetOptArrayArgVisitor extends NodeVisitorAbstract

public const ATTRIBUTE_NAME = 'isCurlSetOptArrayArg';

public const PARAMETER_NAMES = ['handle', 'options'];

#[Override]
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();
$args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES);
if (isset($args[1])) {
$args[1]->setAttribute(self::ATTRIBUTE_NAME, true);
}
Expand Down
11 changes: 8 additions & 3 deletions src/Parser/ImplodeArgVisitor.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 function in_array;

Expand All @@ -14,15 +15,19 @@ final class ImplodeArgVisitor extends NodeVisitorAbstract

public const ATTRIBUTE_NAME = 'isImplodeArg';

public const PARAMETER_NAMES = ['separator', 'array'];

#[Override]
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, ['implode', 'join'], true)) {
$args = $node->getArgs();
if (isset($args[0])) {
$args[0]->setAttribute(self::ATTRIBUTE_NAME, true);
$args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES);
// implode(array: $a) leaves the first parameter unfilled
$markedArg = $args[0] ?? $args[1] ?? null;
if ($markedArg !== null) {
$markedArg->setAttribute(self::ATTRIBUTE_NAME, true);
}
}
}
Expand Down
Loading
Loading