Skip to content

Commit af9e18f

Browse files
committed
add NarrowingValidatorCompiler
1 parent 8204dce commit af9e18f

20 files changed

+307
-14
lines changed

phpstan.neon.dist

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,11 @@ parameters:
4545
-
4646
message: '#throws checked exception ShipMonk\\InputMapper\\Runtime\\Exception\\MappingFailedException but it''s missing from the PHPDoc @throws tag\.#'
4747
paths: [tests/**/*Test.php]
48+
-
49+
message: "#^Method ShipMonkTests\\\\InputMapper\\\\Compiler\\\\Validator\\\\Array\\\\Data\\\\ListItemValidatorMapper\\:\\:map\\(\\) should return list\\<int\\<1, max\\>\\> but returns list\\<int\\>\\.$#"
50+
count: 1
51+
path: tests/Compiler/Validator/Array/Data/ListItemValidatorMapper.php
52+
-
53+
message: "#^Method ShipMonkTests\\\\InputMapper\\\\Compiler\\\\Validator\\\\Array\\\\Data\\\\ListItemValidatorWithMultipleValidatorsMapper\\:\\:map\\(\\) should return list\\<int\\<1, max\\>\\> but returns list\\<int\\>\\.$#"
54+
count: 1
55+
path: tests/Compiler/Validator/Array/Data/ListItemValidatorWithMultipleValidatorsMapper.php

src/Compiler/Mapper/Wrapper/ValidatedMapperCompiler.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler;
1111
use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder;
1212
use ShipMonk\InputMapper\Compiler\Type\PhpDocTypeUtils;
13+
use ShipMonk\InputMapper\Compiler\Validator\NarrowingValidatorCompiler;
1314
use ShipMonk\InputMapper\Compiler\Validator\ValidatorCompiler;
1415

1516
class ValidatedMapperCompiler implements MapperCompiler
@@ -65,7 +66,15 @@ public function getInputType(): TypeNode
6566

6667
public function getOutputType(): TypeNode
6768
{
68-
return $this->mapperCompiler->getOutputType();
69+
$outputTypes = [$this->mapperCompiler->getOutputType()];
70+
71+
foreach ($this->validatorCompilers as $validatorCompiler) {
72+
if ($validatorCompiler instanceof NarrowingValidatorCompiler) {
73+
$outputTypes[] = $validatorCompiler->getNarrowedInputType();
74+
}
75+
}
76+
77+
return PhpDocTypeUtils::intersect(...$outputTypes);
6978
}
7079

7180
}

src/Compiler/Validator/Array/AssertListItem.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
1111
use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder;
1212
use ShipMonk\InputMapper\Compiler\Type\PhpDocTypeUtils;
13+
use ShipMonk\InputMapper\Compiler\Validator\NarrowingValidatorCompiler;
1314
use ShipMonk\InputMapper\Compiler\Validator\ValidatorCompiler;
1415
use function array_map;
1516
use function count;
1617

1718
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
18-
class AssertListItem implements ValidatorCompiler
19+
class AssertListItem implements NarrowingValidatorCompiler
1920
{
2021

2122
/**
@@ -72,4 +73,20 @@ public function getInputType(): TypeNode
7273
);
7374
}
7475

76+
public function getNarrowedInputType(): TypeNode
77+
{
78+
$validatorOutputTypes = [];
79+
80+
foreach ($this->validators as $validator) {
81+
$validatorOutputTypes[] = $validator instanceof NarrowingValidatorCompiler
82+
? $validator->getNarrowedInputType()
83+
: $validator->getInputType();
84+
}
85+
86+
return new GenericTypeNode(
87+
new IdentifierTypeNode('list'),
88+
[PhpDocTypeUtils::intersect(...$validatorOutputTypes)],
89+
);
90+
}
91+
7592
}

src/Compiler/Validator/Int/AssertIntRange.php

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,21 @@
55
use Attribute;
66
use PhpParser\Node\Expr;
77
use PhpParser\Node\Stmt;
8+
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
9+
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
10+
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
811
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
912
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
1013
use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder;
11-
use ShipMonk\InputMapper\Compiler\Validator\ValidatorCompiler;
14+
use ShipMonk\InputMapper\Compiler\Validator\NarrowingValidatorCompiler;
1215
use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException;
16+
use function max;
17+
use function min;
18+
use const PHP_INT_MAX;
19+
use const PHP_INT_MIN;
1320

1421
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
15-
class AssertIntRange implements ValidatorCompiler
22+
class AssertIntRange implements NarrowingValidatorCompiler
1623
{
1724

1825
public function __construct(
@@ -92,4 +99,46 @@ public function getInputType(): TypeNode
9299
return new IdentifierTypeNode('int');
93100
}
94101

102+
public function getNarrowedInputType(): TypeNode
103+
{
104+
$inclusiveLowerBounds = [PHP_INT_MIN];
105+
$inclusiveUpperBounds = [PHP_INT_MAX];
106+
107+
if ($this->gte !== null) {
108+
$inclusiveLowerBounds[] = $this->gte;
109+
}
110+
111+
if ($this->gt !== null) {
112+
$inclusiveLowerBounds[] = $this->gt + 1;
113+
}
114+
115+
if ($this->lt !== null) {
116+
$inclusiveUpperBounds[] = $this->lt - 1;
117+
}
118+
119+
if ($this->lte !== null) {
120+
$inclusiveUpperBounds[] = $this->lte;
121+
}
122+
123+
$inclusiveLowerBound = max($inclusiveLowerBounds);
124+
$inclusiveUpperBound = min($inclusiveUpperBounds);
125+
126+
if ($inclusiveLowerBound === PHP_INT_MIN && $inclusiveUpperBound === PHP_INT_MAX) {
127+
return new IdentifierTypeNode('int');
128+
}
129+
130+
$inclusiveLowerBoundType = $inclusiveLowerBound !== PHP_INT_MIN
131+
? new ConstTypeNode(new ConstExprIntegerNode((string) $inclusiveLowerBound))
132+
: new IdentifierTypeNode('min');
133+
134+
$inclusiveUpperBoundType = $inclusiveUpperBound !== PHP_INT_MAX
135+
? new ConstTypeNode(new ConstExprIntegerNode((string) $inclusiveUpperBound))
136+
: new IdentifierTypeNode('max');
137+
138+
return new GenericTypeNode(new IdentifierTypeNode('int'), [
139+
$inclusiveLowerBoundType,
140+
$inclusiveUpperBoundType,
141+
]);
142+
}
143+
95144
}

src/Compiler/Validator/Int/AssertNegativeInt.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace ShipMonk\InputMapper\Compiler\Validator\Int;
44

55
use Attribute;
6+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
7+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
68

79
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
810
class AssertNegativeInt extends AssertIntRange
@@ -15,4 +17,9 @@ public function __construct()
1517
);
1618
}
1719

20+
public function getNarrowedInputType(): TypeNode
21+
{
22+
return new IdentifierTypeNode('negative-int');
23+
}
24+
1825
}

src/Compiler/Validator/Int/AssertPositiveInt.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace ShipMonk\InputMapper\Compiler\Validator\Int;
44

55
use Attribute;
6+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
7+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
68

79
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
810
class AssertPositiveInt extends AssertIntRange
@@ -15,4 +17,9 @@ public function __construct()
1517
);
1618
}
1719

20+
public function getNarrowedInputType(): TypeNode
21+
{
22+
return new IdentifierTypeNode('positive-int');
23+
}
24+
1825
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\InputMapper\Compiler\Validator;
4+
5+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
6+
7+
interface NarrowingValidatorCompiler extends ValidatorCompiler
8+
{
9+
10+
/**
11+
* Must return subtype of input type returned by {@link self::getInputType()}.
12+
*/
13+
public function getNarrowedInputType(): TypeNode;
14+
15+
}

tests/Compiler/Validator/Array/AssertListItemTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function testInnerValidatorIsCalledWithCorrectItemType(): void
6868
self::isInstanceOf(PhpCodeBuilder::class),
6969
);
7070

71-
$itemValidator->expects(self::once())
71+
$itemValidator->expects(self::exactly(3))
7272
->method('getInputType')
7373
->willReturn(new IdentifierTypeNode('int'));
7474

tests/Compiler/Validator/Array/Data/ListItemValidatorMapper.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
/**
1414
* Generated mapper by {@see ValidatedMapperCompiler}. Do not edit directly.
1515
*
16-
* @implements Mapper<list<int>>
16+
* @implements Mapper<list<positive-int>>
1717
*/
1818
class ListItemValidatorMapper implements Mapper
1919
{
@@ -23,7 +23,7 @@ public function __construct(private readonly MapperProvider $provider)
2323

2424
/**
2525
* @param list<string|int> $path
26-
* @return list<int>
26+
* @return list<positive-int>
2727
* @throws MappingFailedException
2828
*/
2929
public function map(mixed $data, array $path = []): array

tests/Compiler/Validator/Array/Data/ListItemValidatorWithMultipleValidatorsMapper.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
/**
1414
* Generated mapper by {@see ValidatedMapperCompiler}. Do not edit directly.
1515
*
16-
* @implements Mapper<list<int>>
16+
* @implements Mapper<list<positive-int>>
1717
*/
1818
class ListItemValidatorWithMultipleValidatorsMapper implements Mapper
1919
{
@@ -23,7 +23,7 @@ public function __construct(private readonly MapperProvider $provider)
2323

2424
/**
2525
* @param list<string|int> $path
26-
* @return list<int>
26+
* @return list<positive-int>
2727
* @throws MappingFailedException
2828
*/
2929
public function map(mixed $data, array $path = []): array

tests/Compiler/Validator/Int/AssertIntRangeTest.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
namespace ShipMonkTests\InputMapper\Compiler\Validator\Int;
44

5+
use PHPUnit\Framework\Attributes\DataProvider;
56
use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapInt;
67
use ShipMonk\InputMapper\Compiler\Validator\Int\AssertIntRange;
78
use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException;
89
use ShipMonkTests\InputMapper\Compiler\Validator\ValidatorCompilerTestCase;
10+
use const PHP_INT_MAX;
11+
use const PHP_INT_MIN;
912

1013
class AssertIntRangeTest extends ValidatorCompilerTestCase
1114
{
@@ -105,4 +108,86 @@ public function testIntRangeValidatorWithInclusiveLowerAndUpperBound(): void
105108
);
106109
}
107110

111+
#[DataProvider('provideGetNarrowedInputTypeData')]
112+
public function testGetNarrowedInputType(AssertIntRange $validatorCompiler, string $expectedNarrowedType): void
113+
{
114+
self::assertSame($expectedNarrowedType, $validatorCompiler->getNarrowedInputType()->__toString());
115+
}
116+
117+
/**
118+
* @return iterable<array{AssertIntRange, string}>
119+
*/
120+
public static function provideGetNarrowedInputTypeData(): iterable
121+
{
122+
yield [
123+
new AssertIntRange(),
124+
'int',
125+
];
126+
127+
yield [
128+
new AssertIntRange(gte: 5),
129+
'int<5, max>',
130+
];
131+
132+
yield [
133+
new AssertIntRange(gt: 5),
134+
'int<6, max>',
135+
];
136+
137+
yield [
138+
new AssertIntRange(lte: 5),
139+
'int<min, 5>',
140+
];
141+
142+
yield [
143+
new AssertIntRange(lt: 5),
144+
'int<min, 4>',
145+
];
146+
147+
yield [
148+
new AssertIntRange(gte: 5, lte: 10),
149+
'int<5, 10>',
150+
];
151+
152+
yield [
153+
new AssertIntRange(gt: 5, lte: 10),
154+
'int<6, 10>',
155+
];
156+
157+
yield [
158+
new AssertIntRange(gte: 5, lt: 10),
159+
'int<5, 9>',
160+
];
161+
162+
yield [
163+
new AssertIntRange(gt: 5, lt: 10),
164+
'int<6, 9>',
165+
];
166+
167+
yield [
168+
new AssertIntRange(gt: 0, gte: 5),
169+
'int<5, max>',
170+
];
171+
172+
yield [
173+
new AssertIntRange(lt: 0, lte: 5),
174+
'int<min, -1>',
175+
];
176+
177+
yield [
178+
new AssertIntRange(gt: 5, gte: 0),
179+
'int<6, max>',
180+
];
181+
182+
yield [
183+
new AssertIntRange(lt: 5, lte: 0),
184+
'int<min, 0>',
185+
];
186+
187+
yield [
188+
new AssertIntRange(gte: PHP_INT_MIN, lte: PHP_INT_MAX),
189+
'int',
190+
];
191+
}
192+
108193
}

tests/Compiler/Validator/Int/AssertNonNegativeIntTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace ShipMonkTests\InputMapper\Compiler\Validator\Int;
44

5+
use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapInt;
56
use ShipMonk\InputMapper\Compiler\Validator\Int\AssertNonNegativeInt;
67
use ShipMonkTests\InputMapper\Compiler\Validator\ValidatorCompilerTestCase;
78

@@ -10,11 +11,17 @@ class AssertNonNegativeIntTest extends ValidatorCompilerTestCase
1011

1112
public function testNonNegativeIntValidator(): void
1213
{
14+
$mapperCompiler = new MapInt();
1315
$validatorCompiler = new AssertNonNegativeInt();
16+
1417
self::assertSame(0, $validatorCompiler->gte);
1518
self::assertNull($validatorCompiler->gt);
1619
self::assertNull($validatorCompiler->lt);
1720
self::assertNull($validatorCompiler->lte);
21+
22+
$validator = $this->compileValidator('NonNegativeIntValidator', $mapperCompiler, $validatorCompiler);
23+
$validator->map(0);
24+
$validator->map(123);
1825
}
1926

2027
}

tests/Compiler/Validator/Int/AssertPositiveIntTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace ShipMonkTests\InputMapper\Compiler\Validator\Int;
44

5+
use ShipMonk\InputMapper\Compiler\Mapper\Scalar\MapInt;
56
use ShipMonk\InputMapper\Compiler\Validator\Int\AssertPositiveInt;
67
use ShipMonkTests\InputMapper\Compiler\Validator\ValidatorCompilerTestCase;
78

@@ -10,11 +11,16 @@ class AssertPositiveIntTest extends ValidatorCompilerTestCase
1011

1112
public function testPositiveIntValidator(): void
1213
{
14+
$mapperCompiler = new MapInt();
1315
$validatorCompiler = new AssertPositiveInt();
16+
1417
self::assertNull($validatorCompiler->gte);
1518
self::assertSame(0, $validatorCompiler->gt);
1619
self::assertNull($validatorCompiler->lt);
1720
self::assertNull($validatorCompiler->lte);
21+
22+
$validator = $this->compileValidator('PositiveIntValidator', $mapperCompiler, $validatorCompiler);
23+
$validator->map(123);
1824
}
1925

2026
}

0 commit comments

Comments
 (0)