Skip to content

fix parameter of strval, intval and floatval #1005

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: 1.5.x
Choose a base branch
from
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
1 change: 1 addition & 0 deletions conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ rules:
- PHPStan\Rules\Classes\AccessPrivateConstantThroughStaticRule
- PHPStan\Rules\Comparison\UsageOfVoidMatchExpressionRule
- PHPStan\Rules\Functions\IncompatibleDefaultParameterTypeRule
- PHPStan\Rules\Functions\TypevalFamilyParametersRule
- PHPStan\Rules\Generics\ClassAncestorsRule
- PHPStan\Rules\Generics\ClassTemplateTypeRule
- PHPStan\Rules\Generics\EnumAncestorsRule
Expand Down
8 changes: 4 additions & 4 deletions resources/functionMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -2020,7 +2020,7 @@
'DomXsltStylesheet::result_dump_mem' => ['string', 'xmldoc'=>'DOMDocument'],
'DOTNET::__construct' => ['void', 'assembly_name'=>'string', 'class_name'=>'string', 'codepage='=>'int'],
'dotnet_load' => ['int', 'assembly_name'=>'string', 'datatype_name='=>'string', 'codepage='=>'int'],
'doubleval' => ['float', 'var'=>'mixed'],
'doubleval' => ['float', 'var'=>'array|bool|float|int|string|null'],
'Ds\Collection::clear' => ['void'],
'Ds\Collection::copy' => ['Ds\Collection'],
'Ds\Collection::isEmpty' => ['bool'],
Expand Down Expand Up @@ -2988,7 +2988,7 @@
'finfo_file' => ['string|false', 'finfo'=>'resource', 'file_name'=>'string', 'options='=>'int', 'context='=>'resource'],
'finfo_open' => ['resource|false', 'options='=>'int', 'arg='=>'string'],
'finfo_set_flags' => ['bool', 'finfo'=>'resource', 'options'=>'int'],
'floatval' => ['float', 'var'=>'mixed'],
'floatval' => ['float', 'var'=>'array|bool|float|int|string|null'],
'flock' => ['bool', 'fp'=>'resource', 'operation'=>'int', '&w_wouldblock='=>'int'],
'floor' => ['float|false', 'number'=>'float'],
'flush' => ['void'],
Expand Down Expand Up @@ -5607,7 +5607,7 @@
'intltz_to_date_time_zone' => ['DateTimeZone|false', 'obj'=>''],
'intltz_use_daylight_time' => ['bool', 'obj'=>''],
'intlz_create_default' => ['IntlTimeZone'],
'intval' => ['int', 'var'=>'mixed', 'base='=>'int'],
'intval' => ['int', 'var'=>'array|bool|float|int|resource|string|null', 'base='=>'int'],
'InvalidArgumentException::__clone' => ['void'],
'InvalidArgumentException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?InvalidArgumentException)'],
'InvalidArgumentException::__toString' => ['string'],
Expand Down Expand Up @@ -11797,7 +11797,7 @@
'strtoupper' => ['string', 'str'=>'string'],
'strtr' => ['string', 'str'=>'string', 'from'=>'string', 'to'=>'string'],
'strtr\'1' => ['string', 'str'=>'string', 'replace_pairs'=>'array'],
'strval' => ['string', 'var'=>'mixed'],
'strval' => ['string', 'var'=>'bool|float|int|resource|string|null'],
'substr' => ['string', 'string'=>'string', 'start'=>'int', 'length='=>'int'],
'substr_compare' => ['int|false', 'main_str'=>'string', 'str'=>'string', 'offset'=>'int', 'length='=>'int', 'case_sensitivity='=>'bool'],
'substr_count' => ['0|positive-int', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'length='=>'int'],
Expand Down
92 changes: 92 additions & 0 deletions src/Rules/Functions/TypevalFamilyParametersRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Type\ErrorType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use function count;
use function in_array;
use function sprintf;
use function strtolower;
use const PHP_VERSION_ID;

/**
* @implements Rule<Node\Expr\FuncCall>
*/
class TypevalFamilyParametersRule implements Rule
{

private const FUNCTIONS = [
'intval',
'floatval',
'doubleval',
];

public function __construct(
private RuleLevelHelper $ruleLevelHelper,
)
{
}

public function getNodeType(): string
{
return Node\Expr\FuncCall::class;
}

public function processNode(Node $node, Scope $scope): array
{
if (!($node->name instanceof Node\Name)) {
return [];
}
$name = strtolower($node->name->toString());
if (!in_array($name, self::FUNCTIONS, true)) {
return [];
}
if (count($node->getArgs()) === 0) {
return [];
}

$paramTypeCallback = static fn (Type $type): Type => $type->toString();

$typeResult = $this->ruleLevelHelper->findTypeToCheck(
$scope,
$node->getArgs()[0]->value,
'',
static function (Type $type) use ($paramTypeCallback): bool {
$paramType = $paramTypeCallback($type);

return !$paramType instanceof ErrorType;
},
);
$type = $typeResult->getType();
$base = new ObjectType('');

if ($type instanceof ErrorType || !$base->isSuperTypeOf($type)->maybe()) {
Comment on lines +69 to +71
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a better way, to check if this is an instance of any object?

return [];
}

$paramType = $paramTypeCallback($type);
// only check if a stringable object is given, everything else is handled by the parameter typehint
if (!$paramType instanceof ErrorType) {

return [
RuleErrorBuilder::message(sprintf(
'Parameter #1 %s of function %s does not accept object, %s given.',
PHP_VERSION_ID < 80000 ? '$var' : '$value',
$name,
$scope->getType($node->getArgs()[0]->value)->describe(VerbosityLevel::value()),
))->line($node->getLine())->build(),
];
}

return [];
}

}
43 changes: 43 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use function array_reverse;
use function count;
use function extension_loaded;
use function method_exists;
use function restore_error_handler;
use function sprintf;
use const PHP_VERSION_ID;

class AnalyserIntegrationTest extends PHPStanTestCase
Expand Down Expand Up @@ -541,6 +543,47 @@ public function testBug6501(): void
$this->assertNoErrors($errors);
}

public function testTypevalFamilyFunctionParameters(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/typeval-functions.php');
$paramName = '$value';
if (PHP_VERSION_ID < 80000) {
$paramName = '$var';
}

$expectedErrors = [
['Cannot cast array{} to string.', 19],
[sprintf('Parameter #1 %s of function strval expects bool|float|int|resource|string|null, array given.', $paramName), 20],

['Cannot cast resource to float.', 65],
[sprintf('Parameter #1 %s of function floatval expects array|bool|float|int|string|null, resource given.', $paramName),66],
[sprintf('Parameter #1 %s of function doubleval expects array|bool|float|int|string|null, resource given.', $paramName),67],

['Cannot cast stdClass to string.', 79],
[sprintf('Parameter #1 %s of function strval expects bool|float|int|resource|string|null, stdClass given.', $paramName),80],

['Cannot cast stdClass to int.', 82],
[sprintf('Parameter #1 %s of function intval expects array|bool|float|int|resource|string|null, stdClass given.', $paramName),83],

['Cannot cast stdClass to float.', 85],
[sprintf('Parameter #1 %s of function floatval expects array|bool|float|int|string|null, stdClass given.', $paramName),86],
[sprintf('Parameter #1 %s of function doubleval expects array|bool|float|int|string|null, stdClass given.', $paramName),87],

['Cannot cast class@anonymous/tests/PHPStan/Analyser/data/typeval-functions.php:10 to int.', 92],
[sprintf('Parameter #1 %s of function intval does not accept object, class@anonymous/tests/PHPStan/Analyser/data/typeval-functions.php:10 given.', $paramName),93],

['Cannot cast class@anonymous/tests/PHPStan/Analyser/data/typeval-functions.php:10 to float.', 95],
[sprintf('Parameter #1 %s of function floatval does not accept object, class@anonymous/tests/PHPStan/Analyser/data/typeval-functions.php:10 given.', $paramName),96],
[sprintf('Parameter #1 %s of function doubleval does not accept object, class@anonymous/tests/PHPStan/Analyser/data/typeval-functions.php:10 given.', $paramName),97],
];

$this->assertCount(count($expectedErrors), $errors);
foreach ($expectedErrors as $key => [$message, $line]) {
$this->assertSame($message, $errors[$key]->getMessage());
$this->assertSame($line, $errors[$key]->getLine());
}
}

/**
* @param string[]|null $allAnalysedFiles
* @return Error[]
Expand Down
107 changes: 107 additions & 0 deletions tests/PHPStan/Analyser/data/typeval-functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php
$array = [];
$string = '';
$int = 1;
$float = .1;
$resource = fopen('./.env', 'r');
\assert(false !== $resource);
$bool = true;
$object = new stdClass();
$stringable = new class() {
public function __toString(): string {
return '';
}
};
$null = null;
/** @var mixed $mixed */
$mixed = null;

echo (string) $array . PHP_EOL;
echo strval($array) . PHP_EOL;

echo (int) $array . PHP_EOL;
echo intval($array) . PHP_EOL;

echo (float) $array . PHP_EOL;
echo floatval($array) . PHP_EOL;
echo doubleval($array) . PHP_EOL;

echo (string) $string . PHP_EOL;
echo strval($string) . PHP_EOL;

echo (int) $string . PHP_EOL;
echo intval($string) . PHP_EOL;

echo (float) $string . PHP_EOL;
echo floatval($string) . PHP_EOL;
echo doubleval($string) . PHP_EOL;

echo (string) $int . PHP_EOL;
echo strval($int) . PHP_EOL;

echo (int) $int . PHP_EOL;
echo intval($int) . PHP_EOL;

echo (float) $int . PHP_EOL;
echo floatval($int) . PHP_EOL;
echo doubleval($int) . PHP_EOL;

echo (string) $float . PHP_EOL;
echo strval($float) . PHP_EOL;

echo (int) $float . PHP_EOL;
echo intval($float) . PHP_EOL;

echo (float) $float . PHP_EOL;
echo floatval($float) . PHP_EOL;
echo doubleval($float) . PHP_EOL;

echo (string) $resource . PHP_EOL;
echo strval($resource) . PHP_EOL;

echo (int) $resource . PHP_EOL;
echo intval($resource) . PHP_EOL;

echo (float) $resource . PHP_EOL;
echo floatval($resource) . PHP_EOL;
echo doubleval($resource) . PHP_EOL;

echo (string) $bool . PHP_EOL;
echo strval($bool) . PHP_EOL;

echo (int) $bool . PHP_EOL;
echo intval($bool) . PHP_EOL;

echo (float) $bool . PHP_EOL;
echo floatval($bool) . PHP_EOL;
echo doubleval($bool) . PHP_EOL;

echo (string) $object . PHP_EOL;
echo strval($object) . PHP_EOL;

echo (int) $object . PHP_EOL;
echo intval($object) . PHP_EOL;

echo (float) $object . PHP_EOL;
echo floatval($object) . PHP_EOL;
echo doubleval($object) . PHP_EOL;

echo (string) $stringable . PHP_EOL;
echo strval($stringable) . PHP_EOL;

echo (int) $stringable . PHP_EOL;
echo intval($stringable) . PHP_EOL;

echo (float) $stringable . PHP_EOL;
echo floatval($stringable) . PHP_EOL;
echo doubleval($stringable) . PHP_EOL;

echo (string) $mixed . PHP_EOL;
echo strval($mixed) . PHP_EOL;

echo (int) $mixed . PHP_EOL;
echo intval($mixed) . PHP_EOL;

echo (float) $mixed . PHP_EOL;
echo floatval($mixed) . PHP_EOL;
echo doubleval($mixed) . PHP_EOL;
45 changes: 45 additions & 0 deletions tests/PHPStan/Rules/Functions/TypevalFamilyParametersRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleLevelHelper;
use PHPStan\Testing\RuleTestCase;
use function sprintf;
use const PHP_VERSION_ID;

/**
* @extends RuleTestCase<TypevalFamilyParametersRule>
*/
class TypevalFamilyParametersRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
$broker = $this->createReflectionProvider();
return new TypevalFamilyParametersRule(new RuleLevelHelper($broker, true, false, true, false));
}

public function testRule(): void
{
$paramName = '$value';
if (PHP_VERSION_ID < 80000) {
$paramName = '$var';
}
$this->analyse([__DIR__ . '/data/typeval.php'], [
[
sprintf('Parameter #1 %s of function intval does not accept object, class@anonymous/tests/PHPStan/Rules/Functions/data/typeval.php:3 given.', $paramName),
10,
],
[
sprintf('Parameter #1 %s of function floatval does not accept object, class@anonymous/tests/PHPStan/Rules/Functions/data/typeval.php:3 given.', $paramName),
13,
],
[
sprintf('Parameter #1 %s of function doubleval does not accept object, class@anonymous/tests/PHPStan/Rules/Functions/data/typeval.php:3 given.', $paramName),
16,
],
]);
}

}
16 changes: 16 additions & 0 deletions tests/PHPStan/Rules/Functions/data/typeval.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
$object = new stdClass();
$stringable = new class() {
public function __toString(): string {
return '';
}
};

echo intval($object);
echo intval($stringable);

echo floatval($object);
echo floatval($stringable);

echo doubleval($object);
echo doubleval($stringable);