Skip to content

Commit 0a0efdc

Browse files
committed
Refactor CallableWrapper and CallableWrapperTest for improved type validation; enhance error messages for better clarity
1 parent 9108910 commit 0a0efdc

6 files changed

Lines changed: 215 additions & 210 deletions

File tree

src/Wrapper/CallableWrapper.php

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,10 @@ public static function wrap(string $function, string $paramName, mixed $callable
3535

3636
$prefix = ($paramName === 'return') ? "$function(): Return value" : "$function(): Callback \$$paramName";
3737

38-
// 1. Single Callable
3938
if (\is_callable($callable)) {
4039
return self::wrapTypeNode($typeNode, $callable, $prefix, $registry);
4140
}
4241

43-
// 2. Collections of Callables (e.g. list<callable(...)> or callable[])
4442
if (\is_array($callable) && $typeNode !== null) {
4543
$innerCallableTypeNode = null;
4644

@@ -116,18 +114,21 @@ private static function enforceClosureConstraints(string $identifierName, mixed
116114
}
117115

118116
/**
119-
* Validates variadic and positional arguments passed into an intercepted callback.
117+
* Validates variadic, positional, and named arguments passed into an intercepted callback.
120118
*
121119
* @param array<int|string, mixed> $args
122120
*/
123121
private static function validateCallbackArguments(CallableTypeNode $typeNode, array $args, string $prefix, TypeValidatorRegistry $registry): void
124122
{
125-
$argCount = \count($args);
123+
$argValues = array_values($args);
124+
$argCount = \count($argValues);
126125

127126
foreach ($typeNode->parameters as $index => $paramNode) {
127+
$rawParamName = ltrim($paramNode->parameterName ?? '', '$');
128+
128129
if ($paramNode->isVariadic) {
129130
for ($vIdx = $index; $vIdx < $argCount; $vIdx++) {
130-
$err = $registry->validate($args[$vIdx], $paramNode->type, "$prefix variadic argument #" . ($vIdx + 1));
131+
$err = $registry->validate($argValues[$vIdx], $paramNode->type, "$prefix variadic argument #" . ($vIdx + 1));
131132
if ($err !== null) {
132133
throw ErrorFactory::prepareException(new TypePHPTypeError($err->getMessage()));
133134
}
@@ -136,12 +137,24 @@ private static function validateCallbackArguments(CallableTypeNode $typeNode, ar
136137
break;
137138
}
138139

139-
if (\array_key_exists($index, $args)) {
140-
$err = $registry->validate($args[$index], $paramNode->type, "$prefix argument #" . ($index + 1));
140+
$val = null;
141+
$hasVal = false;
142+
143+
if ($rawParamName !== '' && \array_key_exists($rawParamName, $args)) {
144+
$val = $args[$rawParamName];
145+
$hasVal = true;
146+
} elseif (\array_key_exists($index, $argValues)) {
147+
$val = $argValues[$index];
148+
$hasVal = true;
149+
}
150+
151+
if ($hasVal) {
152+
$argLabel = $rawParamName !== '' ? "\$$rawParamName" : ('argument #' . ($index + 1));
153+
$err = $registry->validate($val, $paramNode->type, "$prefix $argLabel");
141154
if ($err !== null) {
142155
throw ErrorFactory::prepareException(new TypePHPTypeError($err->getMessage()));
143156
}
144157
}
145158
}
146159
}
147-
}
160+
}

src/Wrapper/IterableWrapper.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,4 @@ private static function wrapGenerator(iterable $iterable, \Closure $typeCheckCal
137137
yield $key => $value;
138138
}
139139
}
140-
}
140+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypePHP\Tests\Fixtures\Services;
6+
7+
class InvokableFormatterService
8+
{
9+
public function __invoke(int $id): string
10+
{
11+
if ($id <= 0) {
12+
return ''; // Violates non-empty-string
13+
}
14+
15+
return "invoked_{$id}";
16+
}
17+
}

tests/TypeChecking/Boundaries/AdvancedEdgeCasesTest.php

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ function processFormatterList(array $formatters, int $id): array
3030
[$id, , $username] = [10, 'skipped_token', 'Alice'];
3131

3232
expect($id)->toBe(10)
33-
->and($username)->toBe('Alice');
33+
->and($username)->toBe('Alice')
34+
;
3435

3536
expect(function () {
3637
/**
@@ -51,15 +52,16 @@ function processFormatterList(array $formatters, int $id): array
5152

5253
expect($id)->toBe(42)
5354
->and($street)->toBe('Broadway')
54-
->and($zip)->toBe(90210);
55+
->and($zip)->toBe(90210)
56+
;
5557

5658
expect(function () {
5759
/**
5860
* @var positive-int $id
5961
* @var non-empty-string $street
6062
* @var int<10000, 99999> $zip
6163
*/
62-
[$id, [$street, $zip]] = [42, ['', 90210]];
64+
[$id, [$street, $zip]] = [42, ['', 90210]];
6365
})->toThrow(TypeError::class, 'Variable $street must be of type non-empty-string');
6466
});
6567
});
@@ -74,15 +76,17 @@ function processFormatterList(array $formatters, int $id): array
7476
$collection->add(20);
7577

7678
expect($collection->count())->toBe(3)
77-
->and($collection->toArray())->toBe([10, null, 20]);
79+
->and($collection->toArray())->toBe([10, null, 20])
80+
;
7881
});
7982

8083
test('throws TypeError when adding invalid scalar to Collection<?positive-int>', function () {
8184
/** @var GenericCollection<?positive-int> $collection */
8285
$collection = new GenericCollection();
8386

8487
expect(fn () => $collection->add(-50))
85-
->toThrow(TypeError::class, 'Argument $item (template T = ?positive-int) must be of type positive-int, negative int (-50) given');
88+
->toThrow(TypeError::class, 'Argument $item (template T = ?positive-int) must be of type positive-int, negative int (-50) given')
89+
;
8690
});
8791
});
8892

@@ -100,11 +104,12 @@ function processFormatterList(array $formatters, int $id): array
100104
test('throws TypeError when a callable in the collection returns an invalid type', function () {
101105
$formatters = [
102106
fn (int $id): string => "id_{$id}",
103-
fn (int $id): string => '',
107+
fn (int $id): string => '',
104108
];
105109

106110
expect(fn () => processFormatterList($formatters, 42))
107-
->toThrow(TypeError::class, 'Callback $formatters[1] return value must be of type non-empty-string');
111+
->toThrow(TypeError::class, 'Callback $formatters[1] return value must be of type non-empty-string')
112+
;
108113
});
109114
});
110-
});
115+
});

0 commit comments

Comments
 (0)