@@ -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
@@ -348,7 +427,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
348
427
if (trait_exists ($ class )) {
349
428
$ file = $ refl ->getFileName ();
350
429
351
- foreach ($ refl ->getMethods (\ReflectionMethod:: IS_PUBLIC | \ReflectionMethod:: IS_PROTECTED ) as $ method ) {
430
+ foreach ($ refl ->getMethods () as $ method ) {
352
431
if ($ method ->getFileName () === $ file ) {
353
432
self ::$ methodTraits [$ file ][$ method ->getStartLine ()] = $ class ;
354
433
}
@@ -368,9 +447,17 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
368
447
self ::$ {$ property }[$ class ] = self ::$ {$ property }[$ class ] ? self ::$ {$ property }[$ use ] + self ::$ {$ property }[$ class ] : self ::$ {$ property }[$ use ];
369
448
}
370
449
}
450
+
451
+ if (null !== (self ::INTERNAL_TYPES [$ use ] ?? null )) {
452
+ foreach (self ::INTERNAL_TYPES [$ use ] as $ method => $ returnType ) {
453
+ if ('void ' !== $ returnType ) {
454
+ self ::$ returnTypes [$ class ] += [$ method => [$ returnType , $ returnType , $ class , '' ]];
455
+ }
456
+ }
457
+ }
371
458
}
372
459
373
- foreach ($ refl ->getMethods (\ReflectionMethod:: IS_PUBLIC | \ReflectionMethod:: IS_PROTECTED ) as $ method ) {
460
+ foreach ($ refl ->getMethods () as $ method ) {
374
461
if ($ method ->class !== $ class ) {
375
462
continue ;
376
463
}
@@ -413,34 +500,71 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
413
500
}
414
501
}
415
502
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 ];
503
+ $ forcePatchTypes = $ this ->patchTypes ['force ' ] ?? null ;
504
+
505
+ if ($ canAddReturnType = null !== $ forcePatchTypes && false === strpos ($ method ->getFileName (), \DIRECTORY_SEPARATOR .'vendor ' .\DIRECTORY_SEPARATOR )) {
506
+ if ('void ' !== (self ::MAGIC_METHODS [$ method ->name ] ?? 'void ' )) {
507
+ $ this ->patchTypes ['force ' ] = $ forcePatchTypes ?: 'docblock ' ;
508
+ }
509
+
510
+ $ canAddReturnType = ($ this ->patchTypes ['force ' ] ?? false )
511
+ || false !== strpos ($ refl ->getFileName (), \DIRECTORY_SEPARATOR .'Tests ' .\DIRECTORY_SEPARATOR )
512
+ || $ refl ->isFinal ()
513
+ || $ method ->isFinal ()
514
+ || $ method ->isPrivate ()
515
+ || ('' === (self ::$ internal [$ class ] ?? null ) && !$ refl ->isAbstract ())
516
+ || '' === (self ::$ final [$ class ] ?? null )
517
+ || preg_match ('/@(final|internal)$/m ' , $ doc )
518
+ ;
519
+ }
520
+
521
+ if (null !== ($ returnType = self ::$ returnTypes [$ class ][$ method ->name ] ?? self ::MAGIC_METHODS [$ method ->name ] ?? null ) && !$ method ->hasReturnType () && !($ doc && preg_match ('/\n\s+\* @return +(\S+)/ ' , $ doc ))) {
522
+ if ('void ' === $ returnType ) {
523
+ $ canAddReturnType = false ;
524
+ }
525
+
526
+ list ($ normalizedType , $ returnType , $ declaringClass , $ declaringFile ) = \is_string ($ returnType ) ? [$ returnType , $ returnType , '' , '' ] : $ returnType ;
418
527
419
- if (null !== $ this -> compatPatch && 0 === strpos ( $ class , $ this ->compatPatch )) {
420
- self :: fixReturnStatements ($ method , $ normalizedType );
528
+ if ($ canAddReturnType && ' docblock ' !== ( $ this ->patchTypes [ ' force ' ] ?? false )) {
529
+ $ this -> patchMethod ($ method, $ returnType , $ declaringFile , $ normalizedType );
421
530
}
422
531
423
532
if (strncmp ($ ns , $ declaringClass , $ len )) {
424
- if (null !== $ this ->compatPatch && 0 === strpos ($ class , $ this ->compatPatch )) {
425
- self ::patchMethod ($ method , $ returnType , $ declaringFile );
533
+ if ('docblock ' === ($ this ->patchTypes ['force ' ] ?? false ) && false === strpos ($ method ->getFileName (), \DIRECTORY_SEPARATOR .'vendor ' .\DIRECTORY_SEPARATOR )) {
534
+ $ this ->patchMethod ($ method , $ returnType , $ declaringFile , $ normalizedType );
535
+ } elseif ('' !== $ declaringClass ) {
536
+ $ 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
537
}
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
538
}
430
539
}
431
540
432
541
if (!$ doc ) {
542
+ $ this ->patchTypes ['force ' ] = $ forcePatchTypes ;
543
+
433
544
continue ;
434
545
}
435
546
436
- if (!$ method ->hasReturnType () && false !== strpos ($ doc , '@return ' ) && preg_match ('/\n\s+\* @return +(\S+)/ ' , $ doc , $ matches )) {
547
+ $ matches = [];
548
+
549
+ if (!$ method ->hasReturnType () && ((false !== strpos ($ doc , '@return ' ) && preg_match ('/\n\s+\* @return +(\S+)/ ' , $ doc , $ matches )) || 'void ' !== (self ::MAGIC_METHODS [$ method ->name ] ?? 'void ' ))) {
550
+ $ matches = $ matches ?: [1 => self ::MAGIC_METHODS [$ method ->name ]];
437
551
$ this ->setReturnType ($ matches [1 ], $ method , $ parent );
438
552
439
- if (null !== $ this ->compatPatch && 0 === strpos ($ class , $ this ->compatPatch )) {
440
- self ::fixReturnStatements ($ method , self ::$ returnTypes [$ class ][$ method ->name ][0 ] ?? '? ' );
553
+ if (isset (self ::$ returnTypes [$ class ][$ method ->name ][0 ]) && $ canAddReturnType ) {
554
+ $ this ->fixReturnStatements ($ method , self ::$ returnTypes [$ class ][$ method ->name ][0 ]);
555
+ }
556
+
557
+ if ($ method ->isPrivate ()) {
558
+ unset(self ::$ returnTypes [$ class ][$ method ->name ]);
441
559
}
442
560
}
443
561
562
+ $ this ->patchTypes ['force ' ] = $ forcePatchTypes ;
563
+
564
+ if ($ method ->isPrivate ()) {
565
+ continue ;
566
+ }
567
+
444
568
$ finalOrInternal = false ;
445
569
446
570
foreach (['final ' , 'internal ' ] as $ annotation ) {
@@ -630,7 +754,7 @@ private function setReturnType(string $types, \ReflectionMethod $method, ?string
630
754
} elseif ('null ' === $ normalizedType ) {
631
755
$ normalizedType = $ t ;
632
756
$ returnType = $ t ;
633
- } elseif ($ n !== $ normalizedType ) {
757
+ } 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
758
// ignore multi-types return declarations
635
759
return ;
636
760
}
@@ -680,7 +804,7 @@ private function normalizeType(string $type, string $class, ?string $parent): st
680
804
/**
681
805
* Utility method to add @return annotations to the Symfony code-base where it triggers a self-deprecations.
682
806
*/
683
- private static function patchMethod (\ReflectionMethod $ method , string $ returnType , string $ declaringFile )
807
+ private function patchMethod (\ReflectionMethod $ method , string $ returnType , string $ declaringFile, string $ normalizedType )
684
808
{
685
809
static $ patchedMethods = [];
686
810
static $ useStatements = [];
@@ -690,8 +814,10 @@ private static function patchMethod(\ReflectionMethod $method, string $returnTyp
690
814
}
691
815
692
816
$ patchedMethods [$ file ][$ startLine ] = true ;
693
- $ patchedMethods [$ file ][0 ] = $ patchedMethods [$ file ][0 ] ?? 0 ;
694
- $ startLine += $ patchedMethods [$ file ][0 ] - 2 ;
817
+ $ fileOffset = self ::$ fileOffsets [$ file ] ?? 0 ;
818
+ $ startLine += $ fileOffset - 2 ;
819
+ $ nullable = '? ' === $ normalizedType [0 ] ? '? ' : '' ;
820
+ $ normalizedType = ltrim ($ normalizedType , '? ' );
695
821
$ returnType = explode ('| ' , $ returnType );
696
822
$ code = file ($ file );
697
823
@@ -737,32 +863,42 @@ private static function patchMethod(\ReflectionMethod $method, string $returnTyp
737
863
if (!isset ($ useMap [$ alias ])) {
738
864
$ useStatements [$ file ][2 ][$ alias ] = $ type ;
739
865
$ code [$ useOffset ] = "use $ type; \n" .$ code [$ useOffset ];
740
- ++$ patchedMethods [ $ file ][ 0 ] ;
866
+ ++$ fileOffset ;
741
867
} elseif ($ useMap [$ alias ] !== $ type ) {
742
868
$ alias .= 'FIXME ' ;
743
869
$ useStatements [$ file ][2 ][$ alias ] = $ type ;
744
870
$ code [$ useOffset ] = "use $ type as $ alias; \n" .$ code [$ useOffset ];
745
- ++$ patchedMethods [ $ file ][ 0 ] ;
871
+ ++$ fileOffset ;
746
872
}
747
873
748
874
$ returnType [$ i ] = null !== $ format ? sprintf ($ format , $ alias ) : $ alias ;
875
+
876
+ if (!isset (self ::SPECIAL_RETURN_TYPES [$ normalizedType ]) && !isset (self ::SPECIAL_RETURN_TYPES [$ returnType [$ i ]])) {
877
+ $ normalizedType = $ returnType [$ i ];
878
+ }
749
879
}
750
880
751
- $ returnType = implode ('| ' , $ returnType );
881
+ if ('docblock ' === ($ this ->patchTypes ['force ' ] ?? null ) || ('object ' === $ normalizedType && ($ this ->patchTypes ['php71-compat ' ] ?? false ))) {
882
+ $ returnType = implode ('| ' , $ returnType );
752
883
753
- if ($ method ->getDocComment ()) {
754
- $ code [$ startLine ] = " * @return $ returnType \n" .$ code [$ startLine ];
755
- } else {
756
- $ code [$ startLine ] .= <<<EOTXT
884
+ if ($ method ->getDocComment ()) {
885
+ $ code [$ startLine ] = " * @return $ returnType \n" .$ code [$ startLine ];
886
+ } else {
887
+ $ code [$ startLine ] .= <<<EOTXT
757
888
/**
758
889
* @return $ returnType
759
890
*/
760
891
761
892
EOTXT ;
893
+ }
894
+
895
+ $ fileOffset += substr_count ($ code [$ startLine ], "\n" ) - 1 ;
762
896
}
763
897
764
- $ patchedMethods [$ file ][ 0 ] += substr_count ( $ code [ $ startLine ], "\n" ) - 1 ;
898
+ self :: $ fileOffsets [$ file ] = $ fileOffset ;
765
899
file_put_contents ($ file , $ code );
900
+
901
+ $ this ->fixReturnStatements ($ method , $ nullable .$ normalizedType );
766
902
}
767
903
768
904
private static function getUseStatements (string $ file ): array
@@ -808,15 +944,25 @@ private static function getUseStatements(string $file): array
808
944
return [$ namespace , $ useOffset , $ useMap ];
809
945
}
810
946
811
- private static function fixReturnStatements (\ReflectionMethod $ method , string $ returnType )
947
+ private function fixReturnStatements (\ReflectionMethod $ method , string $ returnType )
812
948
{
949
+ if (($ this ->patchTypes ['php71-compat ' ] ?? false ) && 'object ' === ltrim ($ returnType , '? ' ) && 'docblock ' !== ($ this ->patchTypes ['force ' ] ?? null )) {
950
+ return ;
951
+ }
952
+
813
953
if (!file_exists ($ file = $ method ->getFileName ())) {
814
954
return ;
815
955
}
816
956
817
957
$ fixedCode = $ code = file ($ file );
818
- $ end = $ method ->getEndLine ();
819
- for ($ i = $ method ->getStartLine (); $ i < $ end ; ++$ i ) {
958
+ $ i = (self ::$ fileOffsets [$ file ] ?? 0 ) + $ method ->getStartLine ();
959
+
960
+ if ('? ' !== $ returnType && 'docblock ' !== ($ this ->patchTypes ['force ' ] ?? null )) {
961
+ $ fixedCode [$ i - 1 ] = preg_replace ('/\)(;?\n)/ ' , "): $ returnType \\1 " , $ code [$ i - 1 ]);
962
+ }
963
+
964
+ $ end = $ method ->isGenerator () ? $ i : $ method ->getEndLine ();
965
+ for (; $ i < $ end ; ++$ i ) {
820
966
if ('void ' === $ returnType ) {
821
967
$ fixedCode [$ i ] = str_replace (' return null; ' , ' return; ' , $ code [$ i ]);
822
968
} elseif ('mixed ' === $ returnType || '? ' === $ returnType [0 ]) {
0 commit comments