Skip to content

Commit 6490b8d

Browse files
phpstan-botVincentLangletclaude
authored
Add array-shape return type for localeconv() and shape-based localtime() return type extension (phpstan#6031)
Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e5e4e4c commit 6490b8d

3 files changed

Lines changed: 113 additions & 1 deletion

File tree

resources/functionMap.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5449,7 +5449,7 @@
54495449
'locale_lookup' => ['string|null', 'langtag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'],
54505450
'locale_parse' => ['array|null', 'locale'=>'string'],
54515451
'locale_set_default' => ['bool', 'locale'=>'string'],
5452-
'localeconv' => ['array'],
5452+
'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<int>, mon_grouping: array<int, int>}'],
54535453
'localtime' => ['array', 'timestamp='=>'int', 'associative_array='=>'bool'],
54545454
'log' => ['float', 'number'=>'float', 'base='=>'float'],
54555455
'log10' => ['float', 'number'=>'float'],
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\DependencyInjection\AutowiredService;
8+
use PHPStan\Reflection\FunctionReflection;
9+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
10+
use PHPStan\Type\Constant\ConstantBooleanType;
11+
use PHPStan\Type\Constant\ConstantStringType;
12+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
13+
use PHPStan\Type\IntegerRangeType;
14+
use PHPStan\Type\IntegerType;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeCombinator;
17+
use function count;
18+
19+
#[AutowiredService]
20+
final class LocaltimeFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
21+
{
22+
23+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
24+
{
25+
return $functionReflection->getName() === 'localtime';
26+
}
27+
28+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
29+
{
30+
$args = $functionCall->getArgs();
31+
32+
$associativeType = count($args) >= 2 ? $scope->getType($args[1]->value)->toBoolean() : new ConstantBooleanType(false);
33+
34+
if ($associativeType->isTrue()->yes()) {
35+
return $this->createAssociativeType();
36+
}
37+
38+
if ($associativeType->isFalse()->yes()) {
39+
return $this->createListType();
40+
}
41+
42+
return TypeCombinator::union($this->createListType(), $this->createAssociativeType());
43+
}
44+
45+
private function createListType(): Type
46+
{
47+
$builder = ConstantArrayTypeBuilder::createEmpty();
48+
foreach ($this->createFieldTypes() as [, $valueType]) {
49+
$builder->setOffsetValueType(null, $valueType);
50+
}
51+
52+
return $builder->getArray();
53+
}
54+
55+
private function createAssociativeType(): Type
56+
{
57+
$builder = ConstantArrayTypeBuilder::createEmpty();
58+
foreach ($this->createFieldTypes() as [$key, $valueType]) {
59+
$builder->setOffsetValueType(new ConstantStringType($key), $valueType);
60+
}
61+
62+
return $builder->getArray();
63+
}
64+
65+
/**
66+
* Fields of the C localtime struct in order, with the value ranges documented at
67+
* https://www.php.net/manual/en/function.localtime.php
68+
*
69+
* @return list<array{string, Type}>
70+
*/
71+
private function createFieldTypes(): array
72+
{
73+
return [
74+
['tm_sec', IntegerRangeType::fromInterval(0, 59)],
75+
['tm_min', IntegerRangeType::fromInterval(0, 59)],
76+
['tm_hour', IntegerRangeType::fromInterval(0, 23)],
77+
['tm_mday', IntegerRangeType::fromInterval(1, 31)],
78+
['tm_mon', IntegerRangeType::fromInterval(0, 11)],
79+
['tm_year', new IntegerType()],
80+
['tm_wday', IntegerRangeType::fromInterval(0, 6)],
81+
['tm_yday', IntegerRangeType::fromInterval(0, 365)],
82+
['tm_isdst', new IntegerType()],
83+
];
84+
}
85+
86+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace LocaleconvBug;
4+
5+
use function localeconv;
6+
use function localtime;
7+
use function PHPStan\Testing\assertType;
8+
9+
function (): void {
10+
$conv = localeconv();
11+
12+
assertType('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<int>, mon_grouping: array<int, int>}', $conv);
13+
14+
assertType('string', $conv['thousands_sep']);
15+
assertType('string', $conv['decimal_point']);
16+
assertType('int', $conv['frac_digits']);
17+
assertType('list<int>', $conv['grouping']);
18+
};
19+
20+
function (int $timestamp, bool $assoc): void {
21+
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());
22+
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));
23+
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));
24+
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));
25+
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));
26+
};

0 commit comments

Comments
 (0)