Skip to content

Commit 6506d87

Browse files
committed
Merge branch '1.5.x' into 1.6.x
2 parents 3cfec29 + c19a822 commit 6506d87

14 files changed

+418
-84
lines changed

build/baseline-8.0.neon

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ parameters:
77

88
-
99
message: "#^Strict comparison using \\=\\=\\= between array and false will always evaluate to false\\.$#"
10+
count: 2
11+
path: ../src/Type/Php/MbFunctionsReturnTypeExtensionTrait.php
12+
13+
-
14+
message: "#^Strict comparison using \\=\\=\\= between int<0, max> and false will always evaluate to false\\.$#"
1015
count: 1
11-
path: ../src/Type/Php/MbFunctionsReturnTypeExtension.php
16+
path: ../src/Type/Php/MbStrlenFunctionReturnTypeExtension.php
1217

1318
-
1419
message: "#^Strict comparison using \\=\\=\\= between non-empty-array<int, string> and false will always evaluate to false\\.$#"

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,11 @@ services:
13461346
tags:
13471347
- phpstan.broker.dynamicFunctionReturnTypeExtension
13481348

1349+
-
1350+
class: PHPStan\Type\Php\MbStrlenFunctionReturnTypeExtension
1351+
tags:
1352+
- phpstan.broker.dynamicFunctionReturnTypeExtension
1353+
13491354
-
13501355
class: PHPStan\Type\Php\MicrotimeFunctionReturnTypeExtension
13511356
tags:

src/Php/PhpVersion.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,9 @@ public function throwsOnInvalidMbStringEncoding(): bool
181181
return $this->versionId >= 80000;
182182
}
183183

184+
public function supportsPassNoneEncodings(): bool
185+
{
186+
return $this->versionId < 70300;
187+
}
188+
184189
}

src/Type/Php/MbFunctionsReturnTypeExtension.php

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use PHPStan\Php\PhpVersion;
88
use PHPStan\Reflection\FunctionReflection;
99
use PHPStan\Reflection\ParametersAcceptorSelector;
10-
use PHPStan\ShouldNotHappenException;
1110
use PHPStan\Type\BooleanType;
1211
use PHPStan\Type\Constant\ConstantBooleanType;
1312
use PHPStan\Type\Constant\ConstantStringType;
@@ -20,57 +19,33 @@
2019
use PHPStan\Type\UnionType;
2120
use function array_key_exists;
2221
use function array_map;
23-
use function array_merge;
2422
use function array_unique;
2523
use function count;
26-
use function function_exists;
27-
use function in_array;
28-
use function mb_encoding_aliases;
29-
use function mb_list_encodings;
30-
use function strtoupper;
3124

3225
class MbFunctionsReturnTypeExtension implements DynamicFunctionReturnTypeExtension
3326
{
3427

35-
/** @var string[] */
36-
private array $supportedEncodings;
28+
use MbFunctionsReturnTypeExtensionTrait;
3729

3830
/** @var int[] */
3931
private array $encodingPositionMap = [
4032
'mb_http_output' => 1,
4133
'mb_regex_encoding' => 1,
4234
'mb_internal_encoding' => 1,
4335
'mb_encoding_aliases' => 1,
44-
'mb_strlen' => 2,
4536
'mb_chr' => 2,
4637
'mb_ord' => 2,
4738
];
4839

4940
public function __construct(private PhpVersion $phpVersion)
5041
{
51-
$supportedEncodings = [];
52-
if (function_exists('mb_list_encodings')) {
53-
foreach (mb_list_encodings() as $encoding) {
54-
$aliases = mb_encoding_aliases($encoding);
55-
if ($aliases === false) {
56-
throw new ShouldNotHappenException();
57-
}
58-
$supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]);
59-
}
60-
}
61-
$this->supportedEncodings = array_map('strtoupper', $supportedEncodings);
6242
}
6343

6444
public function isFunctionSupported(FunctionReflection $functionReflection): bool
6545
{
6646
return array_key_exists($functionReflection->getName(), $this->encodingPositionMap);
6747
}
6848

69-
private function isSupportedEncoding(string $encoding): bool
70-
{
71-
return in_array(strtoupper($encoding), $this->supportedEncodings, true);
72-
}
73-
7449
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
7550
{
7651
$returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PHPStan\ShouldNotHappenException;
6+
use function array_filter;
7+
use function array_map;
8+
use function array_merge;
9+
use function function_exists;
10+
use function in_array;
11+
use function is_null;
12+
use function mb_encoding_aliases;
13+
use function mb_list_encodings;
14+
use function strtoupper;
15+
16+
trait MbFunctionsReturnTypeExtensionTrait
17+
{
18+
19+
/** @var string[]|null */
20+
private ?array $supportedEncodings = null;
21+
22+
private function isSupportedEncoding(string $encoding): bool
23+
{
24+
return in_array(strtoupper($encoding), $this->getSupportedEncodings(), true);
25+
}
26+
27+
/** @return string[] */
28+
private function getSupportedEncodings(): array
29+
{
30+
if (!is_null($this->supportedEncodings)) {
31+
return $this->supportedEncodings;
32+
}
33+
34+
$supportedEncodings = [];
35+
if (function_exists('mb_list_encodings')) {
36+
foreach (mb_list_encodings() as $encoding) {
37+
$aliases = mb_encoding_aliases($encoding);
38+
if ($aliases === false) {
39+
throw new ShouldNotHappenException();
40+
}
41+
$supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]);
42+
}
43+
}
44+
$this->supportedEncodings = array_map('strtoupper', $supportedEncodings);
45+
46+
// PHP 7.3 and 7.4 claims 'pass' and its alias 'none' to be supported, but actually 'pass' was removed in 7.3
47+
if (!$this->phpVersion->supportsPassNoneEncodings()) {
48+
$this->supportedEncodings = array_filter(
49+
$this->supportedEncodings,
50+
static fn (string $enc) => !in_array($enc, ['PASS', 'NONE'], true),
51+
);
52+
}
53+
54+
return $this->supportedEncodings;
55+
}
56+
57+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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\Php\PhpVersion;
8+
use PHPStan\Reflection\FunctionReflection;
9+
use PHPStan\Reflection\ParametersAcceptorSelector;
10+
use PHPStan\ShouldNotHappenException;
11+
use PHPStan\Type\BooleanType;
12+
use PHPStan\Type\Constant\ConstantBooleanType;
13+
use PHPStan\Type\Constant\ConstantIntegerType;
14+
use PHPStan\Type\Constant\ConstantStringType;
15+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
16+
use PHPStan\Type\FloatType;
17+
use PHPStan\Type\IntegerRangeType;
18+
use PHPStan\Type\IntegerType;
19+
use PHPStan\Type\NeverType;
20+
use PHPStan\Type\StringType;
21+
use PHPStan\Type\Type;
22+
use PHPStan\Type\TypeCombinator;
23+
use PHPStan\Type\TypeUtils;
24+
use function array_map;
25+
use function array_merge;
26+
use function array_unique;
27+
use function count;
28+
use function in_array;
29+
use function max;
30+
use function mb_internal_encoding;
31+
use function mb_strlen;
32+
use function min;
33+
use function range;
34+
use function sort;
35+
use function sprintf;
36+
use function var_export;
37+
38+
class MbStrlenFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
39+
{
40+
41+
private const UNSUPPORTED_ENCODING = 'unsupported';
42+
43+
use MbFunctionsReturnTypeExtensionTrait;
44+
45+
public function __construct(private PhpVersion $phpVersion)
46+
{
47+
}
48+
49+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
50+
{
51+
return $functionReflection->getName() === 'mb_strlen';
52+
}
53+
54+
public function getTypeFromFunctionCall(
55+
FunctionReflection $functionReflection,
56+
FuncCall $functionCall,
57+
Scope $scope,
58+
): Type
59+
{
60+
$args = $functionCall->getArgs();
61+
$returnType = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
62+
if (count($args) === 0) {
63+
return $returnType;
64+
}
65+
66+
$encodings = [];
67+
68+
if (count($functionCall->getArgs()) === 1) {
69+
// there is a chance to get an unsupported encoding 'pass' or 'none' here on PHP 7.3-7.4
70+
$encodings = [mb_internal_encoding()];
71+
} elseif (count($functionCall->getArgs()) === 2) { // custom encoding is specified
72+
$encodings = array_map(
73+
static fn (ConstantStringType $t) => $t->getValue(),
74+
TypeUtils::getConstantStrings($scope->getType($functionCall->getArgs()[1]->value)),
75+
);
76+
}
77+
78+
if (count($encodings) > 0) {
79+
for ($i = 0; $i < count($encodings); $i++) {
80+
if ($this->isSupportedEncoding($encodings[$i])) {
81+
continue;
82+
}
83+
$encodings[$i] = self::UNSUPPORTED_ENCODING;
84+
}
85+
86+
$encodings = array_unique($encodings);
87+
88+
if (in_array(self::UNSUPPORTED_ENCODING, $encodings, true) && count($encodings) === 1) {
89+
if ($this->phpVersion->throwsOnInvalidMbStringEncoding()) {
90+
return new NeverType();
91+
}
92+
return new ConstantBooleanType(false);
93+
}
94+
} else { // if there aren't encoding constants, use all available encodings
95+
$encodings = array_merge($this->getSupportedEncodings(), [self::UNSUPPORTED_ENCODING]);
96+
}
97+
98+
$argType = $scope->getType($args[0]->value);
99+
100+
if ($argType->isSuperTypeOf(new BooleanType())->yes()) {
101+
$constantScalars = TypeUtils::getConstantScalars(TypeCombinator::remove($argType, new BooleanType()));
102+
if (count($constantScalars) > 0) {
103+
$constantScalars[] = new ConstantBooleanType(true);
104+
$constantScalars[] = new ConstantBooleanType(false);
105+
}
106+
} else {
107+
$constantScalars = TypeUtils::getConstantScalars($argType);
108+
}
109+
110+
$lengths = [];
111+
foreach ($constantScalars as $constantScalar) {
112+
$stringScalar = $constantScalar->toString();
113+
if (!($stringScalar instanceof ConstantStringType)) {
114+
$lengths = [];
115+
break;
116+
}
117+
118+
foreach ($encodings as $encoding) {
119+
if (!$this->isSupportedEncoding($encoding)) {
120+
continue;
121+
}
122+
123+
$length = mb_strlen($stringScalar->getValue(), $encoding);
124+
if ($length === false) {
125+
throw new ShouldNotHappenException(sprintf('Got false on a supported encoding %s and value %s', $encoding, var_export($stringScalar->getValue(), true)));
126+
}
127+
$lengths[] = $length;
128+
}
129+
}
130+
131+
$range = null;
132+
$isNonEmpty = $argType->isNonEmptyString();
133+
$numeric = TypeCombinator::union(new IntegerType(), new FloatType());
134+
if (count($lengths) > 0) {
135+
$lengths = array_unique($lengths);
136+
sort($lengths);
137+
if ($lengths === range(min($lengths), max($lengths))) {
138+
$range = IntegerRangeType::fromInterval(min($lengths), max($lengths));
139+
} else {
140+
$range = TypeCombinator::union(...array_map(static fn ($l) => new ConstantIntegerType($l), $lengths));
141+
}
142+
} elseif ((new BooleanType())->isSuperTypeOf($argType)->yes()) {
143+
$range = IntegerRangeType::fromInterval(0, 1);
144+
} elseif (
145+
$isNonEmpty->yes()
146+
|| $numeric->isSuperTypeOf($argType)->yes()
147+
|| TypeCombinator::remove($argType, $numeric)->isNonEmptyString()->yes()
148+
) {
149+
$range = IntegerRangeType::fromInterval(1, null);
150+
} elseif ((new StringType())->isSuperTypeOf($argType)->yes() && $isNonEmpty->no()) {
151+
$range = new ConstantIntegerType(0);
152+
} else {
153+
$range = TypeCombinator::remove(
154+
ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(),
155+
new ConstantBooleanType(false),
156+
);
157+
}
158+
159+
if (!$this->phpVersion->throwsOnInvalidMbStringEncoding() && in_array(self::UNSUPPORTED_ENCODING, $encodings, true)) {
160+
return TypeCombinator::union($range, new ConstantBooleanType(false));
161+
}
162+
return $range;
163+
}
164+
165+
}

src/Type/Php/StrSplitFunctionReturnTypeExtension.php

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpParser\Node\Expr\FuncCall;
66
use PHPStan\Analyser\Scope;
7+
use PHPStan\Php\PhpVersion;
78
use PHPStan\Reflection\FunctionReflection;
89
use PHPStan\Reflection\ParametersAcceptorSelector;
910
use PHPStan\ShouldNotHappenException;
@@ -20,37 +21,20 @@
2021
use PHPStan\Type\TypeCombinator;
2122
use PHPStan\Type\TypeUtils;
2223
use function array_map;
23-
use function array_merge;
2424
use function array_unique;
2525
use function count;
26-
use function function_exists;
2726
use function in_array;
28-
use function mb_encoding_aliases;
2927
use function mb_internal_encoding;
30-
use function mb_list_encodings;
3128
use function mb_str_split;
3229
use function str_split;
33-
use function strtoupper;
3430

3531
final class StrSplitFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
3632
{
3733

38-
/** @var string[] */
39-
private array $supportedEncodings;
34+
use MbFunctionsReturnTypeExtensionTrait;
4035

41-
public function __construct()
36+
public function __construct(private PhpVersion $phpVersion)
4237
{
43-
$supportedEncodings = [];
44-
if (function_exists('mb_list_encodings')) {
45-
foreach (mb_list_encodings() as $encoding) {
46-
$aliases = mb_encoding_aliases($encoding);
47-
if ($aliases === false) {
48-
throw new ShouldNotHappenException();
49-
}
50-
$supportedEncodings = array_merge($supportedEncodings, $aliases, [$encoding]);
51-
}
52-
}
53-
$this->supportedEncodings = array_map('strtoupper', $supportedEncodings);
5438
}
5539

5640
public function isFunctionSupported(FunctionReflection $functionReflection): bool
@@ -119,11 +103,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
119103
return self::createConstantArrayFrom($items, $scope);
120104
}
121105

122-
private function isSupportedEncoding(string $encoding): bool
123-
{
124-
return in_array(strtoupper($encoding), $this->supportedEncodings, true);
125-
}
126-
127106
/**
128107
* @param string[] $constantArray
129108
*/

0 commit comments

Comments
 (0)