diff --git a/resources/functionMap.php b/resources/functionMap.php index 07bda44a29e..a216ff776a5 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -5449,7 +5449,7 @@ 'locale_lookup' => ['string|null', 'langtag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], 'locale_parse' => ['array|null', 'locale'=>'string'], 'locale_set_default' => ['bool', 'locale'=>'string'], -'localeconv' => ['array'], +'localeconv' => ['array{decimal_point: string, thousands_sep: string, int_curr_symbol: string, currency_symbol: string, mon_decimal_point: string, mon_thousands_sep: string, positive_sign: string, negative_sign: string, int_frac_digits: int, frac_digits: int, p_cs_precedes: int, p_sep_by_space: int, n_cs_precedes: int, n_sep_by_space: int, p_sign_posn: int, n_sign_posn: int, grouping: list, mon_grouping: array}'], 'localtime' => ['array', 'timestamp='=>'int', 'associative_array='=>'bool'], 'log' => ['float', 'number'=>'float', 'base='=>'float'], 'log10' => ['float', 'number'=>'float'], diff --git a/src/Type/Php/LocaltimeFunctionDynamicReturnTypeExtension.php b/src/Type/Php/LocaltimeFunctionDynamicReturnTypeExtension.php new file mode 100644 index 00000000000..a58dd234c33 --- /dev/null +++ b/src/Type/Php/LocaltimeFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,86 @@ +getName() === 'localtime'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + $args = $functionCall->getArgs(); + + $associativeType = count($args) >= 2 ? $scope->getType($args[1]->value)->toBoolean() : new ConstantBooleanType(false); + + if ($associativeType->isTrue()->yes()) { + return $this->createAssociativeType(); + } + + if ($associativeType->isFalse()->yes()) { + return $this->createListType(); + } + + return TypeCombinator::union($this->createListType(), $this->createAssociativeType()); + } + + private function createListType(): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($this->createFieldTypes() as [, $valueType]) { + $builder->setOffsetValueType(null, $valueType); + } + + return $builder->getArray(); + } + + private function createAssociativeType(): Type + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + foreach ($this->createFieldTypes() as [$key, $valueType]) { + $builder->setOffsetValueType(new ConstantStringType($key), $valueType); + } + + return $builder->getArray(); + } + + /** + * Fields of the C localtime struct in order, with the value ranges documented at + * https://www.php.net/manual/en/function.localtime.php + * + * @return list + */ + private function createFieldTypes(): array + { + return [ + ['tm_sec', IntegerRangeType::fromInterval(0, 59)], + ['tm_min', IntegerRangeType::fromInterval(0, 59)], + ['tm_hour', IntegerRangeType::fromInterval(0, 23)], + ['tm_mday', IntegerRangeType::fromInterval(1, 31)], + ['tm_mon', IntegerRangeType::fromInterval(0, 11)], + ['tm_year', new IntegerType()], + ['tm_wday', IntegerRangeType::fromInterval(0, 6)], + ['tm_yday', IntegerRangeType::fromInterval(0, 365)], + ['tm_isdst', new IntegerType()], + ]; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/localeconv.php b/tests/PHPStan/Analyser/nsrt/localeconv.php new file mode 100644 index 00000000000..94f25139fe5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/localeconv.php @@ -0,0 +1,26 @@ +, mon_grouping: array}', $conv); + + assertType('string', $conv['thousands_sep']); + assertType('string', $conv['decimal_point']); + assertType('int', $conv['frac_digits']); + assertType('list', $conv['grouping']); +}; + +function (int $timestamp, bool $assoc): void { + assertType('array{int<0, 59>, int<0, 59>, int<0, 23>, int<1, 31>, int<0, 11>, int, int<0, 6>, int<0, 365>, int}', localtime()); + assertType('array{int<0, 59>, int<0, 59>, int<0, 23>, int<1, 31>, int<0, 11>, int, int<0, 6>, int<0, 365>, int}', localtime($timestamp)); + assertType('array{int<0, 59>, int<0, 59>, int<0, 23>, int<1, 31>, int<0, 11>, int, int<0, 6>, int<0, 365>, int}', localtime($timestamp, false)); + assertType('array{tm_sec: int<0, 59>, tm_min: int<0, 59>, tm_hour: int<0, 23>, tm_mday: int<1, 31>, tm_mon: int<0, 11>, tm_year: int, tm_wday: int<0, 6>, tm_yday: int<0, 365>, tm_isdst: int}', localtime($timestamp, true)); + assertType('array{int<0, 59>, int<0, 59>, int<0, 23>, int<1, 31>, int<0, 11>, int, int<0, 6>, int<0, 365>, int}|array{tm_sec: int<0, 59>, tm_min: int<0, 59>, tm_hour: int<0, 23>, tm_mday: int<1, 31>, tm_mon: int<0, 11>, tm_year: int, tm_wday: int<0, 6>, tm_yday: int<0, 365>, tm_isdst: int}', localtime($timestamp, $assoc)); +};