Skip to content
Merged
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
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ parameters:
strictUnnecessaryNullsafePropertyFetch: true
looseComparison: true
consistentConstructor: true
checkUnresolvableParameterTypes: true
3 changes: 3 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ parameters:
strictUnnecessaryNullsafePropertyFetch: false
looseComparison: false
consistentConstructor: false
checkUnresolvableParameterTypes: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down Expand Up @@ -213,6 +214,7 @@ parametersSchema:
strictUnnecessaryNullsafePropertyFetch: bool(),
looseComparison: bool(),
consistentConstructor: bool()
checkUnresolvableParameterTypes: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down Expand Up @@ -841,6 +843,7 @@ services:
checkArgumentsPassedByReference: %checkArgumentsPassedByReference%
checkExtraArguments: %checkExtraArguments%
checkMissingTypehints: %checkMissingTypehints%
checkUnresolvableParameterTypes: %featureToggles.checkUnresolvableParameterTypes%

-
class: PHPStan\Rules\FunctionDefinitionCheck
Expand Down
49 changes: 44 additions & 5 deletions src/Reflection/ParametersAcceptorSelector.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPStan\TrinaryLogic;
use PHPStan\Type\CallableType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\LateResolvableType;
Expand Down Expand Up @@ -74,7 +75,7 @@ public static function selectFromArgs(
$parameters = $acceptor->getParameters();
$callbackParameters = [];
foreach ($arrayMapArgs as $arg) {
$callbackParameters[] = new DummyParameter('item', $scope->getType($arg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null);
$callbackParameters[] = new DummyParameter('item', self::getIterableValueType($scope->getType($arg->value)), false, PassedByReference::createNo(), false, null);
}
$parameters[0] = new NativeParameterReflection(
$parameters[0]->getName(),
Expand Down Expand Up @@ -104,12 +105,12 @@ public static function selectFromArgs(
if ($mode instanceof ConstantIntegerType) {
if ($mode->getValue() === ARRAY_FILTER_USE_KEY) {
$arrayFilterParameters = [
new DummyParameter('key', $scope->getType($args[0]->value)->getIterableKeyType(), false, PassedByReference::createNo(), false, null),
new DummyParameter('key', self::getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
];
} elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) {
$arrayFilterParameters = [
new DummyParameter('item', $scope->getType($args[0]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
new DummyParameter('key', $scope->getType($args[0]->value)->getIterableKeyType(), false, PassedByReference::createNo(), false, null),
new DummyParameter('item', self::getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
new DummyParameter('key', self::getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
];
}
}
Expand All @@ -122,7 +123,7 @@ public static function selectFromArgs(
$parameters[1]->isOptional(),
new CallableType(
$arrayFilterParameters ?? [
new DummyParameter('item', $scope->getType($args[0]->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null),
new DummyParameter('item', self::getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null),
],
new MixedType(),
false,
Expand Down Expand Up @@ -395,4 +396,42 @@ public static function combineAcceptors(array $acceptors): ParametersAcceptor
);
}

private static function getIterableValueType(Type $type): Type
{
if ($type instanceof UnionType) {
$types = [];
foreach ($type->getTypes() as $innerType) {
$iterableValueType = $innerType->getIterableValueType();
if ($iterableValueType instanceof ErrorType) {
continue;
}

$types[] = $iterableValueType;
}

return TypeCombinator::union(...$types);
}

return $type->getIterableValueType();
}

private static function getIterableKeyType(Type $type): Type
{
if ($type instanceof UnionType) {
$types = [];
foreach ($type->getTypes() as $innerType) {
$iterableKeyType = $innerType->getIterableKeyType();
if ($iterableKeyType instanceof ErrorType) {
continue;
}

$types[] = $iterableKeyType;
}

return TypeCombinator::union(...$types);
}

return $type->getIterableKeyType();
}

}
1 change: 1 addition & 0 deletions src/Rules/AttributesCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public function check(
'Missing parameter $%s in call to ' . $attributeClassName . ' constructor.',
'Unknown parameter $%s in call to ' . $attributeClassName . ' constructor.',
'Return type of call to ' . $attributeClassName . ' constructor contains unresolvable type.',
'Parameter %s of attribute class ' . $attributeClassName . ' constructor contains unresolvable type.',
],
);

Expand Down
1 change: 1 addition & 0 deletions src/Rules/Classes/InstantiationRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope $
'Missing parameter $%s in call to ' . $classDisplayName . ' constructor.',
'Unknown parameter $%s in call to ' . $classDisplayName . ' constructor.',
'Return type of call to ' . $classDisplayName . ' constructor contains unresolvable type.',
'Parameter %s of class ' . $classDisplayName . ' constructor contains unresolvable type.',
],
));
}
Expand Down
47 changes: 38 additions & 9 deletions src/Rules/FunctionCallParametersCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use PHPStan\Type\TypeUtils;
use PHPStan\Type\VerbosityLevel;
use PHPStan\Type\VoidType;
use function array_fill;
use function array_key_exists;
use function count;
use function is_string;
Expand All @@ -43,13 +44,14 @@ public function __construct(
private bool $checkArgumentsPassedByReference,
private bool $checkExtraArguments,
private bool $checkMissingTypehints,
private bool $checkUnresolvableParameterTypes,
)
{
}

/**
* @param Node\Expr\FuncCall|Node\Expr\MethodCall|Node\Expr\StaticCall|Node\Expr\New_ $funcCall
* @param array{string, string, string, string, string, string, string, string, string, string, string, string, string} $messages
* @param array{string, string, string, string, string, string, string, string, string, string, string, string, string, string} $messages
* @return RuleError[]
*/
public function check(
Expand Down Expand Up @@ -223,7 +225,7 @@ public function check(
return $errors;
}

foreach ($argumentsWithParameters as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter]) {
foreach ($argumentsWithParameters as $i => [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter, $originalParameter]) {
if ($this->checkArgumentTypes && $unpack) {
$iterableTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
Expand Down Expand Up @@ -268,6 +270,24 @@ public function check(
))->line($argumentLine)->build();
}

if (
$this->checkArgumentTypes
&& $this->checkUnresolvableParameterTypes
&& $originalParameter !== null
&& !$this->unresolvableTypeHelper->containsUnresolvableType($originalParameter->getType())
&& $this->unresolvableTypeHelper->containsUnresolvableType($parameterType)
) {
$parameterDescription = sprintf('%s$%s', $parameter->isVariadic() ? '...' : '', $parameter->getName());
$errors[] = RuleErrorBuilder::message(sprintf(
$messages[13],
$argumentName === null ? sprintf(
'#%d %s',
$i + 1,
$parameterDescription,
) : $parameterDescription,
))->line($argumentLine)->build();
}

if (
!$this->checkArgumentsPassedByReference
|| !$parameter->passedByReference()->yes()
Expand Down Expand Up @@ -395,7 +415,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty

/**
* @param array<int, array{Expr, Type, bool, string|null, int}> $arguments
* @return array{RuleError[], array<int, array{Expr, Type, bool, (string|null), int, (ParameterReflection|null)}>}
* @return array{RuleError[], array<int, array{Expr, Type, bool, (string|null), int, (ParameterReflection|null), (ParameterReflection|null)}>}
*/
private function processArguments(
ParametersAcceptor $parametersAcceptor,
Expand All @@ -408,11 +428,16 @@ private function processArguments(
): array
{
$parameters = $parametersAcceptor->getParameters();
$originalParameters = $parametersAcceptor instanceof ResolvedFunctionVariant
? $parametersAcceptor->getOriginalParametersAcceptor()->getParameters()
: array_fill(0, count($parameters), null);
$parametersByName = [];
$originalParametersByName = [];
$unusedParametersByName = [];
$errors = [];
foreach ($parametersAcceptor->getParameters() as $parameter) {
foreach ($parameters as $i => $parameter) {
$parametersByName[$parameter->getName()] = $parameter;
$originalParametersByName[$parameter->getName()] = $originalParameters[$i];

if ($parameter->isVariadic()) {
continue;
Expand All @@ -428,21 +453,24 @@ private function processArguments(
if ($argumentName === null) {
if (!isset($parameters[$i])) {
if (!$parametersAcceptor->isVariadic() || count($parameters) === 0) {
$newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null];
$newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null];
break;
}

$parameter = $parameters[count($parameters) - 1];
$originalParameter = $originalParameters[count($originalParameters) - 1];
if (!$parameter->isVariadic()) {
$newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null];
$newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null];
break; // func_get_args
}
} else {
$parameter = $parameters[$i];
$originalParameter = $originalParameters[$i];
}
} elseif (array_key_exists($argumentName, $parametersByName)) {
$namedArgumentAlreadyOccurred = true;
$parameter = $parametersByName[$argumentName];
$originalParameter = $originalParametersByName[$argumentName];
} else {
$namedArgumentAlreadyOccurred = true;

Expand All @@ -453,20 +481,21 @@ private function processArguments(
|| $isBuiltin
) {
$errors[] = RuleErrorBuilder::message(sprintf($unknownParameterMessage, $argumentName))->line($argumentLine)->build();
$newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null];
$newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null];
continue;
}

$parameter = $parameters[$parametersCount - 1];
$originalParameter = $originalParameters[$parametersCount - 1];
}

if ($namedArgumentAlreadyOccurred && $argumentName === null && !$unpack) {
$errors[] = RuleErrorBuilder::message('Named argument cannot be followed by a positional argument.')->line($argumentLine)->nonIgnorable()->build();
$newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null];
$newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, null, null];
continue;
}

$newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter];
$newArguments[$i] = [$argumentValue, $argumentValueType, $unpack, $argumentName, $argumentLine, $parameter, $originalParameter];

if (
$hasNamedArguments
Expand Down
1 change: 1 addition & 0 deletions src/Rules/Functions/CallCallablesRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ public function processNode(
'Missing parameter $%s in call to ' . $callableDescription . '.',
'Unknown parameter $%s in call to ' . $callableDescription . '.',
'Return type of call to ' . $callableDescription . ' contains unresolvable type.',
'Parameter %s of ' . $callableDescription . ' contains unresolvable type.',
],
),
);
Expand Down
1 change: 1 addition & 0 deletions src/Rules/Functions/CallToFunctionParametersRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public function processNode(Node $node, Scope $scope): array
'Missing parameter $%s in call to function ' . $functionName . '.',
'Unknown parameter $%s in call to function ' . $functionName . '.',
'Return type of call to function ' . $functionName . ' contains unresolvable type.',
'Parameter %s of function ' . $functionName . ' contains unresolvable type.',
],
);
}
Expand Down
1 change: 1 addition & 0 deletions src/Rules/Methods/CallMethodsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public function processNode(Node $node, Scope $scope): array
'Missing parameter $%s in call to method ' . $messagesMethodName . '.',
'Unknown parameter $%s in call to method ' . $messagesMethodName . '.',
'Return type of call to method ' . $messagesMethodName . ' contains unresolvable type.',
'Parameter %s of method ' . $messagesMethodName . ' contains unresolvable type.',
],
));
}
Expand Down
1 change: 1 addition & 0 deletions src/Rules/Methods/CallStaticMethodsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public function processNode(Node $node, Scope $scope): array
'Missing parameter $%s in call to ' . $lowercasedMethodName . '.',
'Unknown parameter $%s in call to ' . $lowercasedMethodName . '.',
'Return type of call to ' . $lowercasedMethodName . ' contains unresolvable type.',
'Parameter %s of ' . $lowercasedMethodName . ' contains unresolvable type.',
],
));

Expand Down
2 changes: 2 additions & 0 deletions src/Testing/TestCase.neon
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
parameters:
inferPrivatePropertyTypeFromConstructor: true
featureToggles:
checkUnresolvableParameterTypes: true

services:
-
Expand Down
40 changes: 27 additions & 13 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -780,21 +780,23 @@ public function testBug7215(): void
public function testBug7094(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/bug-7094.php');
$this->assertCount(6, $errors);
$this->assertCount(7, $errors);

$this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() expects string, int given.', $errors[0]->getMessage());
$this->assertSame(75, $errors[0]->getLine());
$this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() expects 5|6|7, 3 given.', $errors[1]->getMessage());
$this->assertSame(76, $errors[1]->getLine());
$this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() expects string, int given.', $errors[2]->getMessage());
$this->assertSame(78, $errors[2]->getLine());
$this->assertSame('Return type of call to method Bug7094\Foo::getAttribute() contains unresolvable type.', $errors[3]->getMessage());
$this->assertSame(79, $errors[3]->getLine());
$this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() contains unresolvable type.', $errors[0]->getMessage());
$this->assertSame(74, $errors[0]->getLine());
$this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() expects string, int given.', $errors[1]->getMessage());
$this->assertSame(75, $errors[1]->getLine());
$this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() expects 5|6|7, 3 given.', $errors[2]->getMessage());
$this->assertSame(76, $errors[2]->getLine());
$this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() expects string, int given.', $errors[3]->getMessage());
$this->assertSame(78, $errors[3]->getLine());
$this->assertSame('Return type of call to method Bug7094\Foo::getAttribute() contains unresolvable type.', $errors[4]->getMessage());
$this->assertSame(79, $errors[4]->getLine());

$this->assertSame('Parameter #1 $attr of method Bug7094\Foo::setAttributes() expects array{foo?: string, bar?: 5|6|7, baz?: bool}, non-empty-array<string, 5|6|7|bool|string> given.', $errors[4]->getMessage());
$this->assertSame(29, $errors[4]->getLine());
$this->assertSame('Parameter #1 $attr of method Bug7094\Foo::setAttributes() expects array{foo?: string, bar?: 5|6|7, baz?: bool}, array<\'bar\'|\'baz\'|\'foo\', 5|6|7|bool|string> given.', $errors[5]->getMessage());
$this->assertSame(49, $errors[5]->getLine());
$this->assertSame('Parameter #1 $attr of method Bug7094\Foo::setAttributes() expects array{foo?: string, bar?: 5|6|7, baz?: bool}, non-empty-array<string, 5|6|7|bool|string> given.', $errors[5]->getMessage());
$this->assertSame(29, $errors[5]->getLine());
$this->assertSame('Parameter #1 $attr of method Bug7094\Foo::setAttributes() expects array{foo?: string, bar?: 5|6|7, baz?: bool}, array<\'bar\'|\'baz\'|\'foo\', 5|6|7|bool|string> given.', $errors[6]->getMessage());
$this->assertSame(49, $errors[6]->getLine());
}

public function testOffsetAccess(): void
Expand All @@ -805,6 +807,18 @@ public function testOffsetAccess(): void
$this->assertSame(42, $errors[0]->getLine());
}

public function testUnresolvableParameter(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/unresolvable-parameter.php');
$this->assertCount(3, $errors);
$this->assertSame('Parameter #2 $array of function array_map expects array, array<int, string>|false given.', $errors[0]->getMessage());
$this->assertSame(18, $errors[0]->getLine());
$this->assertSame('Method UnresolvableParameter\Collection::pipeInto() has parameter $class with no type specified.', $errors[1]->getMessage());
$this->assertSame(30, $errors[1]->getLine());
$this->assertSame('PHPDoc tag @param for parameter $class contains unresolvable type.', $errors[2]->getMessage());
$this->assertSame(30, $errors[2]->getLine());
}

/**
* @param string[]|null $allAnalysedFiles
* @return Error[]
Expand Down
Loading