Skip to content

Commit 5ca8540

Browse files
committed
More precise types after Option::filter()
1 parent d896f81 commit 5ca8540

File tree

4 files changed

+201
-0
lines changed

4 files changed

+201
-0
lines changed

extension.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@ services:
2020
class: Psl\PHPStan\Type\MatchesTypeSpecifyingExtension
2121
tags:
2222
- phpstan.typeSpecifier.methodTypeSpecifyingExtension
23+
24+
-
25+
class: Psl\PHPStan\Option\OptionFilterReturnTypeExtension
26+
tags:
27+
- phpstan.broker.dynamicMethodReturnTypeExtension
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Psl\PHPStan\Option;
4+
5+
use PhpParser\Node\Arg;
6+
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\FuncCall;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PhpParser\Node\Name;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Node\Expr\TypeExpr;
12+
use PHPStan\Reflection\MethodReflection;
13+
use PHPStan\Type\ArrayType;
14+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
15+
use PHPStan\Type\Generic\GenericObjectType;
16+
use PHPStan\Type\IntegerType;
17+
use PHPStan\Type\Type;
18+
use Psl\Option\Option;
19+
20+
class OptionFilterReturnTypeExtension implements DynamicMethodReturnTypeExtension
21+
{
22+
23+
public function getClass(): string
24+
{
25+
return Option::class;
26+
}
27+
28+
public function isMethodSupported(MethodReflection $methodReflection): bool
29+
{
30+
return $methodReflection->getName() === 'filter';
31+
}
32+
33+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
34+
{
35+
$args = $methodCall->getArgs();
36+
if (!isset($args[0])) {
37+
return null;
38+
}
39+
$filterCallback = $args[0]->value;
40+
41+
$optionType = $scope->getType($methodCall->var);
42+
$originalType = $optionType->getTemplateType(Option::class, 'T');
43+
44+
$refinedType = $this->analyzeFilterCallback($filterCallback, $originalType, $scope);
45+
46+
return new GenericObjectType(
47+
Option::class,
48+
[$refinedType]
49+
);
50+
}
51+
52+
private function analyzeFilterCallback(Expr $filterCallback, Type $originalType, Scope $scope): Type
53+
{
54+
$arrayType = new ArrayType(new IntegerType(), $originalType);
55+
56+
$refinedType = $scope
57+
->getType(
58+
new FuncCall(
59+
new Name('array_filter'),
60+
[new Arg(new TypeExpr($arrayType)), new Arg($filterCallback)]
61+
)
62+
)
63+
->getIterableValueType();
64+
65+
if (!$refinedType->equals($originalType)) {
66+
return $refinedType;
67+
}
68+
69+
return $originalType;
70+
}
71+
72+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Psl\PHPStan\Option;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
class PslTypeSpecifyingExtensionTest extends TypeInferenceTestCase
8+
{
9+
10+
/**
11+
* @return iterable<mixed>
12+
*/
13+
public function dataFileAsserts(): iterable
14+
{
15+
yield from $this->gatherAssertTypes(__DIR__ . '/data/filter.php');
16+
}
17+
18+
/**
19+
* @dataProvider dataFileAsserts
20+
* @param mixed ...$args
21+
*/
22+
public function testFileAsserts(
23+
string $assertType,
24+
string $file,
25+
...$args
26+
): void
27+
{
28+
$this->assertFileAsserts($assertType, $file, ...$args);
29+
}
30+
31+
/***
32+
* @return string[]
33+
*/
34+
public static function getAdditionalConfigFiles(): array
35+
{
36+
return [__DIR__ . '/../../extension.neon'];
37+
}
38+
39+
}

tests/Option/data/filter.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PslOptionTest;
6+
7+
use Psl\Option;
8+
9+
use function PHPStan\Testing\assertType;
10+
11+
12+
function positive_int(int $value): void
13+
{
14+
$option = Option\some($value);
15+
$option = $option->filter(fn($value) => $value > 0);
16+
assertType('Psl\Option\Option<int<1, max>>', $option);
17+
18+
$option = Option\some($value);
19+
$option = $option->filter(\Psl\Type\positive_int()->matches(...));
20+
assertType('Psl\Option\Option<int<1, max>>', $option);
21+
}
22+
23+
function non_empty_string(string $value): void
24+
{
25+
$option = Option\some($value);
26+
$option = $option->filter(fn($value) => '' !== $value);
27+
assertType('Psl\Option\Option<non-empty-string>', $option);
28+
29+
$option = Option\some($value);
30+
$option = $option->filter(\Psl\Type\non_empty_string()->matches(...));
31+
assertType('Psl\Option\Option<non-empty-string>', $option);
32+
}
33+
34+
35+
function numeric_string(string $value): void
36+
{
37+
$option = Option\some($value);
38+
$option = $option->filter(fn($value) => is_numeric($value));
39+
assertType('Psl\Option\Option<numeric-string>', $option);
40+
41+
$option = Option\some($value);
42+
$option = $option->filter(is_numeric(...));
43+
assertType('Psl\Option\Option<numeric-string>', $option);
44+
45+
$option = Option\some($value);
46+
$option = $option->filter(\Psl\Type\numeric_string()->matches(...));
47+
assertType('Psl\Option\Option<numeric-string>', $option);
48+
}
49+
50+
function literal_string(string $value): void
51+
{
52+
$option = Option\some($value);
53+
$option = $option->filter(fn($value) => 'potato' === $value);
54+
assertType('Psl\Option\Option<\'potato\'>', $option);
55+
56+
$option = Option\some($value);
57+
$option = $option->filter(fn ($value) => 'potato' === $value || 'tomato' === $value);
58+
assertType('Psl\Option\Option<\'potato\'|\'tomato\'>', $option);
59+
60+
$option = Option\some($value);
61+
$option = $option->filter(fn ($value) => in_array($value, ['potato', 'tomato'], true));
62+
assertType('Psl\Option\Option<\'potato\'|\'tomato\'>', $option);
63+
64+
$option = Option\some($value);
65+
$option = $option->filter(\Psl\Type\literal_string('potato')->matches(...));
66+
assertType('Psl\Option\Option<\'potato\'>', $option);
67+
}
68+
69+
70+
/**
71+
* @param list<float> $value
72+
*/
73+
function filter_list(array $value): void
74+
{
75+
$option = Option\some($value);
76+
assertType('Psl\Option\Option<list<float>>', $option);
77+
$option = $option->filter(fn($value) => [] !== $value);
78+
assertType('Psl\Option\Option<non-empty-list<float>>', $option);
79+
80+
$option = Option\some($value);
81+
assertType('Psl\Option\Option<list<float>>', $option);
82+
$option = $option->filter(\Psl\Type\non_empty_list()->matches(...));
83+
assertType('Psl\Option\Option<non-empty-list<float>>', $option);
84+
}
85+

0 commit comments

Comments
 (0)