1919use PHPStan \PhpDocParser \Ast \Type \TypeNode ;
2020use PHPStan \PhpDocParser \Ast \Type \UnionTypeNode ;
2121use TypePHP \Internal \Config ;
22+ use TypePHP \Internal \StubManager ;
2223use TypePHP \Resolver \SpecialTypeResolver ;
2324use TypePHP \Validator \TypeValidatorRegistry ;
2425
2526/**
26- * @internal Main orchestrator parsing and caching PHPDoc contracts (@param, @return, @template, @phpstan-type, @var).
27+ * @internal Main orchestrator parsing and caching PHPDoc contracts (@param, @return, @template, @phpstan-type, @var, stubs ).
2728 */
2829final class ContractParser
2930{
@@ -67,6 +68,7 @@ public static function reset(): void
6768 DocblockExtractor::reset ();
6869 FileFilter::reset ();
6970 TypeValidatorRegistry::reset ();
71+ StubManager::reset ();
7072 }
7173
7274 /**
@@ -214,6 +216,12 @@ private static function findDeclaredPropertyDoc(\ReflectionClass $refClass, stri
214216 {
215217 $ current = $ refClass ;
216218 while ($ current !== false ) {
219+ $ className = $ current ->getName ();
220+ $ stubDoc = StubManager::getPropertyDoc ($ className , $ propertyName );
221+ if ($ stubDoc !== null ) {
222+ return ['doc ' => $ stubDoc , 'declaringClass ' => $ current ];
223+ }
224+
217225 if ($ current ->hasProperty ($ propertyName )) {
218226 $ refProp = $ current ->getProperty ($ propertyName );
219227 $ doc = $ refProp ->getDocComment ();
@@ -226,6 +234,12 @@ private static function findDeclaredPropertyDoc(\ReflectionClass $refClass, stri
226234 }
227235
228236 foreach ($ refClass ->getInterfaces () as $ interface ) {
237+ $ interfaceName = $ interface ->getName ();
238+ $ stubDoc = StubManager::getPropertyDoc ($ interfaceName , $ propertyName );
239+ if ($ stubDoc !== null ) {
240+ return ['doc ' => $ stubDoc , 'declaringClass ' => $ interface ];
241+ }
242+
229243 if ($ interface ->hasProperty ($ propertyName )) {
230244 $ interfaceProp = $ interface ->getProperty ($ propertyName );
231245 $ doc = $ interfaceProp ->getDocComment ();
@@ -248,13 +262,16 @@ private static function findMagicPropertyDoc(\ReflectionClass $refClass, string
248262 $ classHierarchy = HierarchyResolver::getClassHierarchy ($ refClass );
249263
250264 foreach ($ classHierarchy as $ hierClass ) {
265+ $ className = $ hierClass ->getName ();
251266 $ fileName = $ hierClass ->getFileName ();
252- if ($ hierClass !== $ refClass && FileFilter::isFileExcluded ($ fileName !== false ? $ fileName : null )) {
267+ $ stubDoc = StubManager::getClassDoc ($ className );
268+
269+ if ($ stubDoc === null && $ hierClass !== $ refClass && FileFilter::isFileExcluded ($ fileName !== false ? $ fileName : null )) {
253270 continue ;
254271 }
255272
256- $ classDoc = $ hierClass ->getDocComment ();
257- if ($ classDoc !== false ) {
273+ $ classDoc = $ stubDoc ?? $ hierClass ->getDocComment ();
274+ if ($ classDoc !== false && $ classDoc !== null ) {
258275 $ extractedType = DocblockExtractor::extractTypeFromClassPropertyDoc ($ classDoc , $ propertyName );
259276 if ($ extractedType !== null ) {
260277 return [
@@ -338,13 +355,16 @@ private static function findMagicMethodDoc(\ReflectionClass $refClass, string $m
338355 $ classHierarchy = HierarchyResolver::getClassHierarchy ($ refClass );
339356
340357 foreach ($ classHierarchy as $ hierClass ) {
358+ $ className = $ hierClass ->getName ();
341359 $ fileName = $ hierClass ->getFileName ();
342- if ($ hierClass !== $ refClass && FileFilter::isFileExcluded ($ fileName !== false ? $ fileName : null )) {
360+ $ stubDoc = StubManager::getClassDoc ($ className );
361+
362+ if ($ stubDoc === null && $ hierClass !== $ refClass && FileFilter::isFileExcluded ($ fileName !== false ? $ fileName : null )) {
343363 continue ;
344364 }
345365
346- $ classDoc = $ hierClass ->getDocComment ();
347- if ($ classDoc !== false ) {
366+ $ classDoc = $ stubDoc ?? $ hierClass ->getDocComment ();
367+ if ($ classDoc !== false && $ classDoc !== null ) {
348368 $ tag = DocblockExtractor::extractMagicMethodContract ($ classDoc , $ methodName );
349369 if ($ tag !== null ) {
350370 return [
@@ -494,8 +514,11 @@ private static function parseFunction(\ReflectionFunction $ref): array
494514 $ returnType = null ;
495515 $ aliases = [];
496516
497- $ doc = $ ref ->getDocComment ();
498- if ($ doc === false ) {
517+ $ funcName = $ ref ->getName ();
518+ $ stubDoc = StubManager::getFunctionDoc ($ funcName );
519+ $ doc = $ stubDoc ?? $ ref ->getDocComment ();
520+
521+ if ($ doc === false || $ doc === null ) {
499522 return [
500523 'types ' => [],
501524 'templates ' => [],
@@ -549,7 +572,6 @@ private static function parseFunction(\ReflectionFunction $ref): array
549572
550573 /**
551574 * Resolves class-level docblocks (templates and aliases) up the class inheritance chain.
552- * Memoizes results in $classLevelDocCache per class name for O(1) performance across method calls.
553575 *
554576 * @param \ReflectionClass<object> $declaringClass
555577 * @param array<string, TemplateTagValueNode> $templates
@@ -569,13 +591,16 @@ private static function parseClassLevelDocs(\ReflectionClass $declaringClass, ar
569591 $ classHierarchy = HierarchyResolver::getClassHierarchy ($ declaringClass );
570592
571593 foreach ($ classHierarchy as $ hierClass ) {
594+ $ hierClassName = $ hierClass ->getName ();
572595 $ fileName = $ hierClass ->getFileName ();
573- if (FileFilter::isFileExcluded ($ fileName !== false ? $ fileName : null )) {
596+ $ stubDoc = StubManager::getClassDoc ($ hierClassName );
597+
598+ if ($ stubDoc === null && FileFilter::isFileExcluded ($ fileName !== false ? $ fileName : null )) {
574599 continue ;
575600 }
576601
577- $ classDoc = $ hierClass ->getDocComment ();
578- if ($ classDoc !== false ) {
602+ $ classDoc = $ stubDoc ?? $ hierClass ->getDocComment ();
603+ if ($ classDoc !== false && $ classDoc !== null ) {
579604 $ classPhpDocNode = DocblockExtractor::parseDocString ($ classDoc );
580605
581606 foreach (DocblockExtractor::extractTemplates ($ classPhpDocNode ) as $ name => $ tag ) {
@@ -624,14 +649,17 @@ private static function parseMethodHierarchyDocs(
624649
625650 foreach ($ hierarchy as $ hierRef ) {
626651 $ isOriginal = ($ hierRef === $ ref );
652+ $ declaringClass = $ hierRef ->getDeclaringClass ()->getName ();
653+ $ methodName = $ hierRef ->getName ();
654+ $ stubDoc = StubManager::getMethodDoc ($ declaringClass , $ methodName );
627655
628656 $ fileName = $ hierRef ->getFileName ();
629- if (! $ isOriginal && FileFilter::isFileExcluded ($ fileName !== false ? $ fileName : null )) {
657+ if ($ stubDoc === null && ! $ isOriginal && FileFilter::isFileExcluded ($ fileName !== false ? $ fileName : null )) {
630658 continue ;
631659 }
632660
633- $ doc = $ hierRef ->getDocComment ();
634- if ($ doc === false ) {
661+ $ doc = $ stubDoc ?? $ hierRef ->getDocComment ();
662+ if ($ doc === false || $ doc === null ) {
635663 continue ;
636664 }
637665
@@ -729,10 +757,13 @@ private static function applyConstructorPromotionFallback(\ReflectionMethod $ref
729757 $ paramName = $ p ->getName ();
730758
731759 if (! isset ($ types [$ paramName ]) && $ declaringClass ->hasProperty ($ paramName )) {
760+ $ className = $ declaringClass ->getName ();
761+ $ stubDoc = StubManager::getPropertyDoc ($ className , $ paramName );
762+
732763 $ propertyRef = $ declaringClass ->getProperty ($ paramName );
733- $ propDoc = $ propertyRef ->getDocComment ();
764+ $ propDoc = $ stubDoc ?? $ propertyRef ->getDocComment ();
734765
735- if ($ propDoc !== false ) {
766+ if ($ propDoc !== false && $ propDoc !== null ) {
736767 $ propType = DocblockExtractor::extractTypeFromPropertyDoc ($ propDoc , $ paramName );
737768 if ($ propType !== null ) {
738769 if ($ p ->hasType ()) {
0 commit comments