27
27
*/
28
28
class DebugClassLoader
29
29
{
30
+ private const SPECIAL_RETURN_TYPES = [
31
+ 'mixed ' => 'mixed ' ,
32
+ 'void ' => 'void ' ,
33
+ 'null ' => 'null ' ,
34
+ 'resource ' => 'resource ' ,
35
+ 'static ' => 'object ' ,
36
+ '$this ' => 'object ' ,
37
+ 'boolean ' => 'bool ' ,
38
+ 'true ' => 'bool ' ,
39
+ 'false ' => 'bool ' ,
40
+ 'integer ' => 'int ' ,
41
+ 'array ' => 'array ' ,
42
+ 'bool ' => 'bool ' ,
43
+ 'callable ' => 'callable ' ,
44
+ 'float ' => 'float ' ,
45
+ 'int ' => 'integer ' ,
46
+ 'iterable ' => 'iterable ' ,
47
+ 'object ' => 'object ' ,
48
+ 'string ' => 'string ' ,
49
+ 'self ' => 'self ' ,
50
+ 'parent ' => 'parent ' ,
51
+ ];
52
+
53
+ private const BUILTIN_RETURN_TYPES = [
54
+ 'void ' => true ,
55
+ 'array ' => true ,
56
+ 'bool ' => true ,
57
+ 'callable ' => true ,
58
+ 'float ' => true ,
59
+ 'int ' => true ,
60
+ 'iterable ' => true ,
61
+ 'object ' => true ,
62
+ 'string ' => true ,
63
+ 'self ' => true ,
64
+ 'parent ' => true ,
65
+ ];
66
+
30
67
private $ classLoader ;
31
68
private $ isFinder ;
32
69
private $ loaded = [];
@@ -40,6 +77,8 @@ class DebugClassLoader
40
77
private static $ annotatedParameters = [];
41
78
private static $ darwinCache = ['/ ' => ['/ ' , []]];
42
79
private static $ method = [];
80
+ private static $ returnTypes = [];
81
+ private static $ methodTraits = [];
43
82
44
83
public function __construct (callable $ classLoader )
45
84
{
@@ -218,11 +257,11 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
218
257
$ deprecations = [];
219
258
220
259
// Don't trigger deprecations for classes in the same vendor
221
- if (2 > $ len = 1 + (strpos ($ class , '\\' ) ?: strpos ($ class , '_ ' ))) {
222
- $ len = 0 ;
223
- $ ns = '' ;
260
+ if (2 > $ vendorLen = 1 + (strpos ($ class , '\\' ) ?: strpos ($ class , '_ ' ))) {
261
+ $ vendorLen = 0 ;
262
+ $ vendor = '' ;
224
263
} else {
225
- $ ns = str_replace ('_ ' , '\\' , substr ($ class , 0 , $ len ));
264
+ $ vendor = str_replace ('_ ' , '\\' , substr ($ class , 0 , $ vendorLen ));
226
265
}
227
266
228
267
// Detect annotations on the class
@@ -252,7 +291,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
252
291
}
253
292
}
254
293
255
- $ parent = get_parent_class ($ class );
294
+ $ parent = get_parent_class ($ class ) ?: null ;
256
295
$ parentAndOwnInterfaces = $ this ->getOwnInterfaces ($ class , $ parent );
257
296
if ($ parent ) {
258
297
$ parentAndOwnInterfaces [$ parent ] = $ parent ;
@@ -271,13 +310,13 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
271
310
if (!isset (self ::$ checkedClasses [$ use ])) {
272
311
$ this ->checkClass ($ use );
273
312
}
274
- if (isset (self ::$ deprecated [$ use ]) && strncmp ($ ns , str_replace ('_ ' , '\\' , $ use ), $ len ) && !isset (self ::$ deprecated [$ class ])) {
313
+ if (isset (self ::$ deprecated [$ use ]) && strncmp ($ vendor , str_replace ('_ ' , '\\' , $ use ), $ vendorLen ) && !isset (self ::$ deprecated [$ class ])) {
275
314
$ type = class_exists ($ class , false ) ? 'class ' : (interface_exists ($ class , false ) ? 'interface ' : 'trait ' );
276
315
$ verb = class_exists ($ use , false ) || interface_exists ($ class , false ) ? 'extends ' : (interface_exists ($ use , false ) ? 'implements ' : 'uses ' );
277
316
278
317
$ deprecations [] = sprintf ('The "%s" %s %s "%s" that is deprecated%s. ' , $ class , $ type , $ verb , $ use , self ::$ deprecated [$ use ]);
279
318
}
280
- if (isset (self ::$ internal [$ use ]) && strncmp ($ ns , str_replace ('_ ' , '\\' , $ use ), $ len )) {
319
+ if (isset (self ::$ internal [$ use ]) && strncmp ($ vendor , str_replace ('_ ' , '\\' , $ use ), $ vendorLen )) {
281
320
$ deprecations [] = sprintf ('The "%s" %s is considered internal%s. It may change without further notice. You should not use it from "%s". ' , $ use , class_exists ($ use , false ) ? 'class ' : (interface_exists ($ use , false ) ? 'interface ' : 'trait ' ), self ::$ internal [$ use ], $ class );
282
321
}
283
322
if (isset (self ::$ method [$ use ])) {
@@ -305,15 +344,24 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
305
344
}
306
345
307
346
if (trait_exists ($ class )) {
347
+ $ file = $ refl ->getFileName ();
348
+
349
+ foreach ($ refl ->getMethods (\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED ) as $ method ) {
350
+ if ($ method ->getFileName () === $ file ) {
351
+ self ::$ methodTraits [$ file ][$ method ->getStartLine ()] = $ class ;
352
+ }
353
+ }
354
+
308
355
return $ deprecations ;
309
356
}
310
357
311
- // Inherit @final, @internal and @param annotations for methods
358
+ // Inherit @final, @internal, @param and @return annotations for methods
312
359
self ::$ finalMethods [$ class ] = [];
313
360
self ::$ internalMethods [$ class ] = [];
314
361
self ::$ annotatedParameters [$ class ] = [];
362
+ self ::$ returnTypes [$ class ] = [];
315
363
foreach ($ parentAndOwnInterfaces as $ use ) {
316
- foreach (['finalMethods ' , 'internalMethods ' , 'annotatedParameters ' ] as $ property ) {
364
+ foreach (['finalMethods ' , 'internalMethods ' , 'annotatedParameters ' , ' returnTypes ' ] as $ property ) {
317
365
if (isset (self ::$ {$ property }[$ use ])) {
318
366
self ::$ {$ property }[$ class ] = self ::$ {$ property }[$ class ] ? self ::$ {$ property }[$ use ] + self ::$ {$ property }[$ class ] : self ::$ {$ property }[$ use ];
319
367
}
@@ -325,6 +373,16 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
325
373
continue ;
326
374
}
327
375
376
+ if (null === $ ns = self ::$ methodTraits [$ method ->getFileName ()][$ method ->getStartLine ()] ?? null ) {
377
+ $ ns = $ vendor ;
378
+ $ len = $ vendorLen ;
379
+ } elseif (2 > $ len = 1 + (strpos ($ ns , '\\' ) ?: strpos ($ ns , '_ ' ))) {
380
+ $ len = 0 ;
381
+ $ ns = '' ;
382
+ } else {
383
+ $ ns = str_replace ('_ ' , '\\' , substr ($ ns , 0 , $ len ));
384
+ }
385
+
328
386
if ($ parent && isset (self ::$ finalMethods [$ parent ][$ method ->name ])) {
329
387
list ($ declaringClass , $ message ) = self ::$ finalMethods [$ parent ][$ method ->name ];
330
388
$ deprecations [] = sprintf ('The "%s::%s()" method is considered final%s. It may change without further notice as of its next major version. You should not extend it from "%s". ' , $ declaringClass , $ method ->name , $ message , $ class );
@@ -353,10 +411,26 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
353
411
}
354
412
}
355
413
414
+ if (isset (self ::$ returnTypes [$ class ][$ method ->name ]) && !$ method ->hasReturnType () && !($ doc && preg_match ('/\n\s+\* @return +(\S+)/ ' , $ doc ))) {
415
+ list ($ returnType , $ declaringClass , $ declaringFile ) = self ::$ returnTypes [$ class ][$ method ->name ];
416
+
417
+ if (strncmp ($ ns , $ declaringClass , $ len )) {
418
+ //if (0 === strpos($class, 'Symfony\\')) {
419
+ // self::patchMethod($method, $returnType, $declaringFile);
420
+ //}
421
+
422
+ $ deprecations [] = sprintf ('Method "%s::%s()" will return "%s" as of its next major version. Doing the same in child class "%s" will be required when upgrading. ' , $ declaringClass , $ method ->name , $ returnType , $ class );
423
+ }
424
+ }
425
+
356
426
if (!$ doc ) {
357
427
continue ;
358
428
}
359
429
430
+ if (!$ method ->hasReturnType () && false !== strpos ($ doc , '@return ' ) && preg_match ('/\n\s+\* @return +(\S+)/ ' , $ doc , $ matches )) {
431
+ $ this ->setReturnType ($ matches [1 ], $ method , $ parent );
432
+ }
433
+
360
434
$ finalOrInternal = false ;
361
435
362
436
foreach (['final ' , 'internal ' ] as $ annotation ) {
@@ -496,11 +570,9 @@ private function darwinRealpath(string $real): string
496
570
/**
497
571
* `class_implements` includes interfaces from the parents so we have to manually exclude them.
498
572
*
499
- * @param string|false $parent
500
- *
501
573
* @return string[]
502
574
*/
503
- private function getOwnInterfaces (string $ class , $ parent ): array
575
+ private function getOwnInterfaces (string $ class , ? string $ parent ): array
504
576
{
505
577
$ ownInterfaces = class_implements ($ class , false );
506
578
@@ -518,4 +590,191 @@ private function getOwnInterfaces(string $class, $parent): array
518
590
519
591
return $ ownInterfaces ;
520
592
}
593
+
594
+ private function setReturnType (string $ types , \ReflectionMethod $ method , ?string $ parent ): void
595
+ {
596
+ $ nullable = false ;
597
+ $ typesMap = [];
598
+ foreach (explode ('| ' , $ types ) as $ t ) {
599
+ $ t = $ this ->normalizeType ($ t , $ method ->class , $ parent );
600
+ $ typesMap [strtolower ($ t )] = $ t ;
601
+ }
602
+
603
+ if (isset ($ typesMap ['array ' ]) && isset ($ typesMap ['iterable ' ])) {
604
+ if ('[] ' === substr ($ typesMap ['array ' ], -2 )) {
605
+ $ typesMap ['iterable ' ] = $ typesMap ['array ' ];
606
+ }
607
+ unset($ typesMap ['array ' ]);
608
+ }
609
+
610
+ $ normalizedType = key ($ typesMap );
611
+ $ returnType = current ($ typesMap );
612
+
613
+ foreach ($ typesMap as $ n => $ t ) {
614
+ if ('null ' === $ n ) {
615
+ $ nullable = true ;
616
+ } elseif ('null ' === $ normalizedType ) {
617
+ $ normalizedType = $ t ;
618
+ $ returnType = $ t ;
619
+ } elseif ($ n !== $ normalizedType ) {
620
+ // ignore multi-types return declarations
621
+ return ;
622
+ }
623
+ }
624
+
625
+ if ('void ' === $ normalizedType ) {
626
+ $ nullable = false ;
627
+ } elseif (!isset (self ::BUILTIN_RETURN_TYPES [$ normalizedType ]) && isset (self ::SPECIAL_RETURN_TYPES [$ normalizedType ])) {
628
+ // ignore other special return types
629
+ return ;
630
+ }
631
+
632
+ if ($ nullable ) {
633
+ $ returnType = '? ' .$ returnType ;
634
+ }
635
+
636
+ self ::$ returnTypes [$ method ->class ][$ method ->name ] = [$ returnType , $ method ->class , $ method ->getFileName ()];
637
+ }
638
+
639
+ private function normalizeType (string $ type , string $ class , ?string $ parent ): string
640
+ {
641
+ if (isset (self ::SPECIAL_RETURN_TYPES [$ lcType = strtolower ($ type )])) {
642
+ if ('parent ' === $ lcType = self ::SPECIAL_RETURN_TYPES [$ lcType ]) {
643
+ $ lcType = null !== $ parent ? '\\' .$ parent : 'parent ' ;
644
+ } elseif ('self ' === $ lcType ) {
645
+ $ lcType = '\\' .$ class ;
646
+ }
647
+
648
+ return $ lcType ;
649
+ }
650
+
651
+ if ('[] ' === substr ($ type , -2 )) {
652
+ return 'array ' ;
653
+ }
654
+
655
+ if (preg_match ('/^(array|iterable|callable) *[<(]/ ' , $ lcType , $ m )) {
656
+ return $ m [1 ];
657
+ }
658
+
659
+ // We could resolve "use" statements to return the FQDN
660
+ // but this would be too expensive for a runtime checker
661
+
662
+ return $ type ;
663
+ }
664
+
665
+ /**
666
+ * Utility method to add @return annotations to the Symfony code-base where it triggers a self-deprecations.
667
+ */
668
+ private static function patchMethod (\ReflectionMethod $ method , string $ returnType , string $ declaringFile )
669
+ {
670
+ static $ patchedMethods = [];
671
+ static $ useStatements = [];
672
+
673
+ if (!file_exists ($ file = $ method ->getFileName ()) || isset ($ patchedMethods [$ file ][$ startLine = $ method ->getStartLine ()])) {
674
+ return ;
675
+ }
676
+
677
+ $ patchedMethods [$ file ][$ startLine ] = true ;
678
+ $ patchedMethods [$ file ][0 ] = $ patchedMethods [$ file ][0 ] ?? 0 ;
679
+ $ startLine += $ patchedMethods [$ file ][0 ] - 2 ;
680
+ $ nullable = '? ' === $ returnType [0 ] ? '? ' : '' ;
681
+ $ returnType = ltrim ($ returnType , '? ' );
682
+ $ code = file ($ file );
683
+
684
+ if (!isset (self ::BUILTIN_RETURN_TYPES [$ returnType ]) && ('\\' !== $ returnType [0 ] || $ p = strrpos ($ returnType , '\\' , 1 ))) {
685
+ list ($ namespace , $ useOffset , $ useMap ) = $ useStatements [$ file ] ?? $ useStatements [$ file ] = self ::getUseStatements ($ file );
686
+
687
+ if ('\\' !== $ returnType [0 ]) {
688
+ list ($ declaringNamespace , , $ declaringUseMap ) = $ useStatements [$ declaringFile ] ?? $ useStatements [$ declaringFile ] = self ::getUseStatements ($ declaringFile );
689
+
690
+ $ p = strpos ($ returnType , '\\' , 1 );
691
+ $ alias = $ p ? substr ($ returnType , 0 , $ p ) : $ returnType ;
692
+
693
+ if (isset ($ declaringUseMap [$ alias ])) {
694
+ $ returnType = '\\' .$ declaringUseMap [$ alias ].($ p ? substr ($ returnType , $ p ) : '' );
695
+ } else {
696
+ $ returnType = '\\' .$ declaringNamespace .$ returnType ;
697
+ }
698
+
699
+ $ p = strrpos ($ returnType , '\\' , 1 );
700
+ }
701
+
702
+ $ alias = substr ($ returnType , 1 + $ p );
703
+ $ returnType = substr ($ returnType , 1 );
704
+
705
+ if (!isset ($ useMap [$ alias ]) && (class_exists ($ c = $ namespace .$ alias ) || interface_exists ($ c ) || trait_exists ($ c ))) {
706
+ $ useMap [$ alias ] = $ c ;
707
+ }
708
+
709
+ if (!isset ($ useMap [$ alias ])) {
710
+ $ useStatements [$ file ][2 ][$ alias ] = $ returnType ;
711
+ $ code [$ useOffset ] = "use $ returnType; \n" .$ code [$ useOffset ];
712
+ ++$ patchedMethods [$ file ][0 ];
713
+ } elseif ($ useMap [$ alias ] !== $ returnType ) {
714
+ $ alias .= 'FIXME ' ;
715
+ $ useStatements [$ file ][2 ][$ alias ] = $ returnType ;
716
+ $ code [$ useOffset ] = "use $ returnType as $ alias; \n" .$ code [$ useOffset ];
717
+ ++$ patchedMethods [$ file ][0 ];
718
+ }
719
+
720
+ $ returnType = $ alias ;
721
+ }
722
+
723
+ if ($ method ->getDocComment ()) {
724
+ $ code [$ startLine ] = " * @return $ nullable$ returnType \n" .$ code [$ startLine ];
725
+ } else {
726
+ $ code [$ startLine ] .= <<<EOTXT
727
+ /**
728
+ * @return $ nullable$ returnType
729
+ */
730
+
731
+ EOTXT ;
732
+ }
733
+
734
+ $ patchedMethods [$ file ][0 ] += substr_count ($ code [$ startLine ], "\n" ) - 1 ;
735
+ file_put_contents ($ file , $ code );
736
+ }
737
+
738
+ private static function getUseStatements (string $ file ): array
739
+ {
740
+ $ namespace = '' ;
741
+ $ useMap = [];
742
+ $ useOffset = 0 ;
743
+
744
+ if (!file_exists ($ file )) {
745
+ return [$ namespace , $ useOffset , $ useMap ];
746
+ }
747
+
748
+ $ file = file ($ file );
749
+
750
+ for ($ i = 0 ; $ i < \count ($ file ); ++$ i ) {
751
+ if (preg_match ('/^(class|interface|trait|abstract) / ' , $ file [$ i ])) {
752
+ break ;
753
+ }
754
+
755
+ if (0 === strpos ($ file [$ i ], 'namespace ' )) {
756
+ $ namespace = substr ($ file [$ i ], \strlen ('namespace ' ), -2 ).'\\' ;
757
+ $ useOffset = $ i + 2 ;
758
+ }
759
+
760
+ if (0 === strpos ($ file [$ i ], 'use ' )) {
761
+ $ useOffset = $ i ;
762
+
763
+ for (; 0 === strpos ($ file [$ i ], 'use ' ); ++$ i ) {
764
+ $ u = explode (' as ' , substr ($ file [$ i ], 4 , -2 ), 2 );
765
+
766
+ if (1 === \count ($ u )) {
767
+ $ p = strrpos ($ u [0 ], '\\' );
768
+ $ useMap [substr ($ u [0 ], false !== $ p ? 1 + $ p : 0 )] = $ u [0 ];
769
+ } else {
770
+ $ useMap [$ u [1 ]] = $ u [0 ];
771
+ }
772
+ }
773
+
774
+ break ;
775
+ }
776
+ }
777
+
778
+ return [$ namespace , $ useOffset , $ useMap ];
779
+ }
521
780
}
0 commit comments