From 8404ff05b592d5c267f9f447c6b4896e8f956df5 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:54:02 +0000 Subject: [PATCH 1/2] Report non-zero integer literals passed directly to parameters with allowed constants - FunctionCallParametersCheck: for parameters that have an allowed-constants definition (single or bitmask), walk the argument's `|` tree and report every hardcoded non-zero `Scalar\Int_` leaf (identifier `argument.integerLiteral`, tip "Use constants instead."). `0` stays allowed as the "no flags" value; variables and other dynamic operands are ignored. - Added the new message to all callers of the shared check: functions, methods, static methods, constructors, attributes, callables and call_user_func(). - Updated existing expectations for round() mode and PDOStatement::setFetchMode() which pass integer literals. Co-Authored-By: Claude Opus 5.5 --- src/Rules/AttributesCheck.php | 1 + src/Rules/Classes/InstantiationRule.php | 1 + src/Rules/FunctionCallParametersCheck.php | 38 +++++++++++++++++++ src/Rules/Functions/CallCallablesRule.php | 1 + .../CallToFunctionParametersRule.php | 1 + src/Rules/Functions/CallUserFuncRule.php | 1 + src/Rules/Methods/CallMethodsRule.php | 1 + src/Rules/Methods/CallStaticMethodsRule.php | 1 + .../Rules/Classes/InstantiationRuleTest.php | 5 +++ ...constant-parameter-check-instantiation.php | 3 ++ .../Rules/Functions/CallCallablesRuleTest.php | 5 +++ .../CallToFunctionParametersRuleTest.php | 32 ++++++++++++++++ .../Rules/Functions/CallUserFuncRuleTest.php | 5 +++ .../Rules/Functions/data/bug-14727.php | 24 ++++++++++++ ...onstant-parameter-check-call-user-func.php | 3 ++ .../constant-parameter-check-callables.php | 3 ++ .../Rules/Methods/CallMethodsRuleTest.php | 15 ++++++++ .../Methods/CallStaticMethodsRuleTest.php | 5 +++ .../data/constant-parameter-check-methods.php | 3 ++ .../data/constant-parameter-check-static.php | 3 ++ 20 files changed, 151 insertions(+) create mode 100644 tests/PHPStan/Rules/Functions/data/bug-14727.php diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index be32556cde5..5bace0d32f9 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -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 literal %s is not allowed for %s of attribute class ' . $attributeClassName . ' constructor.', null, ); diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index f0b4f4bf424..11fed568a61 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -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 literal %s is not allowed for %s of class ' . $classDisplayName . ' constructor.', null, )); } diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 40ad616d8ec..947d6207c50 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -98,6 +98,7 @@ public function check( string $invalidConstantMessage, string $exclusiveConstantsMessage, string $bitmaskNotAllowedMessage, + string $integerLiteralMessage, ?array $renamedNamedArgumentParameterData, ): array { @@ -453,6 +454,20 @@ public function check( $parameter instanceof ExtendedParameterReflection && $scope->getPhpVersion()->supportsNamedArguments()->yes() ) { + if ($parameter->getAllowedConstants() !== null) { + foreach ($this->findNonZeroIntegerLiterals($argumentValue) as $integerLiteral) { + $errors[] = RuleErrorBuilder::message(sprintf( + $integerLiteralMessage, + (string) $integerLiteral->value, + lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), + )) + ->identifier('argument.integerLiteral') + ->line($argumentLine) + ->tip('Use constants instead.') + ->build(); + } + } + $constantReflections = $this->resolveConstantReflections($argumentValue, $scope); if ($constantReflections !== null) { if ($parameter->getAllowedConstants() !== null) { @@ -860,6 +875,29 @@ private function resolveConstantReflections(Expr $expr, Scope $scope): ?array return null; } + /** + * @return list + */ + private function findNonZeroIntegerLiterals(Expr $expr): array + { + if ($expr instanceof Node\Scalar\Int_) { + if ($expr->value === 0) { + return []; + } + + return [$expr]; + } + + if ($expr instanceof Expr\BinaryOp\BitwiseOr) { + return [ + ...$this->findNonZeroIntegerLiterals($expr->left), + ...$this->findNonZeroIntegerLiterals($expr->right), + ]; + } + + return []; + } + private function callReturnsByReference(Expr $expr, Scope $scope): bool { if ($expr instanceof Node\Expr\MethodCall) { diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index 85c33490b1b..aedcacc5285 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -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 literal %s is not allowed for %s of ' . $callableDescription . '.', null, ), ); diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 06fe1ce0459..327081d6c0e 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -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 literal %s is not allowed for %s of function ' . $functionName . '.', null, ); } diff --git a/src/Rules/Functions/CallUserFuncRule.php b/src/Rules/Functions/CallUserFuncRule.php index cfc8f1955be..5e250723e90 100644 --- a/src/Rules/Functions/CallUserFuncRule.php +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -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 literal %s is not allowed for %s of ' . $callableDescription . '.', null, ); } diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index 61c7cd620c8..30404e12891 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -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 literal %s is not allowed for %s of method ' . $messagesMethodName . '.', !$methodReflection->isPrivate() && !$declaringClass->isFinal() ? [ $declaringClass->getName(), $methodReflection->getName(), diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index 53d797361e5..8cb19566d3e 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -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 literal %s is not allowed for %s of ' . $lowercasedMethodName . '.', null, )); diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index d518365eaf6..642f474ffb6 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -707,6 +707,11 @@ public function testConstantParameterCheckInstantiation(): void 'Constant IntlDateFormatter::GREGORIAN is not allowed for parameter #2 $dateType of class IntlDateFormatter constructor.', 18, ], + [ + 'Integer literal 16 is not allowed for parameter #1 $flags of class finfo constructor.', + 21, + 'Use constants instead.', + ], ]); } diff --git a/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php b/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php index 256c7639938..730a9c91b02 100644 --- a/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php +++ b/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php @@ -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(16 | FILEINFO_MIME_ENCODING); diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index 536bd63a807..190e9589771 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -373,6 +373,11 @@ public function testConstantParameterCheckCallables(): void 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of closure.', 10, ], + [ + 'Integer literal 128 is not allowed for parameter #2 $flags of closure.', + 13, + 'Use constants instead.', + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 0045da2ad19..d97be759a77 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2912,6 +2912,23 @@ public function testBug14312b(): void $this->analyse([__DIR__ . '/data/bug-14312b.php'], []); } + #[RequiresPhp('>= 8.0.0')] + public function testBug14727(): void + { + $tip = 'Use constants instead.'; + $this->analyse([__DIR__ . '/data/bug-14727.php'], [ + ['Integer literal 1 is not allowed for parameter #2 $flags of function json_encode.', 13, $tip], + ['Integer literal 1 is not allowed for parameter #2 $flags of function json_encode.', 14, $tip], + ['Integer literal 2 is not allowed for parameter #2 $flags of function json_encode.', 14, $tip], + ['Integer literal 2 is not allowed for parameter #2 $flags of function json_encode.', 15, $tip], + ['Integer literal 2 is not allowed for parameter #2 $flags of function json_encode.', 16, $tip], + ['Integer literal 64 is not allowed for parameter #2 $flags of function json_encode.', 20, $tip], + ['Integer literal 128 is not allowed for parameter $flags of function json_encode.', 21, $tip], + ['Integer literal 2 is not allowed for parameter #2 $flags of function array_unique.', 22, $tip], + ['Integer literal 4194304 is not allowed for parameter #4 $flags of function json_decode.', 24, $tip], + ]); + } + #[RequiresPhp('>= 8.0.0')] public function testConstantParameterCheck(): void { @@ -3226,6 +3243,21 @@ public function testLevenshteinArgumentsCount(): void public function testRoundModePhp84(): void { $this->analyse([__DIR__ . '/data/round-mode-php84.php'], [ + [ + 'Integer literal 5 is not allowed for parameter #3 $mode of function round.', + 8, + 'Use constants instead.', + ], + [ + 'Integer literal 8 is not allowed for parameter #3 $mode of function round.', + 9, + 'Use constants instead.', + ], + [ + 'Integer literal 9 is not allowed for parameter #3 $mode of function round.', + 11, + 'Use constants instead.', + ], [ 'Parameter #3 $mode of function round expects int<1, 8>|RoundingMode, 9 given.', 11, diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index be9e08217fe..bd80ffe6a06 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -197,6 +197,11 @@ public function testConstantParameterCheckCallUserFunc(): void 'Constant SORT_REGULAR is not allowed for parameter #2 $flags of callable passed to call_user_func().', 9, ], + [ + 'Integer literal 128 is not allowed for parameter #2 $flags of callable passed to call_user_func().', + 12, + 'Use constants instead.', + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/bug-14727.php b/tests/PHPStan/Rules/Functions/data/bug-14727.php new file mode 100644 index 00000000000..09bc599bd3c --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-14727.php @@ -0,0 +1,24 @@ += 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'], 0); +json_encode(['payload'], JSON_PRETTY_PRINT | 0); +json_encode(['payload'], JSON_PRETTY_PRINT | 64); +json_encode(['payload'], flags: 128); +array_unique([], 2); +json_decode('{}', true, 512, JSON_THROW_ON_ERROR); +json_decode('{}', true, 512, 4194304); diff --git a/tests/PHPStan/Rules/Functions/data/constant-parameter-check-call-user-func.php b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-call-user-func.php index 50b1ab12822..a804fde3e64 100644 --- a/tests/PHPStan/Rules/Functions/data/constant-parameter-check-call-user-func.php +++ b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-call-user-func.php @@ -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', [], 128); diff --git a/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php index 9fa10a94055..4c971f8ae64 100644 --- a/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php +++ b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php @@ -8,3 +8,6 @@ // Callable from a function name - wrong $encode([], SORT_REGULAR); + +// integer literal instead of constant +$encode([], 128); diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 328900e183e..9e08cb0b2fd 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3328,6 +3328,16 @@ public function testNamedParametersForMultiVariantFunctions(): void 'Unknown parameter $constructorArgs in call to method PDO::query().', 17, ], + [ + 'Integer literal 5 is not allowed for parameter $mode of method PDOStatement::setFetchMode().', + 20, + 'Use constants instead.', + ], + [ + 'Integer literal 5 is not allowed for parameter $mode of method PDOStatement::setFetchMode().', + 22, + 'Use constants instead.', + ], [ 'Unknown parameter $className in call to method PDOStatement::setFetchMode().', 22, @@ -4179,6 +4189,11 @@ public function testConstantParameterCheckMethods(): void 'Constants PDO::FETCH_ASSOC, PDO::FETCH_NUM cannot be combined for parameter $mode of method PDOStatement::setFetchMode().', 31, ], + [ + 'Integer literal 16 is not allowed for parameter #2 $flags of method finfo::file().', + 34, + 'Use constants instead.', + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index fbed93ba6c9..c2117d10fae 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -1062,6 +1062,11 @@ public function testConstantParameterCheckStatic(): void 'Constant NumberFormatter::TYPE_INT32 is not allowed for parameter #2 $style of static method NumberFormatter::create().', 15, ], + [ + 'Integer literal 1 is not allowed for parameter #2 $style of static method NumberFormatter::create().', + 18, + 'Use constants instead.', + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php index 4b69554a4b1..d5abfeebb69 100644 --- a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php +++ b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php @@ -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', 16); diff --git a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php index 16b7298f55c..35bb00fe0aa 100644 --- a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php +++ b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php @@ -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', 1); From 4896818baaebde0da4558e5cb7496a6de277e280 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 24 Sep 2026 07:16:29 +0000 Subject: [PATCH 2/2] Report only integer literals that cannot be built from allowed constants Instead of reporting every non-zero integer literal passed to a parameter with allowed constants, resolve the effective value of the literals in the argument (including literals combined with `|`) and report it only when it does not match an allowed constant (single-value parameters) or cannot be constructed by combining allowed constants (bitmask parameters). Co-Authored-By: Claude Opus 5.5 --- src/Reflection/ParameterAllowedConstants.php | 8 ++ src/Rules/AttributesCheck.php | 2 +- src/Rules/Classes/InstantiationRule.php | 2 +- src/Rules/FunctionCallParametersCheck.php | 97 ++++++++++++++++--- src/Rules/Functions/CallCallablesRule.php | 2 +- .../CallToFunctionParametersRule.php | 2 +- src/Rules/Functions/CallUserFuncRule.php | 2 +- src/Rules/Methods/CallMethodsRule.php | 2 +- src/Rules/Methods/CallStaticMethodsRule.php | 2 +- .../Rules/Classes/InstantiationRuleTest.php | 3 +- ...constant-parameter-check-instantiation.php | 2 +- .../Rules/Functions/CallCallablesRuleTest.php | 3 +- .../CallToFunctionParametersRuleTest.php | 30 +++--- .../Rules/Functions/CallUserFuncRuleTest.php | 3 +- .../Rules/Functions/data/bug-14727.php | 8 +- ...onstant-parameter-check-call-user-func.php | 2 +- .../constant-parameter-check-callables.php | 2 +- .../Rules/Methods/CallMethodsRuleTest.php | 13 +-- .../Methods/CallStaticMethodsRuleTest.php | 3 +- .../data/constant-parameter-check-methods.php | 2 +- .../data/constant-parameter-check-static.php | 2 +- 21 files changed, 127 insertions(+), 65 deletions(-) diff --git a/src/Reflection/ParameterAllowedConstants.php b/src/Reflection/ParameterAllowedConstants.php index 3400ea1239d..83e45e61f4e 100644 --- a/src/Reflection/ParameterAllowedConstants.php +++ b/src/Reflection/ParameterAllowedConstants.php @@ -39,6 +39,14 @@ public function isBitmask(): bool return $this->type === 'bitmask'; } + /** + * @return list + */ + public function getConstants(): array + { + return $this->constants; + } + /** * @return list> */ diff --git a/src/Rules/AttributesCheck.php b/src/Rules/AttributesCheck.php index 5bace0d32f9..6b01e0f618a 100644 --- a/src/Rules/AttributesCheck.php +++ b/src/Rules/AttributesCheck.php @@ -162,7 +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 literal %s 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, ); diff --git a/src/Rules/Classes/InstantiationRule.php b/src/Rules/Classes/InstantiationRule.php index 11fed568a61..6cd06663bd2 100644 --- a/src/Rules/Classes/InstantiationRule.php +++ b/src/Rules/Classes/InstantiationRule.php @@ -323,7 +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 literal %s is not allowed for %s of class ' . $classDisplayName . ' constructor.', + 'Integer %s does not correspond to constants allowed for %s of class ' . $classDisplayName . ' constructor.', null, )); } diff --git a/src/Rules/FunctionCallParametersCheck.php b/src/Rules/FunctionCallParametersCheck.php index 947d6207c50..64f6eba7615 100644 --- a/src/Rules/FunctionCallParametersCheck.php +++ b/src/Rules/FunctionCallParametersCheck.php @@ -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; @@ -38,6 +39,7 @@ 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; @@ -45,6 +47,7 @@ use function lcfirst; use function max; use function sprintf; +use function str_contains; #[AutowiredService] final class FunctionCallParametersCheck @@ -454,16 +457,17 @@ public function check( $parameter instanceof ExtendedParameterReflection && $scope->getPhpVersion()->supportsNamedArguments()->yes() ) { - if ($parameter->getAllowedConstants() !== null) { - foreach ($this->findNonZeroIntegerLiterals($argumentValue) as $integerLiteral) { + $allowedConstants = $parameter->getAllowedConstants(); + if ($allowedConstants !== null) { + $literalValue = $this->resolveIntegerLiteralValue($argumentValue); + if ($literalValue !== null && !$this->isIntegerValueAllowed($literalValue, $allowedConstants, $scope)) { $errors[] = RuleErrorBuilder::message(sprintf( $integerLiteralMessage, - (string) $integerLiteral->value, + (string) $literalValue, lcfirst($this->describeParameter($parameter, $argumentName ?? $i + 1)), )) - ->identifier('argument.integerLiteral') + ->identifier('argument.invalidIntegerLiteral') ->line($argumentLine) - ->tip('Use constants instead.') ->build(); } } @@ -876,26 +880,87 @@ private function resolveConstantReflections(Expr $expr, Scope $scope): ?array } /** - * @return list + * 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 findNonZeroIntegerLiterals(Expr $expr): array + private function resolveIntegerLiteralValue(Expr $expr): ?int { if ($expr instanceof Node\Scalar\Int_) { - if ($expr->value === 0) { - return []; + 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 [$expr]; + return $left | $right; } - if ($expr instanceof Expr\BinaryOp\BitwiseOr) { - return [ - ...$this->findNonZeroIntegerLiterals($expr->left), - ...$this->findNonZeroIntegerLiterals($expr->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 []; + return $values[0]; } private function callReturnsByReference(Expr $expr, Scope $scope): bool diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index aedcacc5285..c4e74e09694 100644 --- a/src/Rules/Functions/CallCallablesRule.php +++ b/src/Rules/Functions/CallCallablesRule.php @@ -144,7 +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 literal %s is not allowed for %s of ' . $callableDescription . '.', + 'Integer %s does not correspond to constants allowed for %s of ' . $callableDescription . '.', null, ), ); diff --git a/src/Rules/Functions/CallToFunctionParametersRule.php b/src/Rules/Functions/CallToFunctionParametersRule.php index 327081d6c0e..2fe9080e45c 100644 --- a/src/Rules/Functions/CallToFunctionParametersRule.php +++ b/src/Rules/Functions/CallToFunctionParametersRule.php @@ -77,7 +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 literal %s is not allowed for %s of function ' . $functionName . '.', + 'Integer %s does not correspond to constants allowed for %s of function ' . $functionName . '.', null, ); } diff --git a/src/Rules/Functions/CallUserFuncRule.php b/src/Rules/Functions/CallUserFuncRule.php index 5e250723e90..79c2cdb5dd0 100644 --- a/src/Rules/Functions/CallUserFuncRule.php +++ b/src/Rules/Functions/CallUserFuncRule.php @@ -97,7 +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 literal %s is not allowed for %s of ' . $callableDescription . '.', + 'Integer %s does not correspond to constants allowed for %s of ' . $callableDescription . '.', null, ); } diff --git a/src/Rules/Methods/CallMethodsRule.php b/src/Rules/Methods/CallMethodsRule.php index 30404e12891..d5c602905ba 100644 --- a/src/Rules/Methods/CallMethodsRule.php +++ b/src/Rules/Methods/CallMethodsRule.php @@ -114,7 +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 literal %s 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(), diff --git a/src/Rules/Methods/CallStaticMethodsRule.php b/src/Rules/Methods/CallStaticMethodsRule.php index 8cb19566d3e..7227a126bc4 100644 --- a/src/Rules/Methods/CallStaticMethodsRule.php +++ b/src/Rules/Methods/CallStaticMethodsRule.php @@ -123,7 +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 literal %s is not allowed for %s of ' . $lowercasedMethodName . '.', + 'Integer %s does not correspond to constants allowed for %s of ' . $lowercasedMethodName . '.', null, )); diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 642f474ffb6..5ee5d9034ad 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -708,9 +708,8 @@ public function testConstantParameterCheckInstantiation(): void 18, ], [ - 'Integer literal 16 is not allowed for parameter #1 $flags of class finfo constructor.', + 'Integer 4 does not correspond to constants allowed for parameter #1 $flags of class finfo constructor.', 21, - 'Use constants instead.', ], ]); } diff --git a/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php b/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php index 730a9c91b02..8884627e21a 100644 --- a/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php +++ b/tests/PHPStan/Rules/Classes/data/constant-parameter-check-instantiation.php @@ -18,4 +18,4 @@ new \IntlDateFormatter('en_US', \IntlDateFormatter::GREGORIAN, \IntlDateFormatter::SHORT); // integer literal instead of constant -new \finfo(16 | FILEINFO_MIME_ENCODING); +new \finfo(4 | FILEINFO_MIME_ENCODING); diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index 190e9589771..e1b93efcb04 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -374,9 +374,8 @@ public function testConstantParameterCheckCallables(): void 10, ], [ - 'Integer literal 128 is not allowed for parameter #2 $flags of closure.', + 'Integer 4096 does not correspond to constants allowed for parameter #2 $flags of closure.', 13, - 'Use constants instead.', ], ]); } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index d97be759a77..8f5a5b96cdf 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -2915,17 +2915,14 @@ public function testBug14312b(): void #[RequiresPhp('>= 8.0.0')] public function testBug14727(): void { - $tip = 'Use constants instead.'; $this->analyse([__DIR__ . '/data/bug-14727.php'], [ - ['Integer literal 1 is not allowed for parameter #2 $flags of function json_encode.', 13, $tip], - ['Integer literal 1 is not allowed for parameter #2 $flags of function json_encode.', 14, $tip], - ['Integer literal 2 is not allowed for parameter #2 $flags of function json_encode.', 14, $tip], - ['Integer literal 2 is not allowed for parameter #2 $flags of function json_encode.', 15, $tip], - ['Integer literal 2 is not allowed for parameter #2 $flags of function json_encode.', 16, $tip], - ['Integer literal 64 is not allowed for parameter #2 $flags of function json_encode.', 20, $tip], - ['Integer literal 128 is not allowed for parameter $flags of function json_encode.', 21, $tip], - ['Integer literal 2 is not allowed for parameter #2 $flags of function array_unique.', 22, $tip], - ['Integer literal 4194304 is not allowed for parameter #4 $flags of function json_decode.', 24, $tip], + ['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], ]); } @@ -3244,24 +3241,25 @@ public function testRoundModePhp84(): void { $this->analyse([__DIR__ . '/data/round-mode-php84.php'], [ [ - 'Integer literal 5 is not allowed for parameter #3 $mode of function round.', + 'Integer 5 does not correspond to constants allowed for parameter #3 $mode of function round.', 8, - 'Use constants instead.', ], [ - 'Integer literal 8 is not allowed for parameter #3 $mode of function round.', + 'Integer 8 does not correspond to constants allowed for parameter #3 $mode of function round.', 9, - 'Use constants instead.', ], [ - 'Integer literal 9 is not allowed for parameter #3 $mode of function round.', + 'Integer 9 does not correspond to constants allowed for parameter #3 $mode of function round.', 11, - 'Use constants instead.', ], [ '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, diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index bd80ffe6a06..23f968ba272 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -198,9 +198,8 @@ public function testConstantParameterCheckCallUserFunc(): void 9, ], [ - 'Integer literal 128 is not allowed for parameter #2 $flags of callable passed to call_user_func().', + 'Integer 4096 does not correspond to constants allowed for parameter #2 $flags of callable passed to call_user_func().', 12, - 'Use constants instead.', ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/bug-14727.php b/tests/PHPStan/Rules/Functions/data/bug-14727.php index 09bc599bd3c..83742f3eabc 100644 --- a/tests/PHPStan/Rules/Functions/data/bug-14727.php +++ b/tests/PHPStan/Rules/Functions/data/bug-14727.php @@ -14,11 +14,17 @@ class C { 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'], flags: 128); +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); diff --git a/tests/PHPStan/Rules/Functions/data/constant-parameter-check-call-user-func.php b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-call-user-func.php index a804fde3e64..0e2f273133b 100644 --- a/tests/PHPStan/Rules/Functions/data/constant-parameter-check-call-user-func.php +++ b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-call-user-func.php @@ -9,4 +9,4 @@ call_user_func('json_encode', [], SORT_REGULAR); // integer literal instead of constant -call_user_func('json_encode', [], 128); +call_user_func('json_encode', [], 4096); diff --git a/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php index 4c971f8ae64..2703fd67e8e 100644 --- a/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php +++ b/tests/PHPStan/Rules/Functions/data/constant-parameter-check-callables.php @@ -10,4 +10,4 @@ $encode([], SORT_REGULAR); // integer literal instead of constant -$encode([], 128); +$encode([], 4096); diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 9e08cb0b2fd..53ccaf5e053 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3328,16 +3328,6 @@ public function testNamedParametersForMultiVariantFunctions(): void 'Unknown parameter $constructorArgs in call to method PDO::query().', 17, ], - [ - 'Integer literal 5 is not allowed for parameter $mode of method PDOStatement::setFetchMode().', - 20, - 'Use constants instead.', - ], - [ - 'Integer literal 5 is not allowed for parameter $mode of method PDOStatement::setFetchMode().', - 22, - 'Use constants instead.', - ], [ 'Unknown parameter $className in call to method PDOStatement::setFetchMode().', 22, @@ -4190,9 +4180,8 @@ public function testConstantParameterCheckMethods(): void 31, ], [ - 'Integer literal 16 is not allowed for parameter #2 $flags of method finfo::file().', + 'Integer 4 does not correspond to constants allowed for parameter #2 $flags of method finfo::file().', 34, - 'Use constants instead.', ], ]); } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index c2117d10fae..5f8dd856b19 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -1063,9 +1063,8 @@ public function testConstantParameterCheckStatic(): void 15, ], [ - 'Integer literal 1 is not allowed for parameter #2 $style of static method NumberFormatter::create().', + 'Integer 100 does not correspond to constants allowed for parameter #2 $style of static method NumberFormatter::create().', 18, - 'Use constants instead.', ], ]); } diff --git a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php index d5abfeebb69..48d7b488295 100644 --- a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php +++ b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-methods.php @@ -31,4 +31,4 @@ $stmt->setFetchMode(mode: \PDO::FETCH_ASSOC | \PDO::FETCH_NUM); // integer literal instead of constant -$finfo->file('test.txt', 16); +$finfo->file('test.txt', 4); diff --git a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php index 35bb00fe0aa..f77d440edbf 100644 --- a/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php +++ b/tests/PHPStan/Rules/Methods/data/constant-parameter-check-static.php @@ -15,4 +15,4 @@ \NumberFormatter::create('en_US', \NumberFormatter::TYPE_INT32); // integer literal instead of constant -\NumberFormatter::create('en_US', 1); +\NumberFormatter::create('en_US', 100);