Skip to content

Commit cee6f68

Browse files
TRowbothamondrejmirtes
authored andcommitted
Add dynamic return type extension for mb_substitute_character
1 parent 853ef3f commit cee6f68

File tree

7 files changed

+240
-0
lines changed

7 files changed

+240
-0
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,6 +1253,11 @@ services:
12531253
tags:
12541254
- phpstan.broker.dynamicFunctionReturnTypeExtension
12551255

1256+
-
1257+
class: PHPStan\Type\Php\MbSubstituteCharacterDynamicReturnTypeExtension
1258+
tags:
1259+
- phpstan.broker.dynamicFunctionReturnTypeExtension
1260+
12561261
-
12571262
class: PHPStan\Type\Php\MicrotimeFunctionReturnTypeExtension
12581263
tags:

src/Php/PhpVersion.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,34 @@ public function throwsTypeErrorForInternalFunctions(): bool
9797
return $this->versionId >= 80000;
9898
}
9999

100+
public function throwsValueErrorForInternalFunctions(): bool
101+
{
102+
return $this->versionId >= 80000;
103+
}
104+
100105
public function supportsHhPrintfSpecifier(): bool
101106
{
102107
return $this->versionId >= 80000;
103108
}
104109

110+
public function isEmptyStringValidAliasForNoneInMbSubstituteCharacter(): bool
111+
{
112+
return $this->versionId < 80000;
113+
}
114+
115+
public function supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter(): bool
116+
{
117+
return $this->versionId >= 70200;
118+
}
119+
120+
public function isNumericStringValidArgInMbSubstituteCharacter(): bool
121+
{
122+
return $this->versionId < 80000;
123+
}
124+
125+
public function isNullValidArgInMbSubstituteCharacter(): bool
126+
{
127+
return $this->versionId >= 80000;
128+
}
129+
105130
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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\Type\BooleanType;
10+
use PHPStan\Type\Constant\ConstantBooleanType;
11+
use PHPStan\Type\Constant\ConstantIntegerType;
12+
use PHPStan\Type\Constant\ConstantStringType;
13+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
14+
use PHPStan\Type\IntegerRangeType;
15+
use PHPStan\Type\IntegerType;
16+
use PHPStan\Type\NeverType;
17+
use PHPStan\Type\NullType;
18+
use PHPStan\Type\StringType;
19+
use PHPStan\Type\Type;
20+
use PHPStan\Type\TypeCombinator;
21+
22+
class MbSubstituteCharacterDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
23+
{
24+
25+
private PhpVersion $phpVersion;
26+
27+
public function __construct(PhpVersion $phpVersion)
28+
{
29+
$this->phpVersion = $phpVersion;
30+
}
31+
32+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
33+
{
34+
return $functionReflection->getName() === 'mb_substitute_character';
35+
}
36+
37+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
38+
{
39+
$minCodePoint = $this->phpVersion->getVersionId() < 80000 ? 1 : 0;
40+
$maxCodePoint = $this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter() ? 0x10FFFF : 0xFFFE;
41+
$ranges = [];
42+
43+
if ($this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter()) {
44+
// Surrogates aren't valid in PHP 7.2+
45+
$ranges[] = IntegerRangeType::fromInterval($minCodePoint, 0xD7FF);
46+
$ranges[] = IntegerRangeType::fromInterval(0xE000, $maxCodePoint);
47+
} else {
48+
$ranges[] = IntegerRangeType::fromInterval($minCodePoint, $maxCodePoint);
49+
}
50+
51+
if (!isset($functionCall->args[0])) {
52+
return TypeCombinator::union(
53+
new ConstantStringType('none'),
54+
new ConstantStringType('long'),
55+
new ConstantStringType('entity'),
56+
...$ranges
57+
);
58+
}
59+
60+
$argType = $scope->getType($functionCall->args[0]->value);
61+
$isString = (new StringType())->isSuperTypeOf($argType);
62+
$isNull = (new NullType())->isSuperTypeOf($argType);
63+
$isInteger = (new IntegerType())->isSuperTypeOf($argType);
64+
65+
if ($isString->no() && $isNull->no() && $isInteger->no()) {
66+
if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) {
67+
return new NeverType();
68+
}
69+
70+
return new BooleanType();
71+
}
72+
73+
if ($isInteger->yes()) {
74+
$invalidRanges = [];
75+
76+
foreach ($ranges as $range) {
77+
$isInRange = $range->isSuperTypeOf($argType);
78+
79+
if ($isInRange->yes()) {
80+
return new ConstantBooleanType(true);
81+
}
82+
83+
$invalidRanges[] = $isInRange->no();
84+
}
85+
86+
if ($argType instanceof ConstantIntegerType || !in_array(false, $invalidRanges, true)) {
87+
if ($this->phpVersion->throwsValueErrorForInternalFunctions()) {
88+
return new NeverType();
89+
}
90+
91+
return new ConstantBooleanType(false);
92+
}
93+
} elseif ($isString->yes()) {
94+
if ($argType->isNonEmptyString()->no()) {
95+
// The empty string was a valid alias for "none" in PHP < 8.
96+
if ($this->phpVersion->isEmptyStringValidAliasForNoneInMbSubstituteCharacter()) {
97+
return new ConstantBooleanType(true);
98+
}
99+
100+
return new NeverType();
101+
}
102+
103+
if (!$this->phpVersion->isNumericStringValidArgInMbSubstituteCharacter() && $argType->isNumericString()->yes()) {
104+
return new NeverType();
105+
}
106+
107+
if ($argType instanceof ConstantStringType) {
108+
$value = strtolower($argType->getValue());
109+
110+
if ($value === 'none' || $value === 'long' || $value === 'entity') {
111+
return new ConstantBooleanType(true);
112+
}
113+
114+
if ($argType->isNumericString()->yes()) {
115+
$codePoint = (int) $value;
116+
$isValid = $codePoint >= $minCodePoint && $codePoint <= $maxCodePoint;
117+
118+
if ($this->phpVersion->supportsAllUnicodeScalarCodePointsInMbSubstituteCharacter()) {
119+
$isValid = $isValid && ($codePoint < 0xD800 || $codePoint > 0xDFFF);
120+
}
121+
122+
return new ConstantBooleanType($isValid);
123+
}
124+
125+
if ($this->phpVersion->throwsValueErrorForInternalFunctions()) {
126+
return new NeverType();
127+
}
128+
129+
return new ConstantBooleanType(false);
130+
}
131+
} elseif ($isNull->yes()) {
132+
// The $substitute_character arg is nullable in PHP 8+
133+
return new ConstantBooleanType($this->phpVersion->isNullValidArgInMbSubstituteCharacter());
134+
}
135+
136+
return new BooleanType();
137+
}
138+
139+
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,14 @@ public function dataFileAsserts(): iterable
453453
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5372.php');
454454
}
455455
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Arrays/data/bug-5372_2.php');
456+
457+
if (PHP_VERSION_ID >= 80000) {
458+
yield from $this->gatherAssertTypes(__DIR__ . '/data/mb_substitute_character-php8.php');
459+
} elseif (PHP_VERSION_ID < 70200) {
460+
yield from $this->gatherAssertTypes(__DIR__ . '/data/mb_substitute_character-php71.php');
461+
} else {
462+
yield from $this->gatherAssertTypes(__DIR__ . '/data/mb_substitute_character.php');
463+
}
456464
}
457465

458466
/**
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
\PHPStan\Testing\assertType('\'entity\'|\'long\'|\'none\'|int<1, 65534>', mb_substitute_character());
4+
\PHPStan\Testing\assertType('true', mb_substitute_character(''));
5+
\PHPStan\Testing\assertType('false', mb_substitute_character(null));
6+
\PHPStan\Testing\assertType('true', mb_substitute_character('none'));
7+
\PHPStan\Testing\assertType('true', mb_substitute_character('long'));
8+
\PHPStan\Testing\assertType('true', mb_substitute_character('entity'));
9+
\PHPStan\Testing\assertType('false', mb_substitute_character('foo'));
10+
\PHPStan\Testing\assertType('true', mb_substitute_character('123'));
11+
\PHPStan\Testing\assertType('true', mb_substitute_character('123.4'));
12+
\PHPStan\Testing\assertType('true', mb_substitute_character(0xFFFD));
13+
\PHPStan\Testing\assertType('false', mb_substitute_character(0x10FFFF));
14+
\PHPStan\Testing\assertType('false', mb_substitute_character(-1));
15+
\PHPStan\Testing\assertType('false', mb_substitute_character(0x110000));
16+
\PHPStan\Testing\assertType('bool', mb_substitute_character($undefined));
17+
\PHPStan\Testing\assertType('bool', mb_substitute_character(new stdClass()));
18+
\PHPStan\Testing\assertType('bool', mb_substitute_character(function () {}));
19+
\PHPStan\Testing\assertType('true', mb_substitute_character(rand(0xD800, 0xDFFF)));
20+
\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0, 0xDFFF)));
21+
\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0xD800, 0x10FFFF)));
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
\PHPStan\Testing\assertType('\'entity\'|\'long\'|\'none\'|int<0, 55295>|int<57344, 1114111>', mb_substitute_character());
4+
\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(''));
5+
\PHPStan\Testing\assertType('true', mb_substitute_character(null));
6+
\PHPStan\Testing\assertType('true', mb_substitute_character('none'));
7+
\PHPStan\Testing\assertType('true', mb_substitute_character('long'));
8+
\PHPStan\Testing\assertType('true', mb_substitute_character('entity'));
9+
\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('foo'));
10+
\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('123'));
11+
\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character('123.4'));
12+
\PHPStan\Testing\assertType('true', mb_substitute_character(0xFFFD));
13+
\PHPStan\Testing\assertType('true', mb_substitute_character(0x10FFFF));
14+
\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(-1));
15+
\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(0x110000));
16+
\PHPStan\Testing\assertType('bool', mb_substitute_character($undefined));
17+
\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(new stdClass()));
18+
\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(function () {}));
19+
\PHPStan\Testing\assertType('*NEVER*', mb_substitute_character(rand(0xD800, 0xDFFF)));
20+
\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0, 0xDFFF)));
21+
\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0xD800, 0x10FFFF)));
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
\PHPStan\Testing\assertType('\'entity\'|\'long\'|\'none\'|int<1, 55295>|int<57344, 1114111>', mb_substitute_character());
4+
\PHPStan\Testing\assertType('true', mb_substitute_character(''));
5+
\PHPStan\Testing\assertType('false', mb_substitute_character(null));
6+
\PHPStan\Testing\assertType('true', mb_substitute_character('none'));
7+
\PHPStan\Testing\assertType('true', mb_substitute_character('long'));
8+
\PHPStan\Testing\assertType('true', mb_substitute_character('entity'));
9+
\PHPStan\Testing\assertType('false', mb_substitute_character('foo'));
10+
\PHPStan\Testing\assertType('true', mb_substitute_character('123'));
11+
\PHPStan\Testing\assertType('true', mb_substitute_character('123.4'));
12+
\PHPStan\Testing\assertType('true', mb_substitute_character(0xFFFD));
13+
\PHPStan\Testing\assertType('true', mb_substitute_character(0x10FFFF));
14+
\PHPStan\Testing\assertType('false', mb_substitute_character(-1));
15+
\PHPStan\Testing\assertType('false', mb_substitute_character(0x110000));
16+
\PHPStan\Testing\assertType('bool', mb_substitute_character($undefined));
17+
\PHPStan\Testing\assertType('bool', mb_substitute_character(new stdClass()));
18+
\PHPStan\Testing\assertType('bool', mb_substitute_character(function () {}));
19+
\PHPStan\Testing\assertType('false', mb_substitute_character(rand(0xD800, 0xDFFF)));
20+
\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0, 0xDFFF)));
21+
\PHPStan\Testing\assertType('bool', mb_substitute_character(rand(0xD800, 0x10FFFF)));

0 commit comments

Comments
 (0)