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 be32556cde5..6b01e0f618a 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 %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 f0b4f4bf424..6cd06663bd2 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 %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 40ad616d8ec..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 @@ -98,6 +101,7 @@ public function check( string $invalidConstantMessage, string $exclusiveConstantsMessage, string $bitmaskNotAllowedMessage, + string $integerLiteralMessage, ?array $renamedNamedArgumentParameterData, ): array { @@ -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) { @@ -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) { diff --git a/src/Rules/Functions/CallCallablesRule.php b/src/Rules/Functions/CallCallablesRule.php index 85c33490b1b..c4e74e09694 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 %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 06fe1ce0459..2fe9080e45c 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 %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 cfc8f1955be..79c2cdb5dd0 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 %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 61c7cd620c8..d5c602905ba 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 %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 53d797361e5..7227a126bc4 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 %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 d518365eaf6..5ee5d9034ad 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -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, + ], ]); } 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..8884627e21a 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(4 | FILEINFO_MIME_ENCODING); diff --git a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php index 536bd63a807..e1b93efcb04 100644 --- a/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php @@ -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, + ], ]); } diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 0045da2ad19..8f5a5b96cdf 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -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 { @@ -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, diff --git a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php index be9e08217fe..23f968ba272 100644 --- a/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallUserFuncRuleTest.php @@ -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, + ], ]); } 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..83742f3eabc --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-14727.php @@ -0,0 +1,30 @@ += 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); 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..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 @@ -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); 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..2703fd67e8e 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([], 4096); diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 328900e183e..53ccaf5e053 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -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, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index fbed93ba6c9..5f8dd856b19 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -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, + ], ]); } 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..48d7b488295 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', 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 16b7298f55c..f77d440edbf 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', 100);