Skip to content

Commit 3bdec09

Browse files
Merge pull request #108 from sascha-egerer/issue-107
TASK: Add MathUtilityTypeSpecifyingExtension
2 parents a5121d6 + 79658fd commit 3bdec09

File tree

5 files changed

+371
-1
lines changed

5 files changed

+371
-1
lines changed

extension.neon

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ services:
8585
class: Bnf\PhpstanPsrContainer\ContainerDynamicReturnTypeExtension
8686
tags:
8787
- phpstan.broker.dynamicMethodReturnTypeExtension
88-
88+
-
89+
class: SaschaEgerer\PhpstanTypo3\Type\MathUtilityTypeSpecifyingExtension
90+
tags:
91+
- phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension
8992
parameters:
9093
bootstrapFiles:
9194
- phpstan.bootstrap.php

phpstan.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ parameters:
1414
excludePaths:
1515
- '*tests/*/Fixture/*'
1616
- '*tests/*/Source/*'
17+
- '*tests/*/data/*'
1718
ignoreErrors:
1819
-
1920
message: '#^Class TYPO3\\CMS\\Core\\Context\\[a-zA-Z]* not found\.#'
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SaschaEgerer\PhpstanTypo3\Type;
4+
5+
use PhpParser\Node\Arg;
6+
use PhpParser\Node\Expr\Assign;
7+
use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
8+
use PhpParser\Node\Expr\BinaryOp\GreaterOrEqual;
9+
use PhpParser\Node\Expr\BinaryOp\SmallerOrEqual;
10+
use PhpParser\Node\Expr\BooleanNot;
11+
use PhpParser\Node\Expr\FuncCall;
12+
use PhpParser\Node\Expr\StaticCall;
13+
use PhpParser\Node\Name;
14+
use PhpParser\Node\Scalar\LNumber;
15+
use PHPStan\Analyser\Scope;
16+
use PHPStan\Analyser\SpecifiedTypes;
17+
use PHPStan\Analyser\TypeSpecifier;
18+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
19+
use PHPStan\Analyser\TypeSpecifierContext;
20+
use PHPStan\Reflection\MethodReflection;
21+
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
22+
use TYPO3\CMS\Core\Utility\MathUtility;
23+
24+
final class MathUtilityTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
25+
{
26+
27+
private const METHOD_FORCE_INTEGER_IN_RANGE = 'forceIntegerInRange';
28+
private const METHOD_CONVERT_TO_POSITIVE_INTEGER = 'convertToPositiveInteger';
29+
private const METHOD_CAN_BE_INTERPRETED_AS_INTEGER = 'canBeInterpretedAsInteger';
30+
private const METHOD_CAN_BE_INTERPRETED_AS_FLOAT = 'canBeInterpretedAsFloat';
31+
private const METHOD_IS_INTEGER_IN_RANGE = 'isIntegerInRange';
32+
33+
/** @var TypeSpecifier */
34+
private $typeSpecifier;
35+
36+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
37+
{
38+
$this->typeSpecifier = $typeSpecifier;
39+
}
40+
41+
public function getClass(): string
42+
{
43+
return MathUtility::class;
44+
}
45+
46+
public function isStaticMethodSupported(MethodReflection $staticMethodReflection, StaticCall $node, TypeSpecifierContext $context): bool
47+
{
48+
return in_array(
49+
$staticMethodReflection->getName(),
50+
[
51+
self::METHOD_FORCE_INTEGER_IN_RANGE,
52+
self::METHOD_CONVERT_TO_POSITIVE_INTEGER,
53+
self::METHOD_CAN_BE_INTERPRETED_AS_INTEGER,
54+
self::METHOD_CAN_BE_INTERPRETED_AS_FLOAT,
55+
self::METHOD_IS_INTEGER_IN_RANGE,
56+
],
57+
true
58+
);
59+
}
60+
61+
public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
62+
{
63+
if ($staticMethodReflection->getName() === self::METHOD_FORCE_INTEGER_IN_RANGE) {
64+
return $this->specifyTypesForForceIntegerInRange($node, $scope);
65+
}
66+
67+
if ($staticMethodReflection->getName() === self::METHOD_IS_INTEGER_IN_RANGE) {
68+
return $this->specifyTypesForIsIntegerInRange($node, $scope);
69+
}
70+
71+
if ($staticMethodReflection->getName() === self::METHOD_CONVERT_TO_POSITIVE_INTEGER) {
72+
return $this->specifyTypesForConvertToPositiveInteger($node, $scope);
73+
}
74+
75+
if ($staticMethodReflection->getName() === self::METHOD_CAN_BE_INTERPRETED_AS_INTEGER) {
76+
return $this->specifyTypesForCanBeInterpretedAsInteger($node, $scope);
77+
}
78+
79+
return $this->specifyTypesForCanBeInterpretedAsFloat($node, $scope);
80+
}
81+
82+
private function specifyTypesForForceIntegerInRange(StaticCall $node, Scope $scope): SpecifiedTypes
83+
{
84+
$parentNode = $node->getAttribute('parent');
85+
86+
if (!$parentNode instanceof Assign) {
87+
return new SpecifiedTypes();
88+
}
89+
90+
$min = isset($node->getArgs()[1]) ? $node->getArgs()[1]->value : new LNumber(0);
91+
$max = isset($node->getArgs()[2]) ? $node->getArgs()[2]->value : new LNumber(2000000000);
92+
93+
return $this->typeSpecifier->specifyTypesInCondition(
94+
$scope,
95+
new BooleanAnd(
96+
new FuncCall(
97+
new Name('is_int'),
98+
[new Arg($parentNode->var)]
99+
),
100+
new BooleanAnd(
101+
new GreaterOrEqual(
102+
$parentNode->var,
103+
$min
104+
),
105+
new SmallerOrEqual(
106+
$parentNode->var,
107+
$max
108+
)
109+
)
110+
),
111+
TypeSpecifierContext::createTruthy()
112+
);
113+
}
114+
115+
private function specifyTypesForIsIntegerInRange(StaticCall $node, Scope $scope): SpecifiedTypes
116+
{
117+
$firstArgument = $node->getArgs()[0];
118+
$firstArgumentType = $scope->getType($firstArgument->value);
119+
120+
$min = $node->getArgs()[1]->value;
121+
$max = $node->getArgs()[2]->value;
122+
123+
if ($firstArgumentType->isString()->no()) {
124+
$typeCheckFuncCall = new FuncCall(
125+
new Name('is_int'),
126+
[$firstArgument]
127+
);
128+
} else {
129+
$typeCheckFuncCall = new BooleanAnd(
130+
new FuncCall(
131+
new Name('is_numeric'),
132+
[$firstArgument]
133+
),
134+
new BooleanNot(
135+
new FuncCall(
136+
new Name('is_float'),
137+
[$firstArgument]
138+
)
139+
)
140+
);
141+
}
142+
143+
return $this->typeSpecifier->specifyTypesInCondition(
144+
$scope,
145+
new BooleanAnd(
146+
$typeCheckFuncCall,
147+
new BooleanAnd(
148+
new GreaterOrEqual(
149+
$firstArgument->value,
150+
$min
151+
),
152+
new SmallerOrEqual(
153+
$firstArgument->value,
154+
$max
155+
)
156+
)
157+
),
158+
TypeSpecifierContext::createTruthy()
159+
);
160+
}
161+
162+
private function specifyTypesForConvertToPositiveInteger(StaticCall $node, Scope $scope): SpecifiedTypes
163+
{
164+
$parentNode = $node->getAttribute('parent');
165+
166+
if (!$parentNode instanceof Assign) {
167+
return new SpecifiedTypes();
168+
}
169+
170+
return $this->typeSpecifier->specifyTypesInCondition(
171+
$scope,
172+
new BooleanAnd(
173+
new FuncCall(
174+
new Name('is_int'),
175+
[new Arg($parentNode)]
176+
),
177+
new BooleanAnd(
178+
new GreaterOrEqual(
179+
$parentNode->var,
180+
new LNumber(0)
181+
),
182+
new SmallerOrEqual(
183+
$parentNode->var,
184+
new LNumber(PHP_INT_MAX)
185+
)
186+
)
187+
),
188+
TypeSpecifierContext::createTruthy()
189+
);
190+
}
191+
192+
private function specifyTypesForCanBeInterpretedAsInteger(StaticCall $node, Scope $scope): SpecifiedTypes
193+
{
194+
$firstArgument = $node->getArgs()[0];
195+
196+
return $this->typeSpecifier->specifyTypesInCondition(
197+
$scope,
198+
new FuncCall(
199+
new Name('is_numeric'),
200+
[$firstArgument]
201+
),
202+
TypeSpecifierContext::createTruthy()
203+
);
204+
}
205+
206+
private function specifyTypesForCanBeInterpretedAsFloat(StaticCall $node, Scope $scope): SpecifiedTypes
207+
{
208+
$firstArgument = $node->getArgs()[0];
209+
210+
return $this->typeSpecifier->specifyTypesInCondition(
211+
$scope,
212+
new FuncCall(
213+
new Name('is_float'),
214+
[$firstArgument]
215+
),
216+
TypeSpecifierContext::createTruthy()
217+
);
218+
}
219+
220+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SaschaEgerer\PhpstanTypo3\Tests\Unit\Type\MathUtilityTypeSpecifyingExtension;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
final class MathUtilityTypeSpecifyingExtensionTest extends TypeInferenceTestCase
8+
{
9+
10+
/**
11+
* @return iterable<mixed>
12+
*/
13+
public function dataFileAsserts(): iterable
14+
{
15+
yield from $this->gatherAssertTypes(__DIR__ . '/data/MathUtilityTest.php');
16+
}
17+
18+
/**
19+
* @dataProvider dataFileAsserts
20+
* @param string $assertType
21+
* @param string $file
22+
* @param mixed ...$args
23+
*/
24+
public function testFileAsserts(
25+
string $assertType,
26+
string $file,
27+
...$args
28+
): void
29+
{
30+
$this->assertFileAsserts($assertType, $file, ...$args);
31+
}
32+
33+
public static function getAdditionalConfigFiles(): array
34+
{
35+
return [
36+
__DIR__ . '/../../../../extension.neon',
37+
];
38+
}
39+
40+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SaschaEgerer\PhpstanTypo3\Tests\Unit\Type\MathUtilityTypeSpecifyingExtension\data;
4+
5+
use TYPO3\CMS\Core\Utility\MathUtility;
6+
use function PHPStan\Testing\assertType;
7+
8+
// phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingAnyTypeHint
9+
final class MathUtilityTest
10+
{
11+
12+
public function forceIntegerInRangeWithMinAndMaxValueDefined(int $theInt): void
13+
{
14+
$forceIntegerInRange = MathUtility::forceIntegerInRange($theInt, 0, 100);
15+
assertType('int<0, 100>', $forceIntegerInRange);
16+
}
17+
18+
public function forceIntegerInRangeWithoutMaxValueSpecified(int $theInt): void
19+
{
20+
$forceIntegerInRange = MathUtility::forceIntegerInRange($theInt, 0);
21+
assertType('int<0, 2000000000>', $forceIntegerInRange);
22+
}
23+
24+
public function forceIntegerInRangeWithMixedType($theInt): void
25+
{
26+
$forceIntegerInRange = MathUtility::forceIntegerInRange($theInt, 4);
27+
assertType('int<4, 2000000000>', $forceIntegerInRange);
28+
}
29+
30+
public function isIntegerInRange($a, string $b, float $c, int $d, int $e, int $f, int $min, int $max): void
31+
{
32+
if (MathUtility::isIntegerInRange($a, 0, 200)) {
33+
return;
34+
}
35+
36+
if (MathUtility::isIntegerInRange($b, 0, 200)) {
37+
return;
38+
}
39+
40+
if (MathUtility::isIntegerInRange($c, 0, 200)) {
41+
return;
42+
}
43+
44+
if (MathUtility::isIntegerInRange($d, $min, $max)) {
45+
return;
46+
}
47+
48+
if (MathUtility::isIntegerInRange($e, 0, $max)) {
49+
return;
50+
}
51+
52+
if (MathUtility::isIntegerInRange($f, $min, 200)) {
53+
return;
54+
}
55+
56+
assertType('int<0, 200>|numeric-string', $a);
57+
assertType('numeric-string', $b);
58+
assertType('*NEVER*', $c);
59+
assertType('int', $d);
60+
assertType('int<0, max>', $e);
61+
assertType('int<min, 200>', $f);
62+
}
63+
64+
public function convertToPositiveInteger($a, $b = -1): void
65+
{
66+
$positiveInteger1 = MathUtility::convertToPositiveInteger($a);
67+
$positiveInteger2 = MathUtility::convertToPositiveInteger($b);
68+
69+
assertType('int<0, max>', $positiveInteger1);
70+
assertType('int<0, max>', $positiveInteger2);
71+
}
72+
73+
public function canBeInterpretedAsInteger($a, int $b, float $c, string $d): void
74+
{
75+
if (MathUtility::canBeInterpretedAsInteger($a)) {
76+
return;
77+
}
78+
79+
if (MathUtility::canBeInterpretedAsInteger($b)) {
80+
return;
81+
}
82+
83+
if (MathUtility::canBeInterpretedAsInteger($c)) {
84+
return;
85+
}
86+
87+
if (MathUtility::canBeInterpretedAsInteger($d)) {
88+
return;
89+
}
90+
91+
assertType('float|int|numeric-string', $a);
92+
assertType('int', $b);
93+
assertType('float', $c);
94+
assertType('numeric-string', $d);
95+
}
96+
97+
public function canBeInterpretedAsFloat($theFloat): void
98+
{
99+
if (MathUtility::canBeInterpretedAsFloat($theFloat)) {
100+
return;
101+
}
102+
103+
assertType('float', $theFloat);
104+
}
105+
106+
}

0 commit comments

Comments
 (0)