InternalCallMapHandler   F
last analyzed

Complexity

Total Complexity 63

Size/Duplication

Total Lines 399
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
dl 0
loc 399
rs 3.36
c 0
b 0
f 0
wmc 63
lcom 1
cbo 9

5 Methods

Rating   Name   Duplication   Size   Complexity  
A getCallableFromCallMapById() 0 21 2
F getMatchingCallableFromCallMapOptions() 0 131 31
C getCallMap() 0 72 12
A inCallMap() 0 4 1
F getCallablesFromCallMap() 0 105 17

How to fix   Complexity   

Complex Class

Complex classes like InternalCallMapHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use InternalCallMapHandler, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Psalm\Internal\Codebase;
3
4
use function array_shift;
5
use function assert;
6
use function count;
7
use function file_exists;
8
use PhpParser;
9
use Psalm\Codebase;
10
use Psalm\Internal\Analyzer\ProjectAnalyzer;
11
use Psalm\Internal\Analyzer\TypeAnalyzer;
12
use Psalm\Storage\FunctionLikeParameter;
13
use Psalm\Type;
14
use Psalm\Type\Atomic\TCallable;
15
use function strtolower;
16
use function substr;
17
use function version_compare;
18
19
/**
20
 * @internal
21
 *
22
 * Gets values from the call map array, which stores data about native functions and methods
23
 */
24
class InternalCallMapHandler
25
{
26
    const PHP_MAJOR_VERSION = 7;
27
    const PHP_MINOR_VERSION = 4;
28
    const LOWEST_AVAILABLE_DELTA = 71;
29
30
    /**
31
     * @var ?int
32
     */
33
    private static $loaded_php_major_version = null;
34
    /**
35
     * @var ?int
36
     */
37
    private static $loaded_php_minor_version = null;
38
39
    /**
40
     * @var array<array<int|string,string>>|null
41
     */
42
    private static $call_map = null;
43
44
    /**
45
     * @var array<array<int, TCallable>>|null
46
     */
47
    private static $call_map_callables = [];
48
49
    /**
50
     * @var array<string, list<list<Type\TaintKind::*>>>
51
     */
52
    private static $taint_sink_map = [];
53
54
    /**
55
     * @param  string                           $method_id
56
     * @param  array<int, PhpParser\Node\Arg>   $args
57
     *
58
     * @return TCallable
59
     */
60
    public static function getCallableFromCallMapById(
61
        Codebase $codebase,
62
        $method_id,
63
        array $args,
64
        ?\Psalm\Internal\Provider\NodeDataProvider $nodes
65
    ) {
66
        $possible_callables = self::getCallablesFromCallMap($method_id);
67
68
        if ($possible_callables === null) {
69
            throw new \UnexpectedValueException(
70
                'Not expecting $function_param_options to be null for ' . $method_id
71
            );
72
        }
73
74
        return self::getMatchingCallableFromCallMapOptions(
75
            $codebase,
76
            $possible_callables,
77
            $args,
78
            $nodes
79
        );
80
    }
81
82
    /**
83
     * @param  array<int, TCallable>  $callables
84
     * @param  array<int, PhpParser\Node\Arg>                 $args
85
     *
86
     * @return TCallable
87
     */
88
    public static function getMatchingCallableFromCallMapOptions(
89
        Codebase $codebase,
90
        array $callables,
91
        array $args,
92
        ?\Psalm\NodeTypeProvider $nodes
93
    ) {
94
        if (count($callables) === 1) {
95
            return $callables[0];
96
        }
97
98
        $matching_param_count_callable = null;
99
        $matching_coerced_param_count_callable = null;
100
101
        foreach ($callables as $possible_callable) {
102
            $possible_function_params = $possible_callable->params;
103
104
            assert($possible_function_params !== null);
105
106
            $all_args_match = true;
107
            $type_coerced = false;
108
109
            $last_param = count($possible_function_params)
110
                ? $possible_function_params[count($possible_function_params) - 1]
111
                : null;
112
113
            $mandatory_param_count = count($possible_function_params);
114
115
            foreach ($possible_function_params as $i => $possible_function_param) {
116
                if ($possible_function_param->is_optional) {
117
                    $mandatory_param_count = $i;
118
                    break;
119
                }
120
            }
121
122
            if ($mandatory_param_count > count($args) && !($last_param && $last_param->is_variadic)) {
123
                continue;
124
            }
125
126
            foreach ($args as $argument_offset => $arg) {
127
                if ($argument_offset >= count($possible_function_params)) {
128
                    if (!$last_param || !$last_param->is_variadic) {
129
                        $all_args_match = false;
130
                        break;
131
                    }
132
133
                    $function_param = $last_param;
134
                } else {
135
                    $function_param = $possible_function_params[$argument_offset];
136
                }
137
138
                $param_type = $function_param->type;
139
140
                if (!$param_type) {
141
                    continue;
142
                }
143
144
                $arg_type = null;
145
146
                if (!$nodes
147
                    || !($arg_type = $nodes->getType($arg->value))
148
                ) {
149
                    continue;
150
                }
151
152
                if ($arg_type->hasMixed()) {
153
                    continue;
154
                }
155
156
                if ($arg->unpack && !$function_param->is_variadic) {
157
                    if ($arg_type->hasArray()) {
158
                        /**
159
                         * @psalm-suppress PossiblyUndefinedStringArrayOffset
160
                         * @var Type\Atomic\TArray|Type\Atomic\ObjectLike|Type\Atomic\TList
161
                         */
162
                        $array_atomic_type = $arg_type->getAtomicTypes()['array'];
163
164
                        if ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
165
                            $arg_type = $array_atomic_type->getGenericValueType();
166
                        } elseif ($array_atomic_type instanceof Type\Atomic\TList) {
167
                            $arg_type = $array_atomic_type->type_param;
168
                        } else {
169
                            $arg_type = $array_atomic_type->type_params[1];
170
                        }
171
                    }
172
                }
173
174
                $arg_result = new \Psalm\Internal\Analyzer\TypeComparisonResult();
175
176
                if (TypeAnalyzer::isContainedBy(
177
                    $codebase,
178
                    $arg_type,
179
                    $param_type,
180
                    true,
181
                    true,
182
                    $arg_result
183
                ) || $arg_result->type_coerced) {
184
                    if ($arg_result->type_coerced) {
185
                        $type_coerced = true;
186
                    }
187
188
                    continue;
189
                }
190
191
                $all_args_match = false;
192
                break;
193
            }
194
195
            if (count($args) === count($possible_function_params)) {
196
                $matching_param_count_callable = $possible_callable;
197
            }
198
199
            if ($all_args_match && !$type_coerced) {
200
                return $possible_callable;
201
            }
202
203
            if ($all_args_match) {
204
                $matching_coerced_param_count_callable = $possible_callable;
205
            }
206
        }
207
208
        if ($matching_coerced_param_count_callable) {
209
            return $matching_coerced_param_count_callable;
210
        }
211
212
        if ($matching_param_count_callable) {
213
            return $matching_param_count_callable;
214
        }
215
216
        // if we don't succeed in finding a match, set to the first possible and wait for issues below
217
        return $callables[0];
218
    }
219
220
    /**
221
     * @param  string $function_id
222
     *
223
     * @return array|null
224
     * @psalm-return array<int, TCallable>|null
225
     */
226
    public static function getCallablesFromCallMap($function_id)
227
    {
228
        $call_map_key = strtolower($function_id);
229
230
        if (isset(self::$call_map_callables[$call_map_key])) {
231
            return self::$call_map_callables[$call_map_key];
232
        }
233
234
        $call_map = self::getCallMap();
235
236
        if (!isset($call_map[$call_map_key])) {
237
            return null;
238
        }
239
240
        $call_map_functions = [];
241
        $call_map_functions[] = $call_map[$call_map_key];
242
243
        for ($i = 1; $i < 10; ++$i) {
244
            if (!isset($call_map[$call_map_key . '\'' . $i])) {
245
                break;
246
            }
247
248
            $call_map_functions[] = $call_map[$call_map_key . '\'' . $i];
249
        }
250
251
        $possible_callables = [];
252
253
        foreach ($call_map_functions as $call_map_function_args) {
254
            $return_type_string = array_shift($call_map_function_args);
255
256
            if (!$return_type_string) {
257
                $return_type = Type::getMixed();
258
            } else {
259
                $return_type = Type::parseString($return_type_string);
260
            }
261
262
            $function_params = [];
263
264
            $arg_offset = 0;
265
266
            /** @var string $arg_name - key type changed with above array_shift */
267
            foreach ($call_map_function_args as $arg_name => $arg_type) {
268
                $by_reference = false;
269
                $optional = false;
270
                $variadic = false;
271
272
                if ($arg_name[0] === '&') {
273
                    $arg_name = substr($arg_name, 1);
274
                    $by_reference = true;
275
                }
276
277
                if (substr($arg_name, -1) === '=') {
278
                    $arg_name = substr($arg_name, 0, -1);
279
                    $optional = true;
280
                }
281
282
                if (substr($arg_name, 0, 3) === '...') {
283
                    $arg_name = substr($arg_name, 3);
284
                    $variadic = true;
285
                }
286
287
                $param_type = $arg_type
288
                    ? Type::parseString($arg_type)
289
                    : Type::getMixed();
290
291
                $out_type = null;
292
293
                if (\strlen($arg_name) > 2 && $arg_name[0] === 'w' && $arg_name[1] === '_') {
294
                    $out_type = $param_type;
295
                    $param_type = Type::getMixed();
296
                }
297
298
                $function_param = new FunctionLikeParameter(
299
                    $arg_name,
300
                    $by_reference,
301
                    $param_type,
302
                    null,
303
                    null,
304
                    $optional,
305
                    false,
306
                    $variadic
307
                );
308
309
                if ($out_type) {
310
                    $function_param->out_type = $out_type;
311
                }
312
313
                if (isset(self::$taint_sink_map[$call_map_key][$arg_offset])) {
314
                    $function_param->sinks = self::$taint_sink_map[$call_map_key][$arg_offset];
315
                }
316
317
                $function_param->signature_type = null;
318
319
                $function_params[] = $function_param;
320
321
                $arg_offset++;
322
            }
323
324
            $possible_callables[] = new TCallable('callable', $function_params, $return_type);
325
        }
326
327
        self::$call_map_callables[$call_map_key] = $possible_callables;
328
329
        return $possible_callables;
330
    }
331
332
    /**
333
     * Gets the method/function call map
334
     *
335
     * @return array<string, array<int|string, string>>
0 ignored issues
show
Documentation introduced by
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
336
     * @psalm-suppress MixedInferredReturnType as the use of require buggers things up
337
     * @psalm-suppress MixedTypeCoercion
338
     * @psalm-suppress MixedReturnStatement
339
     */
340
    public static function getCallMap()
341
    {
342
        $codebase = ProjectAnalyzer::getInstance()->getCodebase();
343
        $analyzer_major_version = $codebase->php_major_version;
344
        $analyzer_minor_version = $codebase->php_minor_version;
345
346
        $analyzer_version = $analyzer_major_version . '.' . $analyzer_minor_version;
347
        $current_version = self::PHP_MAJOR_VERSION . '.' . self::PHP_MINOR_VERSION;
348
349
        $analyzer_version_int = (int) ($analyzer_major_version . $analyzer_minor_version);
350
        $current_version_int = (int) (self::PHP_MAJOR_VERSION . self::PHP_MINOR_VERSION);
351
352
        if (self::$call_map !== null
353
            && $analyzer_major_version === self::$loaded_php_major_version
354
            && $analyzer_minor_version === self::$loaded_php_minor_version
355
        ) {
356
            return self::$call_map;
357
        }
358
359
        /** @var array<string, array<int|string, string>> */
360
        $call_map = require(__DIR__ . '/../CallMap.php');
361
362
        self::$call_map = [];
363
364
        foreach ($call_map as $key => $value) {
365
            $cased_key = strtolower($key);
366
            self::$call_map[$cased_key] = $value;
367
        }
368
369
        /**
370
         * @var array<string, list<list<Type\TaintKind::*>>>
371
         */
372
        $taint_map = require(__DIR__ . '/../InternalTaintSinkMap.php');
373
374
        foreach ($taint_map as $key => $value) {
375
            $cased_key = strtolower($key);
376
            self::$taint_sink_map[$cased_key] = $value;
377
        }
378
379
        if (version_compare($analyzer_version, $current_version, '<')) {
380
            // the following assumes both minor and major versions a single digits
381
            for ($i = $current_version_int; $i > $analyzer_version_int && $i >= self::LOWEST_AVAILABLE_DELTA; --$i) {
382
                $delta_file = __DIR__ . '/../CallMap_' . $i . '_delta.php';
383
                if (!file_exists($delta_file)) {
384
                    continue;
385
                }
386
                /**
387
                 * @var array{
388
                 *     old: array<string, array<int|string, string>>,
389
                 *     new: array<string, array<int|string, string>>
390
                 * }
391
                 * @psalm-suppress UnresolvableInclude
392
                 */
393
                $diff_call_map = require($delta_file);
394
395
                foreach ($diff_call_map['new'] as $key => $_) {
396
                    $cased_key = strtolower($key);
397
                    unset(self::$call_map[$cased_key]);
398
                }
399
400
                foreach ($diff_call_map['old'] as $key => $value) {
401
                    $cased_key = strtolower($key);
402
                    self::$call_map[$cased_key] = $value;
403
                }
404
            }
405
        }
406
407
        self::$loaded_php_major_version = $analyzer_major_version;
408
        self::$loaded_php_minor_version = $analyzer_minor_version;
409
410
        return self::$call_map;
411
    }
412
413
    /**
414
     * @param   string $key
415
     *
416
     * @return  bool
417
     */
418
    public static function inCallMap($key)
419
    {
420
        return isset(self::getCallMap()[strtolower($key)]);
421
    }
422
}
423