Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
160 changes: 159 additions & 1 deletion src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
use function is_int;
use function is_string;
use function sprintf;
use function str_contains;
use const ARRAY_FILTER_USE_BOTH;
use const ARRAY_FILTER_USE_KEY;
use const CURLOPT_SHARE;
Expand Down Expand Up @@ -461,7 +462,18 @@ public static function selectFromArgs(
if (count($parametersAcceptors) === 1) {
$acceptor = $parametersAcceptors[0];
if (!self::hasAcceptorTemplateOrLateResolvableType($acceptor)) {
return $acceptor;
$skipEarlyReturn = false;
if ($namedArgumentsVariants !== null && self::acceptorHasCompoundParameterNames($acceptor)) {
foreach ($args as $arg) {
if ($arg->name !== null) {
$skipEarlyReturn = true;
break;
}
}
}
if (!$skipEarlyReturn) {
return $acceptor;
}
}
}

Expand Down Expand Up @@ -868,6 +880,142 @@ public static function combineAcceptors(array $acceptors): ExtendedParametersAcc
);
}

/**
* @param ParametersAcceptor[] $acceptors
*/
public static function combineAcceptorsByParameterName(array $acceptors): ExtendedParametersAcceptor
{
if (count($acceptors) === 0) {
throw new ShouldNotHappenException(
'getVariants() must return at least one variant.',
);
}
if (count($acceptors) === 1) {
return self::wrapAcceptor($acceptors[0]);
}

$parametersByName = [];
$parameterNames = [];
foreach ($acceptors as $acceptor) {
foreach ($acceptor->getParameters() as $parameter) {
$name = $parameter->getName();
if (!isset($parametersByName[$name])) {
$parameterNames[] = $name;
$parametersByName[$name] = [];
}
$parametersByName[$name][] = $parameter;
}
}

$acceptorCount = count($acceptors);
$parameters = [];
$isVariadic = false;
$returnTypes = [];
$phpDocReturnTypes = [];
$nativeReturnTypes = [];

foreach ($acceptors as $acceptor) {
$returnTypes[] = $acceptor->getReturnType();
if ($acceptor instanceof ExtendedParametersAcceptor) {
$phpDocReturnTypes[] = $acceptor->getPhpDocReturnType();
$nativeReturnTypes[] = $acceptor->getNativeReturnType();
}
$isVariadic = $isVariadic || $acceptor->isVariadic();
}

foreach ($parameterNames as $name) {
$params = $parametersByName[$name];
$existsInAll = count($params) === $acceptorCount;

$types = [];
$nativeTypes = [];
$phpDocTypes = [];
$defaultValues = [];
$paramIsVariadic = false;
$outType = null;
$closureThisType = null;
$immediatelyInvokedCallable = TrinaryLogic::createMaybe();
$attributes = [];
$isOptional = !$existsInAll;
$passedByRef = $params[0]->passedByReference();

foreach ($params as $j => $param) {
$types[] = $param->getType();
$paramIsVariadic = $paramIsVariadic || $param->isVariadic();

if (!$isOptional && $param->isOptional()) {
$isOptional = true;
}

$defaultValue = $param->getDefaultValue();
if ($defaultValue !== null) {
$defaultValues[] = $defaultValue;
}

if ($j > 0) {
$passedByRef = $passedByRef->combine($param->passedByReference());
}

if ($param instanceof ExtendedParameterReflection) {
$nativeTypes[] = $param->getNativeType();
$phpDocTypes[] = $param->getPhpDocType();
$immediatelyInvokedCallable = $param->isImmediatelyInvokedCallable()->or($immediatelyInvokedCallable);
$attributes = array_merge($attributes, $param->getAttributes());

if ($param->getOutType() !== null) {
$outType = $outType === null ? $param->getOutType() : TypeCombinator::union($outType, $param->getOutType());
} else {
$outType = null;
}

if ($param->getClosureThisType() !== null && $closureThisType !== null) {
$closureThisType = TypeCombinator::union($closureThisType, $param->getClosureThisType());
} elseif ($closureThisType === null && $param === $params[0] && $param->getClosureThisType() !== null) {
$closureThisType = $param->getClosureThisType();
} else {
$closureThisType = null;
}
} else {
$nativeTypes[] = new MixedType();
$phpDocTypes[] = $param->getType();
}
}

$combinedDefaultValue = count($defaultValues) === count($params)
? TypeCombinator::union(...$defaultValues)
: null;

$parameters[] = new ExtendedDummyParameter(
$name,
TypeCombinator::union(...$types),
$isOptional,
$passedByRef,
$paramIsVariadic,
$combinedDefaultValue,
TypeCombinator::union(...$nativeTypes),
TypeCombinator::union(...$phpDocTypes),
$outType,
$immediatelyInvokedCallable,
$closureThisType,
$attributes,
);
}

$returnType = TypeCombinator::union(...$returnTypes);
$phpDocReturnType = $phpDocReturnTypes === [] ? null : TypeCombinator::union(...$phpDocReturnTypes);
$nativeReturnType = $nativeReturnTypes === [] ? null : TypeCombinator::union(...$nativeReturnTypes);

return new ExtendedFunctionVariant(
TemplateTypeMap::createEmpty(),
null,
$parameters,
$isVariadic,
$returnType,
$phpDocReturnType ?? $returnType,
$nativeReturnType ?? new MixedType(),
);
}

private static function wrapAcceptor(ParametersAcceptor $acceptor): ExtendedParametersAcceptor
{
if ($acceptor instanceof ExtendedParametersAcceptor) {
Expand Down Expand Up @@ -907,6 +1055,16 @@ private static function wrapAcceptor(ParametersAcceptor $acceptor): ExtendedPara
);
}

private static function acceptorHasCompoundParameterNames(ParametersAcceptor $acceptor): bool
{
foreach ($acceptor->getParameters() as $parameter) {
if (str_contains($parameter->getName(), '|')) {
return true;
}
}
return false;
}

private static function wrapParameter(ParameterReflection $parameter): ExtendedParameterReflection
{
return $parameter instanceof ExtendedParameterReflection ? $parameter : new ExtendedDummyParameter(
Expand Down
13 changes: 11 additions & 2 deletions src/Reflection/Type/IntersectionTypeMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPStan\Reflection\ExtendedMethodReflection;
use PHPStan\Reflection\ExtendedParametersAcceptor;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;
Expand Down Expand Up @@ -128,9 +129,17 @@ public function getOnlyVariant(): ExtendedParametersAcceptor
return $variants[0];
}

public function getNamedArgumentsVariants(): ?array
public function getNamedArgumentsVariants(): array
{
return null;
$allVariants = [];
foreach ($this->methods as $method) {
$namedVariants = $method->getNamedArgumentsVariants();
foreach ($namedVariants ?? $method->getVariants() as $variant) {
$allVariants[] = $variant;
}
}

return [ParametersAcceptorSelector::combineAcceptorsByParameterName($allVariants)];
}

public function isDeprecated(): TrinaryLogic
Expand Down
12 changes: 10 additions & 2 deletions src/Reflection/Type/UnionTypeMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,17 @@ public function getOnlyVariant(): ExtendedParametersAcceptor
return $this->getVariants()[0];
}

public function getNamedArgumentsVariants(): ?array
public function getNamedArgumentsVariants(): array
{
return null;
$allVariants = [];
foreach ($this->methods as $method) {
$namedVariants = $method->getNamedArgumentsVariants();
foreach ($namedVariants ?? $method->getVariants() as $variant) {
$allVariants[] = $variant;
}
}

return [ParametersAcceptorSelector::combineAcceptorsByParameterName($allVariants)];
}

public function isDeprecated(): TrinaryLogic
Expand Down
26 changes: 26 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4106,4 +4106,30 @@ public function testBug14596(): void
]);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug14661(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-14661.php'], [
[
'Parameter $a of method Bug14661\A::differentTypes() expects int, string given.',
76,
],
[
'Parameter $b of method Bug14661\A::differentTypes() expects string, int given.',
76,
],
[
'Parameter $a of method Bug14661\A::differentTypes() expects int, string given.',
77,
],
[
'Parameter $b of method Bug14661\A::differentTypes() expects string, int given.',
77,
],
]);
}

}
7 changes: 7 additions & 0 deletions tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1033,4 +1033,11 @@ public function testBug14596(): void
]);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug14661(): void
{
$this->checkThisOnly = false;
$this->analyse([__DIR__ . '/data/bug-14661-static.php'], []);
}

}
31 changes: 31 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-14661-static.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php // lint >= 8.0

declare(strict_types=1);

namespace Bug14661Static;

class A
{
public static function mixedOrder(
?string $other = null,
?string $target = null,
): void {}
}

class B
{
public static function mixedOrder(
?string $target = null,
?string $other = null,
): void {}
}

/**
* @param class-string<A>|class-string<B> $class
*/
function staticMixedOrder(string $class): void
{
$class::mixedOrder(target: 'value');
$class::mixedOrder(other: 'a', target: 'b');
$class::mixedOrder(target: 'b', other: 'a');
}
84 changes: 84 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-14661.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php // lint >= 8.0

declare(strict_types=1);

namespace Bug14661;

class A
{
public function mixedOrder(
?string $other = null,
?string $target = null,
): void {}

public function sameOrder(
?string $other = null,
?string $target = null,
): void {}

public function differentTypes(
int $a,
string $b,
): void {}
}

class B
{
public function mixedOrder(
?string $target = null,
?string $other = null,
): void {}

public function sameOrder(
?string $other = null,
?string $target = null,
): void {}

public function differentTypes(
string $b,
int $a,
): void {}
}

class C
{
public function mixedOrder(
?string $target = null,
?string $extra = null,
?string $other = null,
): void {}
}

function mixedOrder(A|B $obj): void
{
$obj->mixedOrder(target: 'value');
}

function sameOrder(A|B $obj): void
{
$obj->sameOrder(target: 'value');
}

function mixedOrderBothArgs(A|B $obj): void
{
$obj->mixedOrder(other: 'a', target: 'b');
$obj->mixedOrder(target: 'b', other: 'a');
}

function differentTypes(A|B $obj): void
{
$obj->differentTypes(a: 1, b: 'hello');
$obj->differentTypes(b: 'hello', a: 1);
}

function differentTypesErrors(A|B $obj): void
{
$obj->differentTypes(a: 'hello', b: 1);
$obj->differentTypes(b: 1, a: 'hello');
}

function threeWayUnion(A|B|C $obj): void
{
$obj->mixedOrder(target: 'value');
$obj->mixedOrder(other: 'a', target: 'b');
}
Loading