Skip to content

Commit 4a0bf91

Browse files
committed
Enhance ReturnChecker and IterableWrapper for improved type handling; add ClassUsingAliasedTraitMethod for trait method aliasing tests; update AdvancedEdgeCasesTest with nested conditional return type tests and trait method aliasing validation.
1 parent 7d50b73 commit 4a0bf91

4 files changed

Lines changed: 66 additions & 21 deletions

File tree

src/Internal/Checker/ReturnChecker.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,9 @@ public static function checkReturn(string $function, mixed $value, object|string
124124
if ($value instanceof \Traversable) {
125125
$baseName = '';
126126
if ($returnTypeNode instanceof IdentifierTypeNode) {
127-
$baseName = strtolower($returnTypeNode->name);
127+
$baseName = strtolower(ltrim($returnTypeNode->name, '\\'));
128128
} elseif ($returnTypeNode instanceof GenericTypeNode) {
129-
$baseName = strtolower($returnTypeNode->type->name);
129+
$baseName = strtolower(ltrim($returnTypeNode->type->name, '\\'));
130130
}
131131

132132
$standardIterables = ['iterable', 'traversable', 'iterator', 'generator', 'iteratoraggregate', 'array'];
@@ -139,6 +139,8 @@ public static function checkReturn(string $function, mixed $value, object|string
139139
}
140140

141141
/**
142+
* Recursively resolves multi-branch nested conditional return types.
143+
*
142144
* @param array<int|string, mixed> $vars
143145
* @param array<string, TypeNode> $boundTemplates
144146
*/
@@ -159,7 +161,9 @@ private static function resolveConditionalReturnType(
159161
$isTargetMatch = ! $isTargetMatch;
160162
}
161163

162-
return $isTargetMatch ? $returnTypeNode->if : $returnTypeNode->else;
164+
$selectedBranch = $isTargetMatch ? $returnTypeNode->if : $returnTypeNode->else;
165+
166+
return self::resolveConditionalReturnType($selectedBranch, $vars, $boundTemplates, $registry);
163167
}
164168

165169
if ($returnTypeNode instanceof ConditionalTypeNode) {
@@ -188,9 +192,11 @@ private static function resolveConditionalReturnType(
188192
$isTargetMatch = ! $isTargetMatch;
189193
}
190194

191-
return $isTargetMatch ? $returnTypeNode->if : $returnTypeNode->else;
195+
$selectedBranch = $isTargetMatch ? $returnTypeNode->if : $returnTypeNode->else;
196+
197+
return self::resolveConditionalReturnType($selectedBranch, $vars, $boundTemplates, $registry);
192198
}
193199

194200
return $returnTypeNode;
195201
}
196-
}
202+
}

src/Wrapper/IterableWrapper.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace TypePHP\Wrapper;
66

7+
use ArrayIterator;
78
use Generator;
89
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
910
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
@@ -28,7 +29,6 @@ public static function wrap(string $function, string $paramName, mixed $iterable
2829
return $iterable;
2930
}
3031

31-
// Preserve native PHP arrays for all standard function parameters!
3232
if (\is_array($iterable) && $paramName !== 'return') {
3333
return $iterable;
3434
}
@@ -40,9 +40,9 @@ public static function wrap(string $function, string $paramName, mixed $iterable
4040
if ($typeNode !== null) {
4141
$baseName = '';
4242
if ($typeNode instanceof IdentifierTypeNode) {
43-
$baseName = strtolower($typeNode->name);
43+
$baseName = strtolower(ltrim($typeNode->name, '\\'));
4444
} elseif ($typeNode instanceof GenericTypeNode) {
45-
$baseName = strtolower($typeNode->type->name);
45+
$baseName = strtolower(ltrim($typeNode->type->name, '\\'));
4646
}
4747

4848
$standardIterables = ['iterable', 'traversable', 'iterator', 'generator', 'iteratoraggregate', 'array'];
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TypePHP\Tests\Fixtures\Services;
6+
7+
class ClassUsingAliasedTraitMethod
8+
{
9+
use ShiftedLoggerTrait {
10+
logEvent as recordAuditLog; // Aliases logEvent -> recordAuditLog
11+
}
12+
}

tests/TypeChecking/Boundaries/AdvancedEdgeCasesTest.php

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
use TypePHP\Tests\Fixtures\Generics\GenericCollection;
77

88
/**
9-
* 1. Function accepting a list of lazy callables
9+
* Function accepting a list of lazy callables
1010
*
1111
* @param list<callable(positive-int): non-empty-string> $formatters
1212
*/
@@ -20,14 +20,27 @@ function processFormatterList(array $formatters, int $id): array
2020
return $results;
2121
}
2222

23+
/**
24+
* Multi-Branch Nested Conditional Return Type
25+
*
26+
* @param string $format
27+
* @param mixed $value
28+
*
29+
* @return ($format is 'int' ? positive-int : ($format is 'float' ? positive-float : non-empty-string))
30+
*/
31+
function testNestedConditionalReturn(string $format, mixed $value): mixed
32+
{
33+
return $value;
34+
}
35+
2336
describe('Advanced Edge-Case Behaviors', function () {
2437
describe('Skipped and Deeply Nested Array Destructuring with @var', function () {
2538
test('validates variables when skipping elements with empty commas in destructuring', function () {
2639
/**
2740
* @var positive-int $id
2841
* @var non-empty-string $username
2942
*/
30-
[$id, , $username] = [10, 'skipped_token', 'Alice'];
43+
[$id,, $username] = [10, 'skipped_token', 'Alice'];
3144

3245
expect($id)->toBe(10)
3346
->and($username)->toBe('Alice')
@@ -38,7 +51,7 @@ function processFormatterList(array $formatters, int $id): array
3851
* @var positive-int $id
3952
* @var non-empty-string $username
4053
*/
41-
[$id, , $username] = [-5, 'skipped_token', 'Alice'];
54+
[$id,, $username] = [-5, 'skipped_token', 'Alice'];
4255
})->toThrow(TypeError::class, 'Variable $id must be of type positive-int');
4356
});
4457

@@ -84,17 +97,16 @@ function processFormatterList(array $formatters, int $id): array
8497
/** @var GenericCollection<?positive-int> $collection */
8598
$collection = new GenericCollection();
8699

87-
expect(fn () => $collection->add(-50))
88-
->toThrow(TypeError::class, 'Argument $item (template T = ?positive-int) must be of type positive-int, negative int (-50) given')
89-
;
100+
expect(fn() => $collection->add(-50))
101+
->toThrow(TypeError::class, 'Argument $item (template T = ?positive-int) must be of type positive-int, negative int (-50) given');
90102
});
91103
});
92104

93105
describe('Collections of Lazy Callables', function () {
94106
test('executes and validates a list of lazy callable proxies', function () {
95107
$formatters = [
96-
fn (int $id): string => "id_{$id}",
97-
fn (int $id): string => "user#{$id}",
108+
fn(int $id): string => "id_{$id}",
109+
fn(int $id): string => "user#{$id}",
98110
];
99111

100112
$results = processFormatterList($formatters, 42);
@@ -103,13 +115,28 @@ function processFormatterList(array $formatters, int $id): array
103115

104116
test('throws TypeError when a callable in the collection returns an invalid type', function () {
105117
$formatters = [
106-
fn (int $id): string => "id_{$id}",
107-
fn (int $id): string => '',
118+
fn(int $id): string => "id_{$id}",
119+
fn(int $id): string => '',
108120
];
109121

110-
expect(fn () => processFormatterList($formatters, 42))
111-
->toThrow(TypeError::class, 'Callback $formatters[1] return value must be of type non-empty-string')
112-
;
122+
expect(fn() => processFormatterList($formatters, 42))
123+
->toThrow(TypeError::class, 'Callback $formatters[1] return value must be of type non-empty-string');
124+
});
125+
});
126+
127+
describe('Multi-Branch Nested Conditional Return Types', function () {
128+
test('throws TypeError when return value violates nested conditional branch', function () {
129+
expect(fn() => testNestedConditionalReturn('float', -5.5))
130+
->toThrow(TypeError::class, 'Return value must be of type positive-float');
131+
});
132+
});
133+
134+
describe('Trait Method Aliasing (use Trait { old as new; })', function () {
135+
test('inherits DocBlock contracts when a Trait method is aliased in a class', function () {
136+
$service = new TypePHP\Tests\Fixtures\Services\ClassUsingAliasedTraitMethod();
137+
138+
expect(fn() => $service->recordAuditLog(-1, 'audit_ok'))
139+
->toThrow(TypeError::class, 'Argument $level must be of type positive-int');
113140
});
114141
});
115142
});

0 commit comments

Comments
 (0)