@@ -42,7 +42,7 @@ class DebugClassLoader
42
42
'bool ' => 'bool ' ,
43
43
'callable ' => 'callable ' ,
44
44
'float ' => 'float ' ,
45
- 'int ' => 'integer ' ,
45
+ 'int ' => 'int ' ,
46
46
'iterable ' => 'iterable ' ,
47
47
'object ' => 'object ' ,
48
48
'string ' => 'string ' ,
@@ -64,10 +64,79 @@ class DebugClassLoader
64
64
'parent ' => true ,
65
65
];
66
66
67
+ private const MAGIC_METHODS = [
68
+ '__set ' => 'void ' ,
69
+ '__isset ' => 'bool ' ,
70
+ '__unset ' => 'void ' ,
71
+ '__sleep ' => 'array ' ,
72
+ '__wakeup ' => 'void ' ,
73
+ '__toString ' => 'string ' ,
74
+ '__clone ' => 'void ' ,
75
+ '__debugInfo ' => 'array ' ,
76
+ '__serialize ' => 'array ' ,
77
+ '__unserialize ' => 'void ' ,
78
+ ];
79
+
80
+ private const INTERNAL_TYPES = [
81
+ 'ArrayAccess ' => [
82
+ 'offsetExists ' => 'bool ' ,
83
+ 'offsetSet ' => 'void ' ,
84
+ 'offsetUnset ' => 'void ' ,
85
+ ],
86
+ 'Countable ' => [
87
+ 'count ' => 'int ' ,
88
+ ],
89
+ 'Iterator ' => [
90
+ 'next ' => 'void ' ,
91
+ 'valid ' => 'bool ' ,
92
+ 'rewind ' => 'void ' ,
93
+ ],
94
+ 'IteratorAggregate ' => [
95
+ 'getIterator ' => '\Traversable ' ,
96
+ ],
97
+ 'OuterIterator ' => [
98
+ 'getInnerIterator ' => '\Iterator ' ,
99
+ ],
100
+ 'RecursiveIterator ' => [
101
+ 'hasChildren ' => 'bool ' ,
102
+ ],
103
+ 'SeekableIterator ' => [
104
+ 'seek ' => 'void ' ,
105
+ ],
106
+ 'Serializable ' => [
107
+ 'serialize ' => 'string ' ,
108
+ 'unserialize ' => 'void ' ,
109
+ ],
110
+ 'SessionHandlerInterface ' => [
111
+ 'open ' => 'bool ' ,
112
+ 'close ' => 'bool ' ,
113
+ 'read ' => 'string ' ,
114
+ 'write ' => 'bool ' ,
115
+ 'destroy ' => 'bool ' ,
116
+ 'gc ' => 'bool ' ,
117
+ ],
118
+ 'SessionIdInterface ' => [
119
+ 'create_sid ' => 'string ' ,
120
+ ],
121
+ 'SessionUpdateTimestampHandlerInterface ' => [
122
+ 'validateId ' => 'bool ' ,
123
+ 'updateTimestamp ' => 'bool ' ,
124
+ ],
125
+ 'Throwable ' => [
126
+ 'getMessage ' => 'string ' ,
127
+ 'getCode ' => 'int ' ,
128
+ 'getFile ' => 'string ' ,
129
+ 'getLine ' => 'int ' ,
130
+ 'getTrace ' => 'array ' ,
131
+ 'getPrevious ' => '?\Throwable ' ,
132
+ 'getTraceAsString ' => 'string ' ,
133
+ ],
134
+ ];
135
+
67
136
private $ classLoader ;
68
137
private $ isFinder ;
69
138
private $ loaded = [];
70
- private $ compatPatch ;
139
+ private $ patchTypes ;
71
140
private static $ caseCheck ;
72
141
private static $ checkedClasses = [];
73
142
private static $ final = [];
@@ -80,12 +149,13 @@ class DebugClassLoader
80
149
private static $ method = [];
81
150
private static $ returnTypes = [];
82
151
private static $ methodTraits = [];
152
+ private static $ fileOffsets = [];
83
153
84
154
public function __construct (callable $ classLoader )
85
155
{
86
156
$ this ->classLoader = $ classLoader ;
87
157
$ this ->isFinder = \is_array ($ classLoader ) && method_exists ($ classLoader [0 ], 'findFile ' );
88
- $ this -> compatPatch = getenv ('SYMFONY_PATCH_TYPE_DECLARATIONS_COMPAT ' ) ?: null ;
158
+ parse_str ( getenv ('SYMFONY_PATCH_TYPE_DECLARATIONS ' ) ?: '' , $ this -> patchTypes ) ;
89
159
90
160
if (!isset (self ::$ caseCheck )) {
91
161
$ file = file_exists (__FILE__ ) ? __FILE__ : rtrim (realpath ('. ' ), \DIRECTORY_SEPARATOR );
@@ -160,13 +230,22 @@ public static function disable(): void
160
230
spl_autoload_unregister ($ function );
161
231
}
162
232
233
+ $ loader = null ;
234
+
163
235
foreach ($ functions as $ function ) {
164
236
if (\is_array ($ function ) && $ function [0 ] instanceof self) {
237
+ $ loader = $ function [0 ];
165
238
$ function = $ function [0 ]->getClassLoader ();
166
239
}
167
240
168
241
spl_autoload_register ($ function );
169
242
}
243
+
244
+ if (null !== $ loader ) {
245
+ foreach (array_merge (get_declared_interfaces (), get_declared_traits (), get_declared_classes ()) as $ class ) {
246
+ $ loader ->checkClass ($ class );
247
+ }
248
+ }
170
249
}
171
250
172
251
public function findFile (string $ class ): ?string
@@ -256,6 +335,9 @@ private function checkClass(string $class, string $file = null): void
256
335
257
336
public function checkAnnotations (\ReflectionClass $ refl , string $ class ): array
258
337
{
338
+ if ('Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV7 ' === $ class ) {
339
+ return [];
340
+ }
259
341
$ deprecations = [];
260
342
261
343
// Don't trigger deprecations for classes in the same vendor
@@ -348,7 +430,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
348
430
if (trait_exists ($ class )) {
349
431
$ file = $ refl ->getFileName ();
350
432
351
- foreach ($ refl ->getMethods (\ReflectionMethod:: IS_PUBLIC | \ReflectionMethod:: IS_PROTECTED ) as $ method ) {
433
+ foreach ($ refl ->getMethods () as $ method ) {
352
434
if ($ method ->getFileName () === $ file ) {
353
435
self ::$ methodTraits [$ file ][$ method ->getStartLine ()] = $ class ;
354
436
}
@@ -368,9 +450,17 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
368
450
self ::$ {$ property }[$ class ] = self ::$ {$ property }[$ class ] ? self ::$ {$ property }[$ use ] + self ::$ {$ property }[$ class ] : self ::$ {$ property }[$ use ];
369
451
}
370
452
}
453
+
454
+ if (null !== (self ::INTERNAL_TYPES [$ use ] ?? null )) {
455
+ foreach (self ::INTERNAL_TYPES [$ use ] as $ method => $ returnType ) {
456
+ if ('void ' !== $ returnType ) {
457
+ self ::$ returnTypes [$ class ] += [$ method => [$ returnType , $ returnType , $ class , '' ]];
458
+ }
459
+ }
460
+ }
371
461
}
372
462
373
- foreach ($ refl ->getMethods (\ReflectionMethod:: IS_PUBLIC | \ReflectionMethod:: IS_PROTECTED ) as $ method ) {
463
+ foreach ($ refl ->getMethods () as $ method ) {
374
464
if ($ method ->class !== $ class ) {
375
465
continue ;
376
466
}
@@ -413,34 +503,71 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
413
503
}
414
504
}
415
505
416
- if (isset (self ::$ returnTypes [$ class ][$ method ->name ]) && !$ method ->hasReturnType () && !($ doc && preg_match ('/\n\s+\* @return +(\S+)/ ' , $ doc ))) {
417
- list ($ normalizedType , $ returnType , $ declaringClass , $ declaringFile ) = self ::$ returnTypes [$ class ][$ method ->name ];
506
+ $ forcePatchTypes = $ this ->patchTypes ['force ' ] ?? null ;
507
+
508
+ if ($ canAddReturnType = null !== $ forcePatchTypes && false === strpos ($ method ->getFileName (), \DIRECTORY_SEPARATOR .'vendor ' .\DIRECTORY_SEPARATOR )) {
509
+ if ('void ' !== (self ::MAGIC_METHODS [$ method ->name ] ?? 'void ' )) {
510
+ $ this ->patchTypes ['force ' ] = $ forcePatchTypes ?: 'docblock ' ;
511
+ }
512
+
513
+ $ canAddReturnType = ($ this ->patchTypes ['force ' ] ?? false )
514
+ || false !== strpos ($ refl ->getFileName (), \DIRECTORY_SEPARATOR .'Tests ' .\DIRECTORY_SEPARATOR )
515
+ || $ refl ->isFinal ()
516
+ || $ method ->isFinal ()
517
+ || $ method ->isPrivate ()
518
+ || ('' === (self ::$ internal [$ class ] ?? null ) && !$ refl ->isAbstract ())
519
+ || '' === (self ::$ final [$ class ] ?? null )
520
+ || preg_match ('/@(final|internal)$/m ' , $ doc )
521
+ ;
522
+ }
418
523
419
- if (null !== $ this ->compatPatch && 0 === strpos ($ class , $ this ->compatPatch )) {
420
- self ::fixReturnStatements ($ method , $ normalizedType );
524
+ if (null !== ($ returnType = self ::$ returnTypes [$ class ][$ method ->name ] ?? self ::MAGIC_METHODS [$ method ->name ] ?? null ) && !$ method ->hasReturnType () && !($ doc && preg_match ('/\n\s+\* @return +(\S+)/ ' , $ doc ))) {
525
+ if ('void ' === $ returnType ) {
526
+ $ canAddReturnType = false ;
527
+ }
528
+
529
+ list ($ normalizedType , $ returnType , $ declaringClass , $ declaringFile ) = \is_string ($ returnType ) ? [$ returnType , $ returnType , '' , '' ] : $ returnType ;
530
+
531
+ if ($ canAddReturnType && 'docblock ' !== ($ this ->patchTypes ['force ' ] ?? false )) {
532
+ $ this ->patchMethod ($ method , $ returnType , $ declaringFile , $ normalizedType );
421
533
}
422
534
423
535
if (strncmp ($ ns , $ declaringClass , $ len )) {
424
- if (null !== $ this ->compatPatch && 0 === strpos ($ class , $ this ->compatPatch )) {
425
- self ::patchMethod ($ method , $ returnType , $ declaringFile );
536
+ if ($ canAddReturnType && 'docblock ' === ($ this ->patchTypes ['force ' ] ?? false ) && false === strpos ($ method ->getFileName (), \DIRECTORY_SEPARATOR .'vendor ' .\DIRECTORY_SEPARATOR )) {
537
+ $ this ->patchMethod ($ method , $ returnType , $ declaringFile , $ normalizedType );
538
+ } elseif ('' !== $ declaringClass ) {
539
+ $ 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 , $ normalizedType , $ class );
426
540
}
427
-
428
- $ 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 , $ normalizedType , $ class );
429
541
}
430
542
}
431
543
432
544
if (!$ doc ) {
545
+ $ this ->patchTypes ['force ' ] = $ forcePatchTypes ;
546
+
433
547
continue ;
434
548
}
435
549
436
- if (!$ method ->hasReturnType () && false !== strpos ($ doc , '@return ' ) && preg_match ('/\n\s+\* @return +(\S+)/ ' , $ doc , $ matches )) {
550
+ $ matches = [];
551
+
552
+ if (!$ method ->hasReturnType () && ((false !== strpos ($ doc , '@return ' ) && preg_match ('/\n\s+\* @return +(\S+)/ ' , $ doc , $ matches )) || 'void ' !== (self ::MAGIC_METHODS [$ method ->name ] ?? 'void ' ))) {
553
+ $ matches = $ matches ?: [1 => self ::MAGIC_METHODS [$ method ->name ]];
437
554
$ this ->setReturnType ($ matches [1 ], $ method , $ parent );
438
555
439
- if (null !== $ this ->compatPatch && 0 === strpos ($ class , $ this ->compatPatch )) {
440
- self ::fixReturnStatements ($ method , self ::$ returnTypes [$ class ][$ method ->name ][0 ] ?? '? ' );
556
+ if (isset (self ::$ returnTypes [$ class ][$ method ->name ][0 ]) && $ canAddReturnType ) {
557
+ $ this ->fixReturnStatements ($ method , self ::$ returnTypes [$ class ][$ method ->name ][0 ]);
558
+ }
559
+
560
+ if ($ method ->isPrivate ()) {
561
+ unset(self ::$ returnTypes [$ class ][$ method ->name ]);
441
562
}
442
563
}
443
564
565
+ $ this ->patchTypes ['force ' ] = $ forcePatchTypes ;
566
+
567
+ if ($ method ->isPrivate ()) {
568
+ continue ;
569
+ }
570
+
444
571
$ finalOrInternal = false ;
445
572
446
573
foreach (['final ' , 'internal ' ] as $ annotation ) {
@@ -609,9 +736,13 @@ private function setReturnType(string $types, \ReflectionMethod $method, ?string
609
736
$ typesMap [$ this ->normalizeType ($ t , $ method ->class , $ parent )] = $ t ;
610
737
}
611
738
612
- if (isset ($ typesMap ['array ' ]) && (isset ($ typesMap ['Traversable ' ]) || isset ($ typesMap ['\Traversable ' ]))) {
613
- $ typesMap ['iterable ' ] = 'array ' !== $ typesMap ['array ' ] ? $ typesMap ['array ' ] : 'iterable ' ;
614
- unset($ typesMap ['array ' ], $ typesMap ['Traversable ' ], $ typesMap ['\Traversable ' ]);
739
+ if (isset ($ typesMap ['array ' ])) {
740
+ if (isset ($ typesMap ['Traversable ' ]) || isset ($ typesMap ['\Traversable ' ])) {
741
+ $ typesMap ['iterable ' ] = 'array ' !== $ typesMap ['array ' ] ? $ typesMap ['array ' ] : 'iterable ' ;
742
+ unset($ typesMap ['array ' ], $ typesMap ['Traversable ' ], $ typesMap ['\Traversable ' ]);
743
+ } elseif ('array ' !== $ typesMap ['array ' ] && isset (self ::$ returnTypes [$ method ->class ][$ method ->name ])) {
744
+ return ;
745
+ }
615
746
}
616
747
617
748
if (isset ($ typesMap ['array ' ]) && isset ($ typesMap ['iterable ' ])) {
@@ -630,7 +761,7 @@ private function setReturnType(string $types, \ReflectionMethod $method, ?string
630
761
} elseif ('null ' === $ normalizedType ) {
631
762
$ normalizedType = $ t ;
632
763
$ returnType = $ t ;
633
- } elseif ($ n !== $ normalizedType ) {
764
+ } elseif ($ n !== $ normalizedType || ! preg_match ( ' /^ \\\\ ?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?: \\\\ [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/ ' , $ n ) ) {
634
765
// ignore multi-types return declarations
635
766
return ;
636
767
}
@@ -680,7 +811,7 @@ private function normalizeType(string $type, string $class, ?string $parent): st
680
811
/**
681
812
* Utility method to add @return annotations to the Symfony code-base where it triggers a self-deprecations.
682
813
*/
683
- private static function patchMethod (\ReflectionMethod $ method , string $ returnType , string $ declaringFile )
814
+ private function patchMethod (\ReflectionMethod $ method , string $ returnType , string $ declaringFile, string $ normalizedType )
684
815
{
685
816
static $ patchedMethods = [];
686
817
static $ useStatements = [];
@@ -690,8 +821,10 @@ private static function patchMethod(\ReflectionMethod $method, string $returnTyp
690
821
}
691
822
692
823
$ patchedMethods [$ file ][$ startLine ] = true ;
693
- $ patchedMethods [$ file ][0 ] = $ patchedMethods [$ file ][0 ] ?? 0 ;
694
- $ startLine += $ patchedMethods [$ file ][0 ] - 2 ;
824
+ $ fileOffset = self ::$ fileOffsets [$ file ] ?? 0 ;
825
+ $ startLine += $ fileOffset - 2 ;
826
+ $ nullable = '? ' === $ normalizedType [0 ] ? '? ' : '' ;
827
+ $ normalizedType = ltrim ($ normalizedType , '? ' );
695
828
$ returnType = explode ('| ' , $ returnType );
696
829
$ code = file ($ file );
697
830
@@ -737,32 +870,42 @@ private static function patchMethod(\ReflectionMethod $method, string $returnTyp
737
870
if (!isset ($ useMap [$ alias ])) {
738
871
$ useStatements [$ file ][2 ][$ alias ] = $ type ;
739
872
$ code [$ useOffset ] = "use $ type; \n" .$ code [$ useOffset ];
740
- ++$ patchedMethods [ $ file ][ 0 ] ;
873
+ ++$ fileOffset ;
741
874
} elseif ($ useMap [$ alias ] !== $ type ) {
742
875
$ alias .= 'FIXME ' ;
743
876
$ useStatements [$ file ][2 ][$ alias ] = $ type ;
744
877
$ code [$ useOffset ] = "use $ type as $ alias; \n" .$ code [$ useOffset ];
745
- ++$ patchedMethods [ $ file ][ 0 ] ;
878
+ ++$ fileOffset ;
746
879
}
747
880
748
881
$ returnType [$ i ] = null !== $ format ? sprintf ($ format , $ alias ) : $ alias ;
882
+
883
+ if (!isset (self ::SPECIAL_RETURN_TYPES [$ normalizedType ]) && !isset (self ::SPECIAL_RETURN_TYPES [$ returnType [$ i ]])) {
884
+ $ normalizedType = $ returnType [$ i ];
885
+ }
749
886
}
750
887
751
- $ returnType = implode ('| ' , $ returnType );
888
+ if ('docblock ' === ($ this ->patchTypes ['force ' ] ?? null ) || ('object ' === $ normalizedType && ($ this ->patchTypes ['php71-compat ' ] ?? false ))) {
889
+ $ returnType = implode ('| ' , $ returnType );
752
890
753
- if ($ method ->getDocComment ()) {
754
- $ code [$ startLine ] = " * @return $ returnType \n" .$ code [$ startLine ];
755
- } else {
756
- $ code [$ startLine ] .= <<<EOTXT
891
+ if ($ method ->getDocComment ()) {
892
+ $ code [$ startLine ] = " * @return $ returnType \n" .$ code [$ startLine ];
893
+ } else {
894
+ $ code [$ startLine ] .= <<<EOTXT
757
895
/**
758
896
* @return $ returnType
759
897
*/
760
898
761
899
EOTXT ;
900
+ }
901
+
902
+ $ fileOffset += substr_count ($ code [$ startLine ], "\n" ) - 1 ;
762
903
}
763
904
764
- $ patchedMethods [$ file ][ 0 ] += substr_count ( $ code [ $ startLine ], "\n" ) - 1 ;
905
+ self :: $ fileOffsets [$ file ] = $ fileOffset ;
765
906
file_put_contents ($ file , $ code );
907
+
908
+ $ this ->fixReturnStatements ($ method , $ nullable .$ normalizedType );
766
909
}
767
910
768
911
private static function getUseStatements (string $ file ): array
@@ -808,15 +951,25 @@ private static function getUseStatements(string $file): array
808
951
return [$ namespace , $ useOffset , $ useMap ];
809
952
}
810
953
811
- private static function fixReturnStatements (\ReflectionMethod $ method , string $ returnType )
954
+ private function fixReturnStatements (\ReflectionMethod $ method , string $ returnType )
812
955
{
956
+ if (($ this ->patchTypes ['php71-compat ' ] ?? false ) && 'object ' === ltrim ($ returnType , '? ' ) && 'docblock ' !== ($ this ->patchTypes ['force ' ] ?? null )) {
957
+ return ;
958
+ }
959
+
813
960
if (!file_exists ($ file = $ method ->getFileName ())) {
814
961
return ;
815
962
}
816
963
817
964
$ fixedCode = $ code = file ($ file );
818
- $ end = $ method ->getEndLine ();
819
- for ($ i = $ method ->getStartLine (); $ i < $ end ; ++$ i ) {
965
+ $ i = (self ::$ fileOffsets [$ file ] ?? 0 ) + $ method ->getStartLine ();
966
+
967
+ if ('? ' !== $ returnType && 'docblock ' !== ($ this ->patchTypes ['force ' ] ?? null )) {
968
+ $ fixedCode [$ i - 1 ] = preg_replace ('/\)(;?\n)/ ' , "): $ returnType \\1 " , $ code [$ i - 1 ]);
969
+ }
970
+
971
+ $ end = $ method ->isGenerator () ? $ i : $ method ->getEndLine ();
972
+ for (; $ i < $ end ; ++$ i ) {
820
973
if ('void ' === $ returnType ) {
821
974
$ fixedCode [$ i ] = str_replace (' return null; ' , ' return; ' , $ code [$ i ]);
822
975
} elseif ('mixed ' === $ returnType || '? ' === $ returnType [0 ]) {
0 commit comments