Skip to content

Commit 3a48123

Browse files
add PHPStan template annotations and suppress covariant generics
- Add proper template annotations to test base classes and suppress. - PHPStan errors for lack of covariant generics support. - Maintains DRY test structure while ensuring static analysis passes cleanly.
1 parent 7e62726 commit 3a48123

File tree

9 files changed

+93
-5
lines changed

9 files changed

+93
-5
lines changed

ci/phpstan/baselines/range-baseline.neon

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,25 @@ parameters:
2323
identifier: new.static
2424
count: 4
2525
path: ../../../src/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/Range.php
26+
27+
# Temporary suppression for PHPStan's lack of covariant generics support
28+
# Remove when PHPStan supports covariance (https://github.com/phpstan/phpstan/issues/7427)
29+
-
30+
message: '#.*Template type R on class.*Range is not covariant#'
31+
path: ../../../tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/
32+
33+
-
34+
message: '#.*should return.*Range<DateTimeInterface\|float\|int>.*but returns.*Range#'
35+
path: ../../../tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/
36+
37+
-
38+
message: '#.*Generator expects value type.*Range<DateTimeInterface\|float\|int>.*given#'
39+
path: ../../../tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/
40+
41+
-
42+
message: '#.*extends generic class.*but does not specify its types#'
43+
path: ../../../tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/
44+
45+
-
46+
message: '#.*should be compatible with return type#'
47+
path: ../../../tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/

tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/BaseRangeTestCase.php

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
use PHPUnit\Framework\Attributes\Test;
1010
use PHPUnit\Framework\TestCase;
1111

12+
/**
13+
* @template R of int|float|\DateTimeInterface
14+
*
15+
* TODO: Remove PHPStan suppressions when covariant generics are supported
16+
*
17+
* @see https://github.com/phpstan/phpstan/issues/7427
18+
*/
1219
abstract class BaseRangeTestCase extends TestCase
1320
{
1421
#[Test]
@@ -49,13 +56,19 @@ public function can_create_inclusive_range(): void
4956
self::assertFalse($range->isEmpty());
5057
}
5158

59+
/**
60+
* @param Range<R> $range
61+
*/
5262
#[Test]
5363
#[DataProvider('provideContainsTestCases')]
5464
public function can_check_contains(Range $range, mixed $value, bool $expected): void
5565
{
5666
self::assertEquals($expected, $range->contains($value));
5767
}
5868

69+
/**
70+
* @param Range<R> $expectedRange
71+
*/
5972
#[Test]
6073
#[DataProvider('provideFromStringTestCases')]
6174
public function can_parse_from_string(string $input, Range $expectedRange): void
@@ -89,6 +102,8 @@ public function can_handle_comparison_via_is_empty(): void
89102

90103
/**
91104
* Create a simple range for basic testing.
105+
*
106+
* @return Range<R>
92107
*/
93108
abstract protected function createSimpleRange(): Range;
94109

@@ -99,16 +114,22 @@ abstract protected function getExpectedSimpleRangeString(): string;
99114

100115
/**
101116
* Create an empty range.
117+
*
118+
* @return Range<R>
102119
*/
103120
abstract protected function createEmptyRange(): Range;
104121

105122
/**
106123
* Create an infinite range.
124+
*
125+
* @return Range<R>
107126
*/
108127
abstract protected function createInfiniteRange(): Range;
109128

110129
/**
111130
* Create an inclusive range for testing.
131+
*
132+
* @return Range<R>
112133
*/
113134
abstract protected function createInclusiveRange(): Range;
114135

@@ -119,11 +140,15 @@ abstract protected function getExpectedInclusiveRangeString(): string;
119140

120141
/**
121142
* Parse range from string.
143+
*
144+
* @return Range<R>
122145
*/
123146
abstract protected function parseFromString(string $input): Range;
124147

125148
/**
126149
* Create range for boundary testing.
150+
*
151+
* @return Range<R>
127152
*/
128153
abstract protected function createBoundaryTestRange(): Range;
129154

@@ -137,22 +162,25 @@ abstract protected function getBoundaryTestCases(): array;
137162
/**
138163
* Get comparison test cases.
139164
*
140-
* @return array<string, array{range: Range, expectedEmpty: bool}>
165+
* @return array<string, array{range: Range<R>, expectedEmpty: bool}>
141166
*/
142167
abstract protected function getComparisonTestCases(): array;
143168

144169
/**
145-
* @return \Generator<string, array{Range, mixed, bool}>
170+
* @return \Generator<string, array{Range<R>, mixed, bool}>
146171
*/
147172
abstract public static function provideContainsTestCases(): \Generator;
148173

149174
/**
150-
* @return \Generator<string, array{string, Range}>
175+
* @return \Generator<string, array{string, Range<R>}>
151176
*/
152177
abstract public static function provideFromStringTestCases(): \Generator;
153178

154179
/**
155180
* Assert that a range equals another range by comparing string representation and isEmpty state.
181+
*
182+
* @param Range<R> $expected
183+
* @param Range<R> $actual
156184
*/
157185
protected function assertRangeEquals(Range $expected, Range $actual, string $message = ''): void
158186
{
@@ -163,6 +191,7 @@ protected function assertRangeEquals(Range $expected, Range $actual, string $mes
163191
/**
164192
* Assert that a range contains all the given values.
165193
*
194+
* @param Range<R> $range
166195
* @param array<mixed> $values
167196
*/
168197
protected function assertRangeContainsAll(Range $range, array $values, string $message = ''): void
@@ -178,6 +207,7 @@ protected function assertRangeContainsAll(Range $range, array $values, string $m
178207
/**
179208
* Assert that a range does not contain any of the given values.
180209
*
210+
* @param Range<R> $range
181211
* @param array<mixed> $values
182212
*/
183213
protected function assertRangeContainsNone(Range $range, array $values, string $message = ''): void
@@ -192,6 +222,8 @@ protected function assertRangeContainsNone(Range $range, array $values, string $
192222

193223
/**
194224
* Assert that a range has the expected string representation.
225+
*
226+
* @param Range<R> $range
195227
*/
196228
protected function assertRangeStringEquals(string $expected, Range $range, string $message = ''): void
197229
{
@@ -200,6 +232,8 @@ protected function assertRangeStringEquals(string $expected, Range $range, strin
200232

201233
/**
202234
* Assert that a range is empty.
235+
*
236+
* @param Range<R> $range
203237
*/
204238
protected function assertRangeIsEmpty(Range $range, string $message = ''): void
205239
{
@@ -209,6 +243,8 @@ protected function assertRangeIsEmpty(Range $range, string $message = ''): void
209243

210244
/**
211245
* Assert that a range is not empty.
246+
*
247+
* @param Range<R> $range
212248
*/
213249
protected function assertRangeIsNotEmpty(Range $range, string $message = ''): void
214250
{
@@ -219,6 +255,7 @@ protected function assertRangeIsNotEmpty(Range $range, string $message = ''): vo
219255
/**
220256
* Test boundary conditions for a range with known bounds.
221257
*
258+
* @param Range<R> $range
222259
* @param array<string, array{value: mixed, expected: bool}> $testCases
223260
*/
224261
protected function assertBoundaryConditions(Range $range, array $testCases, string $message = ''): void
@@ -257,7 +294,7 @@ protected function generateBoundaryTestCases(
257294
/**
258295
* Test that a range correctly handles equal bounds with different bracket combinations.
259296
*
260-
* @param callable $rangeFactory Function that creates a range: fn($lower, $upper, $lowerInc, $upperInc) => Range
297+
* @param callable(mixed, mixed, bool, bool): Range<R> $rangeFactory Function that creates a range
261298
*/
262299
protected function assertEqualBoundsHandling(callable $rangeFactory, mixed $value): void
263300
{
@@ -278,7 +315,7 @@ protected function assertEqualBoundsHandling(callable $rangeFactory, mixed $valu
278315
/**
279316
* Test that a range correctly handles reverse bounds (lower > upper).
280317
*
281-
* @param callable $rangeFactory Function that creates a range: fn($lower, $upper, $lowerInc, $upperInc) => Range
318+
* @param callable(mixed, mixed, bool, bool): Range<R> $rangeFactory Function that creates a range
282319
*/
283320
protected function assertReverseBoundsHandling(callable $rangeFactory, mixed $lower, mixed $upper): void
284321
{

tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/BaseTimestampRangeTestCase.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
* Base test case for timestamp range types (TsRange, TstzRange).
1313
* Provides common timestamp-specific test patterns.
1414
*/
15+
/**
16+
* @template R of \DateTimeInterface
17+
*/
1518
abstract class BaseTimestampRangeTestCase extends BaseRangeTestCase
1619
{
1720
protected function getBoundaryTestCases(): array
@@ -36,6 +39,9 @@ protected function getBoundaryTestCases(): array
3639
];
3740
}
3841

42+
/**
43+
* @return array<string, array{range: Range<R>, expectedEmpty: bool}>
44+
*/
3945
protected function getComparisonTestCases(): array
4046
{
4147
return [
@@ -73,6 +79,7 @@ public function throws_exception_for_invalid_constructor_input(): void
7379
{
7480
$this->expectException(\TypeError::class);
7581

82+
/* @phpstan-ignore-next-line */
7683
$this->createRangeWithTimes('invalid', $this->getTestEndTime());
7784
}
7885

@@ -124,6 +131,8 @@ public function can_handle_different_datetime_implementations(): void
124131

125132
/**
126133
* Create a range with specific DateTimeInterface objects.
134+
*
135+
* @return Range<R>
127136
*/
128137
abstract protected function createRangeWithTimes(
129138
?\DateTimeInterface $start,

tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/DateRangeTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
use PHPUnit\Framework\Attributes\DataProvider;
1111
use PHPUnit\Framework\Attributes\Test;
1212

13+
/**
14+
* @extends BaseRangeTestCase<\DateTimeInterface>
15+
*/
1316
final class DateRangeTest extends BaseRangeTestCase
1417
{
1518
protected function createSimpleRange(): Range

tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/Int4RangeTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Range;
99
use PHPUnit\Framework\Attributes\Test;
1010

11+
/**
12+
* @extends BaseRangeTestCase<int>
13+
*/
1114
final class Int4RangeTest extends BaseRangeTestCase
1215
{
1316
protected function createSimpleRange(): Range

tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/Int8RangeTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Range;
99
use PHPUnit\Framework\Attributes\Test;
1010

11+
/**
12+
* @extends BaseRangeTestCase<int>
13+
*/
1114
final class Int8RangeTest extends BaseRangeTestCase
1215
{
1316
protected function createSimpleRange(): Range

tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/NumericRangeTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\Range;
1010
use PHPUnit\Framework\Attributes\Test;
1111

12+
/**
13+
* @extends BaseRangeTestCase<float|int>
14+
*/
1215
final class NumericRangeTest extends BaseRangeTestCase
1316
{
1417
protected function createSimpleRange(): Range

tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/TsRangeTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\TsRange;
99
use PHPUnit\Framework\Attributes\Test;
1010

11+
/**
12+
* @extends BaseTimestampRangeTestCase<\DateTimeInterface>
13+
*/
1114
final class TsRangeTest extends BaseTimestampRangeTestCase
1215
{
1316
protected function createSimpleRange(): Range
@@ -230,6 +233,7 @@ public function throws_exception_for_invalid_value_in_constructor(): void
230233
{
231234
$this->expectException(\TypeError::class);
232235

236+
/* @phpstan-ignore-next-line */
233237
new TsRange('invalid', new \DateTimeImmutable('2023-01-01 18:00:00'));
234238
}
235239

tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/TstzRangeTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
use MartinGeorgiev\Doctrine\DBAL\Types\ValueObject\TstzRange;
99
use PHPUnit\Framework\Attributes\Test;
1010

11+
/**
12+
* @extends BaseTimestampRangeTestCase<\DateTimeInterface>
13+
*/
1114
final class TstzRangeTest extends BaseTimestampRangeTestCase
1215
{
1316
protected function createSimpleRange(): Range
@@ -180,6 +183,7 @@ public function throws_exception_for_invalid_constructor_input(): void
180183
{
181184
$this->expectException(\TypeError::class);
182185

186+
/* @phpstan-ignore-next-line */
183187
new TstzRange('invalid', new \DateTimeImmutable('2023-01-01 18:00:00+00:00'));
184188
}
185189

0 commit comments

Comments
 (0)