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
8 changes: 8 additions & 0 deletions src/Reflection/ParameterAllowedConstants.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ public function isBitmask(): bool
return $this->type === 'bitmask';
}

/**
* @return list<string>
*/
public function getConstants(): array
{
return $this->constants;
}

/**
* @return list<list<string>>
*/
Expand Down
1 change: 1 addition & 0 deletions src/Rules/AttributesCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ public function check(
'Constant %s is not allowed for %s of attribute class ' . $attributeClassName . ' constructor.',
'Constants %s cannot be combined for %s of attribute class ' . $attributeClassName . ' constructor.',
'Combining constants with | is not allowed for %s of attribute class ' . $attributeClassName . ' constructor.',
'Integer %s does not correspond to constants allowed for %s of attribute class ' . $attributeClassName . ' constructor.',
null,
);

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 @@ -323,6 +323,7 @@ private function checkClassName(string $class, bool $isName, Node $node, Scope&N
'Constant %s is not allowed for %s of class ' . $classDisplayName . ' constructor.',
'Constants %s cannot be combined for %s of class ' . $classDisplayName . ' constructor.',
'Combining constants with | is not allowed for %s of class ' . $classDisplayName . ' constructor.',
'Integer %s does not correspond to constants allowed for %s of class ' . $classDisplayName . ' constructor.',
null,
));
}
Expand Down
103 changes: 103 additions & 0 deletions src/Rules/FunctionCallParametersCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\ConstantReflection;
use PHPStan\Reflection\ExtendedParameterReflection;
use PHPStan\Reflection\ParameterAllowedConstants;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ReflectionProvider;
Expand All @@ -38,13 +39,15 @@
use function array_last;
use function array_merge;
use function count;
use function explode;
use function implode;
use function in_array;
use function is_int;
use function is_string;
use function lcfirst;
use function max;
use function sprintf;
use function str_contains;

#[AutowiredService]
final class FunctionCallParametersCheck
Expand Down Expand Up @@ -98,6 +101,7 @@ public function check(
string $invalidConstantMessage,
string $exclusiveConstantsMessage,
string $bitmaskNotAllowedMessage,
string $integerLiteralMessage,
?array $renamedNamedArgumentParameterData,
): array
{
Expand Down Expand Up @@ -453,6 +457,21 @@ public function check(
$parameter instanceof ExtendedParameterReflection
&& $scope->getPhpVersion()->supportsNamedArguments()->yes()
) {
$allowedConstants = $parameter->getAllowedConstants();
if ($allowedConstants !== null) {
$literalValue = $this->resolveIntegerLiteralValue($argumentValue);
if ($literalValue !== null && !$this->isIntegerValueAllowed($literalValue, $allowedConstants, $scope)) {
$errors[] = RuleErrorBuilder::message(sprintf(
$integerLiteralMessage,
(string) $literalValue,
lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)),
))
->identifier('argument.invalidIntegerLiteral')
->line($argumentLine)
->build();
}
}

$constantReflections = $this->resolveConstantReflections($argumentValue, $scope);
if ($constantReflections !== null) {
if ($parameter->getAllowedConstants() !== null) {
Expand Down Expand Up @@ -860,6 +879,90 @@ private function resolveConstantReflections(Expr $expr, Scope $scope): ?array
return null;
}

/**
* Combines integer literals found directly in the argument, including literals
* inside a bitmask built with `|`. Returns null when there are no such literals.
*/
private function resolveIntegerLiteralValue(Expr $expr): ?int
{
if ($expr instanceof Node\Scalar\Int_) {
return $expr->value;
}

if ($expr instanceof Expr\BinaryOp\BitwiseOr) {
$left = $this->resolveIntegerLiteralValue($expr->left);
$right = $this->resolveIntegerLiteralValue($expr->right);
if ($left === null) {
return $right;
}
if ($right === null) {
return $left;
}

return $left | $right;
}

return null;
}

/**
* Single-value parameters accept only a value of one of the allowed constants.
* Bitmask parameters accept any value that can be built by combining allowed constants with `|`.
*/
private function isIntegerValueAllowed(int $value, ParameterAllowedConstants $allowedConstants, Scope $scope): bool
{
$constantValues = [];
foreach ($allowedConstants->getConstants() as $constantName) {
$constantValue = $this->resolveIntegerConstantValue($constantName, $scope);
if ($constantValue === null) {
continue;
}

$constantValues[] = $constantValue;
}

if ($constantValues === []) {
return true;
}

if (!$allowedConstants->isBitmask()) {
return in_array($value, $constantValues, true);
}

$constructibleValue = 0;
foreach ($constantValues as $constantValue) {
if (($constantValue & ~$value) !== 0) {
continue;
}

$constructibleValue |= $constantValue;
}

return $constructibleValue === $value;
}

private function resolveIntegerConstantValue(string $constantName, Scope $scope): ?int
{
if (str_contains($constantName, '::')) {
[$className, $classConstantName] = explode('::', $constantName, 2);
if ($className === '' || $classConstantName === '') {
return null;
}
$constantFetch = new Expr\ClassConstFetch(new Node\Name\FullyQualified($className), $classConstantName);
} elseif ($constantName !== '') {
$constantFetch = new Expr\ConstFetch(new Node\Name\FullyQualified($constantName));
} else {
return null;
}

$values = $scope->getType($constantFetch)->getConstantScalarValues();
if (count($values) !== 1 || !is_int($values[0])) {
return null;
}

return $values[0];
}

private function callReturnsByReference(Expr $expr, Scope $scope): bool
{
if ($expr instanceof Node\Expr\MethodCall) {
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 @@ -144,6 +144,7 @@ public function processNode(
'Constant %s is not allowed for %s of ' . $callableDescription . '.',
'Constants %s cannot be combined for %s of ' . $callableDescription . '.',
'Combining constants with | is not allowed for %s of ' . $callableDescription . '.',
'Integer %s does not correspond to constants allowed for %s of ' . $callableDescription . '.',
null,
),
);
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 @@ -77,6 +77,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE
'Constant %s is not allowed for %s of function ' . $functionName . '.',
'Constants %s cannot be combined for %s of function ' . $functionName . '.',
'Combining constants with | is not allowed for %s of function ' . $functionName . '.',
'Integer %s does not correspond to constants allowed for %s of function ' . $functionName . '.',
null,
);
}
Expand Down
1 change: 1 addition & 0 deletions src/Rules/Functions/CallUserFuncRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE
'Constant %s is not allowed for %s of ' . $callableDescription . '.',
'Constants %s cannot be combined for %s of ' . $callableDescription . '.',
'Combining constants with | is not allowed for %s of ' . $callableDescription . '.',
'Integer %s does not correspond to constants allowed for %s of ' . $callableDescription . '.',
null,
);
}
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 @@ -114,6 +114,7 @@ private function processSingleMethodCall(Scope&NodeCallbackInvoker&CollectedData
'Constant %s is not allowed for %s of method ' . $messagesMethodName . '.',
'Constants %s cannot be combined for %s of method ' . $messagesMethodName . '.',
'Combining constants with | is not allowed for %s of method ' . $messagesMethodName . '.',
'Integer %s does not correspond to constants allowed for %s of method ' . $messagesMethodName . '.',
!$methodReflection->isPrivate() && !$declaringClass->isFinal() ? [
$declaringClass->getName(),
$methodReflection->getName(),
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 @@ -123,6 +123,7 @@ private function processSingleMethodCall(Scope&NodeCallbackInvoker&CollectedData
'Constant %s is not allowed for %s of ' . $lowercasedMethodName . '.',
'Constants %s cannot be combined for %s of ' . $lowercasedMethodName . '.',
'Combining constants with | is not allowed for %s of ' . $lowercasedMethodName . '.',
'Integer %s does not correspond to constants allowed for %s of ' . $lowercasedMethodName . '.',
null,
));

Expand Down
4 changes: 4 additions & 0 deletions tests/PHPStan/Rules/Classes/InstantiationRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,10 @@ public function testConstantParameterCheckInstantiation(): void
'Constant IntlDateFormatter::GREGORIAN is not allowed for parameter #2 $dateType of class IntlDateFormatter constructor.',
18,
],
[
'Integer 4 does not correspond to constants allowed for parameter #1 $flags of class finfo constructor.',
21,
],
]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,6 @@

// IntlDateFormatter::__construct - wrong constant for $dateType
new \IntlDateFormatter('en_US', \IntlDateFormatter::GREGORIAN, \IntlDateFormatter::SHORT);

// integer literal instead of constant
new \finfo(4 | FILEINFO_MIME_ENCODING);
4 changes: 4 additions & 0 deletions tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,10 @@ public function testConstantParameterCheckCallables(): void
'Constant SORT_REGULAR is not allowed for parameter #2 $flags of closure.',
10,
],
[
'Integer 4096 does not correspond to constants allowed for parameter #2 $flags of closure.',
13,
],
]);
}

Expand Down
30 changes: 30 additions & 0 deletions tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2912,6 +2912,20 @@ public function testBug14312b(): void
$this->analyse([__DIR__ . '/data/bug-14312b.php'], []);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug14727(): void
{
$this->analyse([__DIR__ . '/data/bug-14727.php'], [
['Integer 4096 does not correspond to constants allowed for parameter #2 $flags of function json_encode.', 17],
['Integer 4097 does not correspond to constants allowed for parameter #2 $flags of function json_encode.', 18],
['Integer 8192 does not correspond to constants allowed for parameter #2 $flags of function json_encode.', 19],
['Integer 4096 does not correspond to constants allowed for parameter #2 $flags of function json_encode.', 24],
['Integer 4096 does not correspond to constants allowed for parameter $flags of function json_encode.', 25],
['Integer 3 does not correspond to constants allowed for parameter #2 $flags of function array_unique.', 27],
['Integer 4 does not correspond to constants allowed for parameter #4 $flags of function json_decode.', 30],
]);
}

#[RequiresPhp('>= 8.0.0')]
public function testConstantParameterCheck(): void
{
Expand Down Expand Up @@ -3226,10 +3240,26 @@ public function testLevenshteinArgumentsCount(): void
public function testRoundModePhp84(): void
{
$this->analyse([__DIR__ . '/data/round-mode-php84.php'], [
[
'Integer 5 does not correspond to constants allowed for parameter #3 $mode of function round.',
8,
],
[
'Integer 8 does not correspond to constants allowed for parameter #3 $mode of function round.',
9,
],
[
'Integer 9 does not correspond to constants allowed for parameter #3 $mode of function round.',
11,
],
[
'Parameter #3 $mode of function round expects int<1, 8>|RoundingMode, 9 given.',
11,
],
[
'Integer 0 does not correspond to constants allowed for parameter #3 $mode of function round.',
12,
],
[
'Parameter #3 $mode of function round expects int<1, 8>|RoundingMode, 0 given.',
12,
Expand Down
4 changes: 4 additions & 0 deletions tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ public function testConstantParameterCheckCallUserFunc(): void
'Constant SORT_REGULAR is not allowed for parameter #2 $flags of callable passed to call_user_func().',
9,
],
[
'Integer 4096 does not correspond to constants allowed for parameter #2 $flags of callable passed to call_user_func().',
12,
],
]);
}

Expand Down
30 changes: 30 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-14727.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug14727;

$one = 1;

class C {
public static int $unknownInt;
}

json_encode(['payload'], 1);
json_encode(['payload'], 1 | 2);
json_encode(['payload'], $one | 2);
json_encode(['payload'], C::$unknownInt | 2);
json_encode(['payload'], 4096);
json_encode(['payload'], 1 | 4096);
json_encode(['payload'], $one | 8192);

json_encode(['payload'], 0);
json_encode(['payload'], JSON_PRETTY_PRINT | 0);
json_encode(['payload'], JSON_PRETTY_PRINT | 64);
json_encode(['payload'], JSON_PRETTY_PRINT | 4096);
json_encode(['payload'], flags: 4096);
array_unique([], 2);
array_unique([], 3);
json_decode('{}', true, 512, JSON_THROW_ON_ERROR);
json_decode('{}', true, 512, 4194304);
json_decode('{}', true, 512, 4);
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@

// call_user_func with wrong constant
call_user_func('json_encode', [], SORT_REGULAR);

// integer literal instead of constant
call_user_func('json_encode', [], 4096);
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@

// Callable from a function name - wrong
$encode([], SORT_REGULAR);

// integer literal instead of constant
$encode([], 4096);
4 changes: 4 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4179,6 +4179,10 @@ public function testConstantParameterCheckMethods(): void
'Constants PDO::FETCH_ASSOC, PDO::FETCH_NUM cannot be combined for parameter $mode of method PDOStatement::setFetchMode().',
31,
],
[
'Integer 4 does not correspond to constants allowed for parameter #2 $flags of method finfo::file().',
34,
],
]);
}

Expand Down
4 changes: 4 additions & 0 deletions tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,10 @@ public function testConstantParameterCheckStatic(): void
'Constant NumberFormatter::TYPE_INT32 is not allowed for parameter #2 $style of static method NumberFormatter::create().',
15,
],
[
'Integer 100 does not correspond to constants allowed for parameter #2 $style of static method NumberFormatter::create().',
18,
],
]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,6 @@

// PDOStatement::setFetchMode - exclusive base modes via named argument (multi-variant method)
$stmt->setFetchMode(mode: \PDO::FETCH_ASSOC | \PDO::FETCH_NUM);

// integer literal instead of constant
$finfo->file('test.txt', 4);
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@

// NumberFormatter::create - wrong constant for $style
\NumberFormatter::create('en_US', \NumberFormatter::TYPE_INT32);

// integer literal instead of constant
\NumberFormatter::create('en_US', 100);
Loading