Skip to content

Commit ef3ec2d

Browse files
committed
fix php stan errors
1 parent 17a751a commit ef3ec2d

4 files changed

Lines changed: 40 additions & 30 deletions

File tree

src/Contract/ContractParser.php

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,14 @@ public static function parse(string $function): array
100100
return self::$cache[$function] = $contract;
101101
}
102102

103-
/**
104-
* Parses and resolves the @var docblock for a given class property (including PHP 8.4 interface properties).
105-
*/
106103
/**
107104
* Parses and resolves the @var or @property docblock for a given class property.
105+
*
106+
* Resolution Steps:
107+
* 1. Search class and parent class hierarchy for physical properties.
108+
* 2. Search implemented interfaces (PHP 8.4 interface properties).
109+
* 3. Fall back to class-level magic @property tags if enabled and physical property is not found.
110+
* 4. Parse physical @var tags if not already resolved as a magic property.
108111
*/
109112
public static function parseProperty(string $className, string $propertyName): ?TypeNode
110113
{
@@ -126,7 +129,6 @@ public static function parseProperty(string $className, string $propertyName): ?
126129
$typeNode = null;
127130
$isMagicProperty = false;
128131

129-
// 1. Search Class and Parent Class Hierarchy for physical properties
130132
$current = $refClass;
131133
while ($current !== false) {
132134
if ($current->hasProperty($propertyName)) {
@@ -142,7 +144,6 @@ public static function parseProperty(string $className, string $propertyName): ?
142144
$current = $current->getParentClass();
143145
}
144146

145-
// 2. Search Implemented Interfaces (PHP 8.4 Interface Properties)
146147
if ($doc === false) {
147148
foreach ($refClass->getInterfaces() as $interface) {
148149
if ($interface->hasProperty($propertyName)) {
@@ -158,7 +159,6 @@ public static function parseProperty(string $className, string $propertyName): ?
158159
}
159160
}
160161

161-
// 3. NEW: Fallback to class-level magic @property tags if enabled and physical property not found
162162
if ($doc === false && (bool) (Config::get()['magic_properties'] ?? true)) {
163163
$classHierarchy = HierarchyResolver::getClassHierarchy($refClass);
164164
foreach ($classHierarchy as $hierClass) {
@@ -181,13 +181,11 @@ public static function parseProperty(string $className, string $propertyName): ?
181181
return self::$propertyCache[$cacheKey] = null;
182182
}
183183

184-
// Skip property type checks if docblock contains @typephp-ignore
185184
$shouldRespectIgnore = (bool) (Config::get()['respect_ignore_tags'] ?? true);
186185
if ($shouldRespectIgnore && (str_contains($doc, '@typephp-ignore') || str_contains($doc, '@typephp-disable'))) {
187186
return self::$propertyCache[$cacheKey] = null;
188187
}
189188

190-
// 4. Parse physical @var tags if not already resolved as a magic property
191189
if (! $isMagicProperty) {
192190
$phpDocNode = DocblockExtractor::parseDocString($doc);
193191
$varTags = $phpDocNode->getVarTagValues();
@@ -223,6 +221,12 @@ public static function parseProperty(string $className, string $propertyName): ?
223221

224222
/**
225223
* Parses and resolves a class-level @method docblock for __call / __callStatic.
224+
*
225+
* Resolution Steps:
226+
* 1. Search class, parent, interface, and trait hierarchy for @method tags (excluding vendor files).
227+
* 2. Substitute type aliases and resolve FQCNs for parameters and return types.
228+
*
229+
* @return array{return: ?TypeNode, parameters: array<int, array{name: string, type: ?TypeNode, isVariadic: bool, isOptional: bool}>, aliases: array<string, TypeNode>, templates: array<string, TemplateTagValueNode>}|null
226230
*/
227231
public static function parseMagicMethod(string $className, string $methodName): ?array
228232
{
@@ -262,7 +266,7 @@ public static function parseMagicMethod(string $className, string $methodName):
262266
}
263267
}
264268

265-
if ($methodTag === null || $declaringClass === null) {
269+
if ($methodTag === null || $declaringClass === null || $doc === false) {
266270
return self::$magicMethodCache[$cacheKey] = null;
267271
}
268272

@@ -312,12 +316,13 @@ public static function parseMagicMethod(string $className, string $methodName):
312316
}
313317

314318
$pName = ltrim($rawParamName, '$');
319+
$isOptional = (isset($pVars['isOptional']) && (bool) $pVars['isOptional']) || (($p->defaultValue ?? null) !== null);
315320

316321
$resolvedParams[] = [
317322
'name' => $pName,
318323
'type' => $pType,
319-
'isVariadic' => $p->isVariadic ?? false,
320-
'isOptional' => (isset($p->isOptional) ? (bool) $p->isOptional : false) || (($p->defaultValue ?? null) !== null),
324+
'isVariadic' => $p->isVariadic,
325+
'isOptional' => $isOptional,
321326
];
322327
}
323328

src/Internal/Checker/ParamChecker.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
use TypePHP\Validator\TypeValidatorRegistry;
2121

2222
/**
23-
* @internal Evaluates function and method parameter contract validations.
23+
* @internal Evaluates function and method parameter contract validations (including dynamic @method calls via __call / __callStatic).
2424
*/
2525
final class ParamChecker
2626
{
@@ -246,11 +246,7 @@ private static function resolveClassStringTemplate(GenericTypeNode $typeNode, mi
246246
$boundName = $resolvedBound instanceof IdentifierTypeNode ? $resolvedBound->name : (string) $resolvedBound;
247247
$lowerBound = strtolower($boundName);
248248

249-
if ($lowerBound === 'object' || $lowerBound === 'mixed') {
250-
if (! ClassNameValidator::isValid($val) || (! class_exists($val) && ! interface_exists($val) && ! trait_exists($val) && ! enum_exists($val))) {
251-
return ErrorFactory::createError($function . '(): Argument $' . $paramName . ' (class-string<' . $templateName . '>) must be a valid class-string, ' . TypeFormatter::formatGivenValue($val) . ' given');
252-
}
253-
} elseif (! is_a($val, $boundName, true)) {
249+
if ($lowerBound !== 'object' && $lowerBound !== 'mixed' && ! is_a($val, $boundName, true)) {
254250
return ErrorFactory::createError($function . '(): Argument $' . $paramName . ' (class-string<' . $templateName . '>) must be a class-string of ' . $boundName . ", '" . $val . "' given");
255251
}
256252
}

src/Internal/Checker/ReturnChecker.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
use TypePHP\Validator\TypeValidatorRegistry;
1919

2020
/**
21-
* @internal Evaluates function and method return contract validations.
21+
* @internal Evaluates function and method return contract validations (including dynamic @method calls via __call / __callStatic).
2222
*/
2323
final class ReturnChecker
2424
{
@@ -43,7 +43,9 @@ public static function checkReturn(string $function, mixed $value, ?object $this
4343
$isMagicCall = str_ends_with($effectiveFunction, '::__call') || str_ends_with($effectiveFunction, '::__callStatic');
4444
if ($isMagicCall && (bool) (Config::get()['magic_methods'] ?? true)) {
4545
$magicMethodName = array_values($vars)[0] ?? null;
46-
$magicArgs = array_values($vars)[1] ?? [];
46+
$rawMagicArgs = array_values($vars)[1] ?? [];
47+
/** @var array<int|string, mixed> $magicArgs */
48+
$magicArgs = \is_array($rawMagicArgs) ? $rawMagicArgs : [];
4749

4850
if (\is_string($magicMethodName)) {
4951
$className = explode('::', $effectiveFunction, 2)[0];
@@ -73,7 +75,7 @@ public static function checkReturn(string $function, mixed $value, ?object $this
7375
$returnTypeNode = SpecialTypeResolver::resolve($returnTypeNode, $magicFunction, $thisObj);
7476
}
7577

76-
$returnTypeNode = self::resolveConditionalReturnType($returnTypeNode, \is_array($magicArgs) ? $magicArgs : [], $boundTemplates, $registry);
78+
$returnTypeNode = self::resolveConditionalReturnType($returnTypeNode, $magicArgs, $boundTemplates, $registry);
7779

7880
$err = $registry->validate($value, $returnTypeNode, $magicFunction . '(): Return value');
7981
if ($err !== null) {
@@ -135,7 +137,7 @@ public static function checkReturn(string $function, mixed $value, ?object $this
135137
}
136138

137139
/**
138-
* @param array<string, mixed> $vars
140+
* @param array<int|string, mixed> $vars
139141
* @param array<string, TypeNode> $boundTemplates
140142
*/
141143
private static function resolveConditionalReturnType(

src/Resolver/SpecialTypeResolver.php

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -246,22 +246,29 @@ public static function resolveForFile(TypeNode $node, string $file): TypeNode
246246
}
247247

248248
/**
249-
* @param \ReflectionClass<object>|\ReflectionFunction|\ReflectionMethod|string $context
250-
*
251-
* @return \ReflectionClass<object>|\ReflectionFunction|\ReflectionMethod
252-
*/
249+
* @param \ReflectionClass<object>|\ReflectionFunction|\ReflectionMethod|string $context
250+
*
251+
* @return \ReflectionClass<object>|\ReflectionFunction|\ReflectionMethod
252+
*/
253253
private static function getReflectionContext(\ReflectionClass|\ReflectionFunction|\ReflectionMethod|string $context): \ReflectionClass|\ReflectionFunction|\ReflectionMethod
254254
{
255255
if (\is_string($context)) {
256256
if (str_contains($context, '::')) {
257257
[$className, $methodName] = explode('::', $context, 2);
258258

259-
try {
260-
return new \ReflectionMethod($className, $methodName);
261-
} catch (\ReflectionException $e) {
262-
// Method doesn't exist (likely a magic @method). Fall back to Class context.
263-
return new \ReflectionClass($className);
259+
if (class_exists($className) || interface_exists($className) || trait_exists($className)) {
260+
/** @var class-string<object> $className */
261+
try {
262+
return new \ReflectionMethod($className, $methodName);
263+
} catch (\ReflectionException $e) {
264+
return new \ReflectionClass($className);
265+
}
264266
}
267+
268+
/** @var class-string<object> $fallbackClass */
269+
$fallbackClass = \stdClass::class;
270+
271+
return new \ReflectionClass($fallbackClass);
265272
}
266273

267274
return new \ReflectionFunction($context);

0 commit comments

Comments
 (0)