Skip to content

Commit 3d556f9

Browse files
committed
Enhance wildcard constant pattern validation for enums and update documentation
1 parent 4905f83 commit 3d556f9

10 files changed

Lines changed: 83 additions & 19 deletions

File tree

docs/supported-types/primitives-and-scalars.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,12 @@ To prevent rounding artifacts from causing unexpected type failures, TypePHP eva
7474

7575
---
7676

77-
## Wildcard Class Constant Patterns (`Class::PREFIX_*`)
77+
## Wildcard Constant Patterns (`Class::PREFIX_*` & `StatusEnum::*`)
7878

79-
TypePHP supports validating parameters, return types, properties, and `@var` local assignments against wildcard class constant patterns (`Class::PREFIX_*` or `self::PREFIX_*`).
79+
TypePHP supports validating parameters, return types, properties, and `@var` local assignments against wildcard constant patterns (`Class::PREFIX_*` or `StatusEnum::*`).
8080

81-
TypePHP uses Reflection to safely inspect all matching constants—including `public`, `protected`, and `private` class constants—and validates that the incoming value matches one of the declared constant values:
81+
* **Class Constants (`Class::PREFIX_*`):** TypePHP uses Reflection to safely inspect all matching constants—including `public`, `protected`, and `private` class constants—and validates that the incoming value matches one of the declared constant scalar values.
82+
* **Enums (`StatusEnum::*` or `StatusEnum::ACT*`):** Matches incoming **Enum case objects** against the wildcard case pattern.
8283

8384
```php
8485
class MigrationCollectionLoader
@@ -96,16 +97,36 @@ class MigrationCollectionLoader
9697
}
9798
}
9899

100+
enum StatusEnum: string
101+
{
102+
case Active = 'active';
103+
case Inactive = 'inactive';
104+
case Pending = 'pending';
105+
}
106+
107+
class EnumProcessor
108+
{
109+
/**
110+
* @param StatusEnum::ACT* $status
111+
*/
112+
public static function processActive(StatusEnum $status): StatusEnum
113+
{
114+
return $status;
115+
}
116+
}
117+
99118
$loader = new MigrationCollectionLoader();
100119

101-
// Valid Calls (Matches public and private VERSION_SELECTION_* constant values)
120+
// Valid Class Constant Calls (Matches public and private VERSION_SELECTION_* constant values)
102121
$loader->collectAllForVersion('all');
103-
$loader->collectAllForVersion('blue-green');
104122
$loader->collectAllForVersion('internal-mode');
105123

106-
// Invalid Call ('invalid_mode' does not match any VERSION_SELECTION_* constant)
107-
$loader->collectAllForVersion('invalid_mode');
108-
// Throws: TypeError: Argument $mode must be a valid constant matching MigrationCollectionLoader::VERSION_SELECTION_*
124+
// Valid Enum Wildcard Call (Matches StatusEnum::ACT* -> StatusEnum::Active)
125+
EnumProcessor::processActive(StatusEnum::Active);
126+
127+
// Invalid Enum Wildcard Call (StatusEnum::Pending does not match StatusEnum::ACT*)
128+
EnumProcessor::processActive(StatusEnum::Pending);
129+
// Throws: TypeError: Argument $status must be a valid constant matching StatusEnum::ACT*
109130
```
110131

111132
---
@@ -394,4 +415,4 @@ function badHaltExecution(): string
394415

395416
badHaltExecution();
396417
// Throws: TypeError: badHaltExecution(): Return value must be of type never
397-
```
418+
```

src/Validator/ArrayShapeValidator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,4 @@ public function validate(mixed $value, TypeNode $node, string $context, TypeVali
9191

9292
return null;
9393
}
94-
}
94+
}

src/Validator/ConstValidator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,4 @@ private static function resolveWildcardConstantValues(string $className, string
121121

122122
return self::$wildcardConstantCache[$cacheKey] = $values;
123123
}
124-
}
124+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypePHP\Tests\Fixtures\Types;
6+
7+
class EnumWildcard
8+
{
9+
/**
10+
* @param StatusEnum::* $status
11+
*/
12+
public static function processEnumCase(StatusEnum $status): StatusEnum
13+
{
14+
return $status;
15+
}
16+
17+
/**
18+
* @param StatusEnum::ACT* $status
19+
*/
20+
public static function processPrefixEnumCase(StatusEnum $status): StatusEnum
21+
{
22+
return $status;
23+
}
24+
}

tests/Fixtures/Types/GlobalTypes.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@
1010
*/
1111
class GlobalTypes
1212
{
13-
}
13+
}

tests/Fixtures/Types/WildcardConstantFixture.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ public static function setVersionMode(string $mode): string
1717
{
1818
return $mode;
1919
}
20-
}
20+
}

tests/TypeChecking/ArraysAndShapes/ArrayAndListTypesTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ function testKeylessImplicitTupleShape(array $tuple): bool
121121
*
122122
* @phpstan-type LocalTupleAlias array{list<positive-int>, list<non-empty-string>}
123123
* @phpstan-type MixedTupleShape array{non-empty-string, code: positive-int, list<int>}
124+
*
124125
* @phpstan-import-type SharedTupleShape from \TypePHP\Tests\Fixtures\Types\GlobalTypes as ImportedTuple
125126
*
126127
* @param LocalTupleAlias $payload
@@ -153,7 +154,6 @@ function testReturnKeylessTuple(bool $valid): array
153154
return [[10, 20], 'bundle'];
154155
}
155156

156-
157157
describe('Class Object Arrays (Dog[])', function () {
158158
test('accepts array of matching class instances', function () {
159159
expect(testDogArrayParam([new Dog(), new Dog()]))->toBe(2);
@@ -379,4 +379,4 @@ function testReturnKeylessTuple(bool $valid): array
379379
->toThrow(TypeError::class, "Return value['0'][1] must be of type positive-int")
380380
;
381381
});
382-
});
382+
});

tests/TypeChecking/ArraysAndShapes/UnionErrorBubblingTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ public function __construct()
140140

141141
test('falls back gracefully to generic union error when no deep branch matches structure', function () {
142142
expect(fn () => testDeepUnionError('string_instead_of_array'))
143-
->toThrow(TypeError::class, 'must be of type (array{id: int, tags: list<(string | int)>} | null)');
143+
->toThrow(TypeError::class, 'must be of type (array{id: int, tags: list<(string | int)>} | null)')
144+
;
144145
});
145146
});

tests/TypeChecking/Generics/GenericTemplateBoundsStressTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ function testInferredOverridesDefault(mixed $input, mixed $valueToReturn): mixed
226226
expect(testInferredOverridesDefault($dt, $dt))->toBe($dt);
227227

228228
expect(fn () => testInferredOverridesDefault($dt, new stdClass()))
229-
->toThrow(TypeError::class, 'Return value');
229+
->toThrow(TypeError::class, 'Return value')
230+
;
230231
});
231232
});

tests/TypeChecking/Scalars/ExtendedScalarTypesTest.php

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

33
declare(strict_types=1);
44

5+
use TypePHP\Tests\Fixtures\Types\EnumWildcard;
6+
use TypePHP\Tests\Fixtures\Types\StatusEnum;
57
use TypePHP\Tests\Fixtures\Types\WildcardConstantFixture;
68

79
/**
@@ -152,7 +154,22 @@ function testLiteralUnionsParam(string $status, int $code): bool
152154

153155
test('throws TypeError on value not matching wildcard constant pattern', function () {
154156
expect(fn () => WildcardConstantFixture::setVersionMode('invalid_mode'))
155-
->toThrow(TypeError::class, "must be a valid constant matching TypePHP\\Tests\\Fixtures\\Types\\WildcardConstantFixture::VERSION_SELECTION_*")
157+
->toThrow(TypeError::class, 'must be a valid constant matching TypePHP\\Tests\\Fixtures\\Types\\WildcardConstantFixture::VERSION_SELECTION_*')
156158
;
157159
});
158-
});
160+
});
161+
162+
describe('Enum Wildcard Pattern Validation (StatusEnum::*)', function () {
163+
test('accepts Enum case objects matching wildcard patterns (StatusEnum::* & StatusEnum::ACT*)', function () {
164+
expect(EnumWildcard::processEnumCase(StatusEnum::Active))->toBe(StatusEnum::Active);
165+
expect(EnumWildcard::processEnumCase(StatusEnum::Pending))->toBe(StatusEnum::Pending);
166+
167+
expect(EnumWildcard::processPrefixEnumCase(StatusEnum::Active))->toBe(StatusEnum::Active);
168+
});
169+
170+
test('throws TypeError when Enum case object does not match prefix wildcard (e.g. Pending for StatusEnum::ACT*)', function () {
171+
expect(fn () => EnumWildcard::processPrefixEnumCase(StatusEnum::Pending))
172+
->toThrow(TypeError::class, 'must be a valid constant matching TypePHP\\Tests\\Fixtures\\Types\\StatusEnum::ACT*')
173+
;
174+
});
175+
});

0 commit comments

Comments
 (0)