@@ -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
0 commit comments