Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion src/Rules/Functions/ParameterCastableToNumberRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\Rules\ParameterCastableToStringCheck;
use PHPStan\Rules\Rule;
use PHPStan\Type\ErrorType;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use function count;
use function in_array;
Expand Down Expand Up @@ -69,7 +70,7 @@

$castFn = $this->phpVersion->supportsObjectsInArraySumProduct()
? static fn (Type $t) => $t->toNumber()
: static fn (Type $t) => !$t->isObject()->no() ? new ErrorType() : $t->toNumber();
: static fn (Type $t) => !$t instanceof MixedType && !$t->isObject()->no() ? new ErrorType() : $t->toNumber();

Check warning on line 73 in src/Rules/Functions/ParameterCastableToNumberRule.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $castFn = $this->phpVersion->supportsObjectsInArraySumProduct() ? static fn (Type $t) => $t->toNumber() - : static fn (Type $t) => !$t instanceof MixedType && !$t->isObject()->no() ? new ErrorType() : $t->toNumber(); + : static fn (Type $t) => !$t instanceof MixedType && $t->isObject()->yes() ? new ErrorType() : $t->toNumber(); $error = $this->parameterCastableToStringCheck->checkParameter( $origArgs[0],

Check warning on line 73 in src/Rules/Functions/ParameterCastableToNumberRule.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $castFn = $this->phpVersion->supportsObjectsInArraySumProduct() ? static fn (Type $t) => $t->toNumber() - : static fn (Type $t) => !$t instanceof MixedType && !$t->isObject()->no() ? new ErrorType() : $t->toNumber(); + : static fn (Type $t) => !$t instanceof MixedType && $t->isObject()->yes() ? new ErrorType() : $t->toNumber(); $error = $this->parameterCastableToStringCheck->checkParameter( $origArgs[0],

$error = $this->parameterCastableToStringCheck->checkParameter(
$origArgs[0],
Expand Down
24 changes: 17 additions & 7 deletions src/Rules/ParameterCastableToStringCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use PHPStan\Analyser\ArgumentsNormalizer;
use PHPStan\Analyser\Scope;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Node\Expr\TypeExpr;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Type;
Expand Down Expand Up @@ -34,22 +35,31 @@
return null;
}

$typeResult = $this->ruleLevelHelper->findTypeToCheck(
$arrayTypeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$parameter->value,
'',
static fn (Type $type): bool => $type->isArray()->yes() && !$castFn($type->getIterableValueType()) instanceof ErrorType,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The following change follows the same idea than https://github.com/phpstan/phpstan-src/pull/4166/changes for instance.

&& and || doesn't work always very well in UnionTypeCriteriaCallback it's sometimes better to split the check into multiple findTypeToCheck calls.

Here this allows to transform the iterable value type $type->getIterableValueType() into a StrictMixed when it's a Mixed for instance or array<mixed> is never reported at any level.

static fn (Type $type): bool => $type->isArray()->yes(),

Check warning on line 42 in src/Rules/ParameterCastableToStringCheck.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $scope, $parameter->value, '', - static fn (Type $type): bool => $type->isArray()->yes(), + static fn (Type $type): bool => !$type->isArray()->no(), ); $arrayType = $arrayTypeResult->getType();

Check warning on line 42 in src/Rules/ParameterCastableToStringCheck.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $scope, $parameter->value, '', - static fn (Type $type): bool => $type->isArray()->yes(), + static fn (Type $type): bool => !$type->isArray()->no(), ); $arrayType = $arrayTypeResult->getType();
);

$arrayType = $arrayTypeResult->getType();
if (!$arrayType->isArray()->yes()) {
return null;
}

$typeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
new TypeExpr($arrayType->getIterableValueType()),
'',
static fn (Type $type): bool => !$castFn($type) instanceof ErrorType,
);

if (
! $typeResult->getType()->isArray()->yes()
|| !$castFn($typeResult->getType()->getIterableValueType()) instanceof ErrorType
) {
if (!$castFn($typeResult->getType()) instanceof ErrorType) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you're not sure about how to work with RuleLevelHelper::findTypeToCheck, write thorough test for LevelsIntegrationTest. (The test regenerates the JSON files with expectations so 2nd run should always succeed.)

Before level 7, it should only complain about a type that is never castable to number.
On level 7+, it should complain about unions where only some types are castable to number.
On level 8+, it should complain about a nullable type (but maybe since null is castable to number, there will be no difference?)
On level 9+, it should complain about explicit mixed.
On level 10, it should complain about implicit mixed (without typehint).

Also I suspect we'll need also a separate test file for PHP 8.3+ because of supportsObjectsInArraySumProduct.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added tests in LevelsIntegrationTest to show the result.

I don't think it's worth using separate test file for PHP 8.3+ or - ; because the only difference is for some specific object type which are already tested.

return null;
}

return RuleErrorBuilder::message(
sprintf($errorMessageTemplate, $parameterName, $functionName, $typeResult->getType()->describe(VerbosityLevel::typeOnly())),
sprintf($errorMessageTemplate, $parameterName, $functionName, $arrayTypeResult->getType()->describe(VerbosityLevel::typeOnly())),
)->identifier('argument.type')->build();
}

Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Levels/LevelsIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public static function dataTopics(): array
['listType'],
['missingTypes'],
['arrayOffsetAccess'],
['arraySum'],
];
if (PHP_VERSION_ID >= 80300) {
$topics[] = ['constantAccesses83'];
Expand Down
7 changes: 7 additions & 0 deletions tests/PHPStan/Levels/data/arraySum-10-missing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Parameter #1 $array of function array_sum expects an array of values castable to number, array<string> given.",
"line": 23,
"ignorable": true
}
]
7 changes: 7 additions & 0 deletions tests/PHPStan/Levels/data/arraySum-10.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Parameter #1 $array of function array_sum expects array, mixed given.",
"line": 26,
"ignorable": true
}
]
42 changes: 42 additions & 0 deletions tests/PHPStan/Levels/data/arraySum-4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[
{
"message": "Call to function array_sum() on a separate line has no effect.",
"line": 19,
"ignorable": true
},
{
"message": "Call to function array_sum() on a separate line has no effect.",
"line": 20,
"ignorable": true
},
{
"message": "Call to function array_sum() on a separate line has no effect.",
"line": 21,
"ignorable": true
},
{
"message": "Call to function array_sum() on a separate line has no effect.",
"line": 22,
"ignorable": true
},
{
"message": "Call to function array_sum() on a separate line has no effect.",
"line": 23,
"ignorable": true
},
{
"message": "Call to function array_sum() on a separate line has no effect.",
"line": 24,
"ignorable": true
},
{
"message": "Call to function array_sum() on a separate line has no effect.",
"line": 25,
"ignorable": true
},
{
"message": "Call to function array_sum() on a separate line has no effect.",
"line": 26,
"ignorable": true
}
]
12 changes: 12 additions & 0 deletions tests/PHPStan/Levels/data/arraySum-5.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{
"message": "Parameter #1 $array of function array_sum expects an array of values castable to number, array<string> given.",
"line": 20,
"ignorable": true
},
{
"message": "Parameter #1 $array of function array_sum expects an array of values castable to number, array<string> given.",
"line": 23,
"ignorable": true
}
]
7 changes: 7 additions & 0 deletions tests/PHPStan/Levels/data/arraySum-6.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Method Levels\\ArraySum\\Foo::test() has parameter $implicitlyMixed with no type specified.",
"line": 13,
"ignorable": true
}
]
7 changes: 7 additions & 0 deletions tests/PHPStan/Levels/data/arraySum-7-missing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Parameter #1 $array of function array_sum expects an array of values castable to number, array<string> given.",
"line": 23,
"ignorable": true
}
]
22 changes: 22 additions & 0 deletions tests/PHPStan/Levels/data/arraySum-7.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"message": "Parameter #1 $array of function array_sum expects an array of values castable to number, array<int|string> given.",
"line": 21,
"ignorable": true
},
{
"message": "Parameter #1 $array of function array_sum expects array, array<int>|Levels\\ArraySum\\Foo given.",
"line": 22,
"ignorable": true
},
{
"message": "Parameter #1 $array of function array_sum expects array, array<string>|Levels\\ArraySum\\Foo given.",
"line": 23,
"ignorable": true
},
{
"message": "Parameter #1 $array of function array_sum expects array, array<int|string>|Levels\\ArraySum\\Foo given.",
"line": 24,
"ignorable": true
}
]
7 changes: 7 additions & 0 deletions tests/PHPStan/Levels/data/arraySum-8-missing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Parameter #1 $array of function array_sum expects an array of values castable to number, array<string> given.",
"line": 23,
"ignorable": true
}
]
7 changes: 7 additions & 0 deletions tests/PHPStan/Levels/data/arraySum-9-missing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Parameter #1 $array of function array_sum expects an array of values castable to number, array<string> given.",
"line": 23,
"ignorable": true
}
]
7 changes: 7 additions & 0 deletions tests/PHPStan/Levels/data/arraySum-9.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Parameter #1 $array of function array_sum expects array, mixed given.",
"line": 25,
"ignorable": true
}
]
28 changes: 28 additions & 0 deletions tests/PHPStan/Levels/data/arraySum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Levels\ArraySum;

class Foo
{
/**
* @param array<int> $arrayOfInt
* @param array<string> $arrayOfString
* @param array<int|string> $arrayOfUnion
* @param mixed $explicitlyMixed
*/
public function test(array $arrayOfInt, array $arrayOfString, array $arrayOfUnion, $explicitlyMixed, $implicitlyMixed): void
{
$objectOrArrayOfInt = rand(0, 1) ? new Foo() : $arrayOfInt;
$objectOrArrayOfString = rand(0, 1) ? new Foo() : $arrayOfString;
$objectOrArrayOfUnion = rand(0, 1) ? new Foo() : $arrayOfUnion;

array_sum($arrayOfInt);
array_sum($arrayOfString);
array_sum($arrayOfUnion);
array_sum($objectOrArrayOfInt);
array_sum($objectOrArrayOfString);
array_sum($objectOrArrayOfUnion);
array_sum($explicitlyMixed);
array_sum($implicitlyMixed);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@
class ParameterCastableToNumberRuleTest extends RuleTestCase
{

private bool $checkUnion = true;

private bool $checkExplicitMixed = true;

private bool $checkImplicitMixed = true;

protected function getRule(): Rule
{
$broker = self::createReflectionProvider();
return new ParameterCastableToNumberRule(
$broker,
new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, true, true, true, false, true)),
new ParameterCastableToStringCheck(new RuleLevelHelper($broker, true, false, $this->checkUnion, $this->checkExplicitMixed, $this->checkImplicitMixed, false, true)),
self::getContainer()->getByType(PhpVersion::class),
);
}
Expand Down Expand Up @@ -213,6 +219,39 @@ public function testBug13775Bis(): void
$this->analyse([__DIR__ . '/data/bug-13775-bis.php'], $this->hackPhp74ErrorMessages($errors));
}

public function testBug13775TerWithoutMixed(): void
{
$this->checkExplicitMixed = false;
$this->checkImplicitMixed = false;

$this->analyse([__DIR__ . '/data/bug-13775-ter.php'], $this->hackPhp74ErrorMessages([
[
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, int|string> given.',
9,
],
[
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, int|string> given.',
10,
],
]));
}

public function testBug13775TerWithoutUnion(): void
{
$this->checkUnion = false;

$this->analyse([__DIR__ . '/data/bug-13775-ter.php'], $this->hackPhp74ErrorMessages([
[
'Parameter #1 $array of function array_sum expects an array of values castable to number, array<int, mixed> given.',
5,
],
[
'Parameter #1 $array of function array_product expects an array of values castable to number, array<int, mixed> given.',
6,
],
]));
}

public function testBug12146(): void
{
$this->analyse([__DIR__ . '/data/bug-12146.php'], $this->hackPhp74ErrorMessages([
Expand Down
10 changes: 10 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-13775-ter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

/** @var mixed $a */
$a = doFoo();
echo array_sum([$a]) . "\n\n";
echo array_product([$a]) . "\n\n";

$b = rand(0, 1) ? 42 : '';
echo array_sum([$b]) . "\n\n";
echo array_product([$b]) . "\n\n";
Loading