1
+ <?php
2
+
3
+ declare (strict_types=1 );
4
+
5
+ // Written by SamMousa in https://github.com/vimeo/psalm/issues/8101, finalized by @danog
6
+
7
+ require 'vendor/autoload.php ' ;
8
+
9
+ use DG \BypassFinals ;
10
+ use Psalm \Internal \Analyzer \ProjectAnalyzer ;
11
+ use Psalm \Internal \Codebase \Reflection ;
12
+ use Psalm \Internal \Provider \FileProvider ;
13
+ use Psalm \Internal \Provider \Providers ;
14
+ use Psalm \Internal \Type \Comparator \UnionTypeComparator ;
15
+ use Psalm \Tests \TestConfig ;
16
+ use Psalm \Type ;
17
+
18
+ /**
19
+ * Returns the correct reflection type for function or method name.
20
+ */
21
+ function getReflectionFunction (string $ functionName ): ?ReflectionFunctionAbstract
22
+ {
23
+ try {
24
+ if (strpos ($ functionName , ':: ' ) !== false ) {
25
+ return new ReflectionMethod ($ functionName );
26
+ }
27
+
28
+ /** @var callable-string $functionName */
29
+ return new ReflectionFunction ($ functionName );
30
+ } catch (ReflectionException $ e ) {
31
+ return null ;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * @param array<string, string> $entryParameters
37
+ */
38
+ function assertEntryParameters (ReflectionFunctionAbstract $ function , array &$ entryParameters ): void
39
+ {
40
+ assertEntryReturnType ($ function , $ entryParameters [0 ]);
41
+ /**
42
+ * Parse the parameter names from the map.
43
+ *
44
+ * @var array<string, array{byRef: bool, refMode: 'rw'|'w'|'r', variadic: bool, optional: bool, type: string}>
45
+ */
46
+ $ normalizedEntries = [];
47
+
48
+ foreach ($ entryParameters as $ key => $ entry ) {
49
+ if ($ key === 0 ) {
50
+ continue ;
51
+ }
52
+ $ normalizedKey = $ key ;
53
+ /**
54
+ * @var array{byRef: bool, refMode: 'rw'|'w'|'r', variadic: bool, optional: bool, type: string} $normalizedEntry
55
+ */
56
+ $ normalizedEntry = [
57
+ 'variadic ' => false ,
58
+ 'byRef ' => false ,
59
+ 'optional ' => false ,
60
+ 'type ' => $ entry ,
61
+ ];
62
+ if (strncmp ($ normalizedKey , '& ' , 1 ) === 0 ) {
63
+ $ normalizedEntry ['byRef ' ] = true ;
64
+ $ normalizedKey = substr ($ normalizedKey , 1 );
65
+ }
66
+
67
+ if (strncmp ($ normalizedKey , '... ' , 3 ) === 0 ) {
68
+ $ normalizedEntry ['variadic ' ] = true ;
69
+ $ normalizedKey = substr ($ normalizedKey , 3 );
70
+ }
71
+
72
+ // Read the reference mode
73
+ if ($ normalizedEntry ['byRef ' ]) {
74
+ $ parts = explode ('_ ' , $ normalizedKey , 2 );
75
+ if (count ($ parts ) === 2 ) {
76
+ if (!($ parts [0 ] === 'rw ' || $ parts [0 ] === 'w ' || $ parts [0 ] === 'r ' )) {
77
+ throw new InvalidArgumentException ('Invalid refMode: ' .$ parts [0 ]);
78
+ }
79
+ $ normalizedEntry ['refMode ' ] = $ parts [0 ];
80
+ $ normalizedKey = $ parts [1 ];
81
+ } else {
82
+ $ normalizedEntry ['refMode ' ] = 'rw ' ;
83
+ }
84
+ }
85
+
86
+ // Strip prefixes.
87
+ if (substr ($ normalizedKey , -1 , 1 ) === "= " ) {
88
+ $ normalizedEntry ['optional ' ] = true ;
89
+ $ normalizedKey = substr ($ normalizedKey , 0 , -1 );
90
+ }
91
+
92
+ $ normalizedEntry ['name ' ] = $ normalizedKey ;
93
+ $ normalizedEntries [$ normalizedKey ] = $ normalizedEntry ;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * @param array{byRef: bool, name?: string, refMode: 'rw'|'w'|'r', variadic: bool, optional: bool, type: string} $normalizedEntry
99
+ */
100
+ function assertParameter (array &$ normalizedEntry , ReflectionParameter $ param ): void
101
+ {
102
+ $ name = $ param ->getName ();
103
+
104
+ $ expectedType = $ param ->getType ();
105
+
106
+ if (isset ($ expectedType ) && !empty ($ normalizedEntry ['type ' ])) {
107
+ assertTypeValidity ($ expectedType , $ normalizedEntry ['type ' ], "Param ' {$ name }' " );
108
+ }
109
+ }
110
+
111
+ function assertEntryReturnType (ReflectionFunctionAbstract $ function , string &$ entryReturnType ): void
112
+ {
113
+ if (version_compare (PHP_VERSION , '8.1.0 ' , '>= ' )) {
114
+ $ expectedType = $ function ->hasTentativeReturnType () ? $ function ->getTentativeReturnType () : $ function ->getReturnType ();
115
+ } else {
116
+ $ expectedType = $ function ->getReturnType ();
117
+ }
118
+
119
+ if ($ expectedType !== null ) {
120
+ assertTypeValidity ($ expectedType , $ entryReturnType , 'Return ' );
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Since string equality is too strict, we do some extra checking here
126
+ */
127
+ function assertTypeValidity (ReflectionType $ reflected , string &$ specified , string $ msgPrefix ): void
128
+ {
129
+ $ expectedType = Reflection::getPsalmTypeFromReflectionType ($ reflected );
130
+ $ callMapType = Type::parseString ($ specified === '' ? 'mixed ' : $ specified );
131
+
132
+ $ codebase = ProjectAnalyzer::getInstance ()->getCodebase ();
133
+ try {
134
+ if (!UnionTypeComparator::isContainedBy ($ codebase , $ callMapType , $ expectedType , false , false , null , false , false ) && !str_contains ($ specified , 'static ' )) {
135
+ $ specified = $ expectedType ->getId (true );
136
+ }
137
+ } catch (Throwable ) {
138
+ }
139
+
140
+ // Reflection::getPsalmTypeFromReflectionType adds |null to mixed types so skip comparison
141
+ /*if (!$expectedType->hasMixed()) {
142
+ $this->assertSame($expectedType->isNullable(), $callMapType->isNullable(), "{$msgPrefix} type '{$specified}' missing null from reflected type '{$reflected}'");
143
+ //$this->assertSame($expectedType->hasBool(), $callMapType->hasBool(), "{$msgPrefix} type '{$specified}' missing bool from reflected type '{$reflected}'");
144
+ $this->assertSame($expectedType->hasArray(), $callMapType->hasArray(), "{$msgPrefix} type '{$specified}' missing array from reflected type '{$reflected}'");
145
+ $this->assertSame($expectedType->hasInt(), $callMapType->hasInt(), "{$msgPrefix} type '{$specified}' missing int from reflected type '{$reflected}'");
146
+ $this->assertSame($expectedType->hasFloat(), $callMapType->hasFloat(), "{$msgPrefix} type '{$specified}' missing float from reflected type '{$reflected}'");
147
+ }*/
148
+ }
149
+
150
+ BypassFinals::enable ();
151
+
152
+ function writeCallMap (string $ file , array $ callMap ) {
153
+ file_put_contents ($ file , '<?php // phpcs:ignoreFile
154
+ namespace Phan\Language\Internal;
155
+
156
+ return ' .var_export ($ callMap , true ).'; ' );
157
+ }
158
+
159
+ new ProjectAnalyzer (new TestConfig , new Providers (new FileProvider ));
160
+ $ callMap = require "dictionaries/CallMap.php " ;
161
+ $ orig = $ callMap ;
162
+
163
+ $ codebase = ProjectAnalyzer::getInstance ()->getCodebase ();
164
+
165
+ foreach ($ callMap as $ functionName => &$ entry ) {
166
+ $ refl = getReflectionFunction ($ functionName );
167
+ if (!$ refl ) {
168
+ continue ;
169
+ }
170
+ assertEntryParameters ($ refl , $ entry );
171
+ } unset($ entry );
172
+
173
+ writeCallMap ("dictionaries/CallMap.php " , $ callMap );
174
+
175
+ $ diffFile = "dictionaries/CallMap_84_delta.php " ;
176
+
177
+ $ diff = require $ diffFile ;
178
+
179
+ foreach ($ callMap as $ functionName => $ entry ) {
180
+ if ($ orig [$ functionName ] !== $ entry ) {
181
+ $ diff ['changed ' ][$ functionName ]['old ' ] = $ orig [$ functionName ];
182
+ $ diff ['changed ' ][$ functionName ]['new ' ] = $ entry ;
183
+ }
184
+ }
185
+
186
+ writeCallMap ($ diffFile , $ diff );
0 commit comments