Skip to content

Commit adae7d7

Browse files
committed
Fix more edge cases and add type alias support for inline variable validation
1 parent 1bfea4a commit adae7d7

4 files changed

Lines changed: 81 additions & 4 deletions

File tree

src/Contract/ContractParser.php

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,23 @@ public static function parse(string $function): array
4848
try {
4949
if (str_contains($function, '::')) {
5050
[$className, $methodName] = explode('::', $function, 2);
51-
$ref = new \ReflectionMethod($className, $methodName);
52-
$contract = self::parseMethod($ref);
51+
52+
/** @var class-string<object> $className */
53+
$refClass = new \ReflectionClass($className);
54+
if ($refClass->hasMethod($methodName)) {
55+
$ref = $refClass->getMethod($methodName);
56+
$contract = self::parseMethod($ref);
57+
} else {
58+
$templates = [];
59+
$aliases = [];
60+
self::parseClassLevelDocs($refClass, $templates, $aliases);
61+
$contract = [
62+
'types' => [],
63+
'templates' => $templates,
64+
'return' => null,
65+
'aliases' => $aliases,
66+
];
67+
}
5368
} else {
5469
$ref = new \ReflectionFunction($function);
5570
$contract = self::parseFunction($ref);
@@ -81,7 +96,7 @@ public static function parseProperty(string $className, string $propertyName): ?
8196
$doc = false;
8297
$declaringClass = null;
8398

84-
// 1. Search Class and Parent Class Hierarchy
99+
// Search Class and Parent Class Hierarchy
85100
$current = $refClass;
86101
while ($current !== false) {
87102
if ($current->hasProperty($propertyName)) {
@@ -97,7 +112,7 @@ public static function parseProperty(string $className, string $propertyName): ?
97112
$current = $current->getParentClass();
98113
}
99114

100-
// 2. Search Implemented Interfaces (PHP 8.4 Interface Properties)
115+
// Search Implemented Interfaces (PHP 8.4 Interface Properties)
101116
if ($doc === false) {
102117
foreach ($refClass->getInterfaces() as $interface) {
103118
if ($interface->hasProperty($propertyName)) {
@@ -143,6 +158,34 @@ public static function parseProperty(string $className, string $propertyName): ?
143158
}
144159
}
145160

161+
/**
162+
* Extracts and returns all class-level type aliases for a given class.
163+
*
164+
* @return array<string, TypeNode>
165+
*/
166+
public static function parseClassAliases(string $className): array
167+
{
168+
if (! class_exists($className) && ! interface_exists($className) && ! trait_exists($className)) {
169+
return [];
170+
}
171+
172+
try {
173+
$refClass = new \ReflectionClass($className);
174+
$doc = $refClass->getDocComment();
175+
if ($doc === false) {
176+
return [];
177+
}
178+
179+
$phpDocNode = DocblockExtractor::parseDocString($doc);
180+
$aliases = [];
181+
DocblockExtractor::extractAliases($phpDocNode, $aliases, $refClass);
182+
183+
return $aliases;
184+
} catch (\Throwable $e) {
185+
return [];
186+
}
187+
}
188+
146189
/**
147190
* Orchestrates parsing for class methods across the inheritance hierarchy.
148191
*
@@ -264,8 +307,10 @@ private static function parseClassLevelDocs(\ReflectionClass $declaringClass, ar
264307
/**
265308
* Resolves method-level docblocks (@param, @return, @template, aliases) up the method hierarchy.
266309
*
310+
* @param \ReflectionMethod $ref
267311
* @param array<string, TypeNode> $types
268312
* @param array<string, TemplateTagValueNode> $templates
313+
* @param TypeNode|null $returnType
269314
* @param array<string, TypeNode> $aliases
270315
*/
271316
private static function parseMethodHierarchyDocs(

src/Internal/Checker/InlineChecker.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,25 @@ public static function checkVariable(mixed $value, string $typeString, string $v
6767
$typeNode = SpecialTypeResolver::resolveForFile($typeNode, $file);
6868
}
6969

70+
// Resolve local class-level aliases for the executing context by traversing back the call stack
71+
$className = null;
72+
$trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
73+
foreach ($trace as $frame) {
74+
$classCandidate = $frame['class'] ?? null;
75+
if ($classCandidate !== null && ! str_starts_with($classCandidate, 'TypePHP\\Internal\\') && ! str_starts_with($classCandidate, 'TypePHP\\Wrapper\\')) {
76+
$className = $classCandidate;
77+
78+
break;
79+
}
80+
}
81+
82+
if ($className !== null) {
83+
$classAliases = ContractParser::parseClassAliases($className);
84+
if (\count($classAliases) > 0) {
85+
$typeNode = TemplateSubstitutor::substitute($typeNode, $classAliases);
86+
}
87+
}
88+
7089
if (! self::shouldValidateType($typeNode, $config)) {
7190
return $value;
7291
}

src/Internal/StreamWrapper.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,17 @@ public function stream_lock(int $operation): bool
239239
return @flock($this->handle, $operation);
240240
}
241241

242+
public function stream_tell(): int
243+
{
244+
if ($this->handle === null) {
245+
return 0;
246+
}
247+
248+
$res = ftell($this->handle);
249+
250+
return $res !== false ? $res : 0;
251+
}
252+
242253
public function stream_flush(): bool
243254
{
244255
if ($this->handle === null) {

src/Internal/StreamWrapperInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,6 @@ public function stream_metadata(string $path, int $option, mixed $value): bool;
6363
* @return resource|false
6464
*/
6565
public function stream_cast(int $cast_as);
66+
67+
public function stream_tell(): int;
6668
}

0 commit comments

Comments
 (0)