IncludeAnalyzer::analyze()   F
last analyzed

Complexity

Conditions 35
Paths 4082

Size

Total Lines 205

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 35
nc 4082
nop 4
dl 0
loc 205
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Expression;
3
4
use PhpParser;
5
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
6
use Psalm\Internal\Analyzer\StatementsAnalyzer;
7
use Psalm\Internal\Taint\Sink;
8
use Psalm\CodeLocation;
9
use Psalm\Config;
10
use Psalm\Context;
11
use Psalm\Exception\FileIncludeException;
12
use Psalm\Issue\MissingFile;
13
use Psalm\Issue\UnresolvableInclude;
14
use Psalm\IssueBuffer;
15
use function str_replace;
16
use const DIRECTORY_SEPARATOR;
17
use function dirname;
18
use function preg_match;
19
use function in_array;
20
use function realpath;
21
use function get_included_files;
22
use function str_repeat;
23
use const PHP_EOL;
24
use function is_string;
25
use function implode;
26
use function defined;
27
use function constant;
28
use const PATH_SEPARATOR;
29
use function preg_split;
30
use function get_include_path;
31
use function explode;
32
use function substr;
33
use function file_exists;
34
use function preg_replace;
35
36
/**
37
 * @internal
38
 */
39
class IncludeAnalyzer
40
{
41
    public static function analyze(
42
        StatementsAnalyzer $statements_analyzer,
43
        PhpParser\Node\Expr\Include_ $stmt,
44
        Context $context,
45
        Context $global_context = null
46
    ) : bool {
47
        $codebase = $statements_analyzer->getCodebase();
48
        $config = $codebase->config;
49
50
        if (!$config->allow_includes) {
51
            throw new FileIncludeException(
52
                'File includes are not allowed per your Psalm config - check the allowFileIncludes flag.'
53
            );
54
        }
55
56
        $was_inside_call = $context->inside_call;
57
58
        $context->inside_call = true;
59
60
        if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) {
61
            return false;
62
        }
63
64
        if (!$was_inside_call) {
65
            $context->inside_call = false;
66
        }
67
68
        $stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr);
69
70
        if ($stmt->expr instanceof PhpParser\Node\Scalar\String_
71
            || ($stmt_expr_type && $stmt_expr_type->isSingleStringLiteral())
72
        ) {
73
            if ($stmt->expr instanceof PhpParser\Node\Scalar\String_) {
74
                $path_to_file = $stmt->expr->value;
75
            } else {
76
                $path_to_file = $stmt_expr_type->getSingleStringLiteral()->value;
77
            }
78
79
            $path_to_file = str_replace('/', DIRECTORY_SEPARATOR, $path_to_file);
80
81
            // attempts to resolve using get_include_path dirs
82
            $include_path = self::resolveIncludePath($path_to_file, dirname($statements_analyzer->getFilePath()));
83
            $path_to_file = $include_path ? $include_path : $path_to_file;
84
85
            if (DIRECTORY_SEPARATOR === '/') {
86
                $is_path_relative = $path_to_file[0] !== DIRECTORY_SEPARATOR;
87
            } else {
88
                $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $path_to_file);
89
            }
90
91
            if ($is_path_relative) {
92
                $path_to_file = $config->base_dir . DIRECTORY_SEPARATOR . $path_to_file;
93
            }
94
        } else {
95
            $path_to_file = self::getPathTo(
96
                $stmt->expr,
97
                $statements_analyzer->node_data,
98
                $statements_analyzer,
99
                $statements_analyzer->getFileName(),
100
                $config
101
            );
102
        }
103
104
        if ($stmt_expr_type
105
            && $codebase->taint
106
            && $stmt_expr_type->parent_nodes
107
            && $codebase->config->trackTaintsInPath($statements_analyzer->getFilePath())
108
            && !\in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())
109
        ) {
110
            $arg_location = new CodeLocation($statements_analyzer->getSource(), $stmt->expr);
111
112
            $include_param_sink = Sink::getForMethodArgument(
113
                'include',
114
                'include',
115
                0,
116
                $arg_location
117
            );
118
119
            $include_param_sink->taints = [\Psalm\Type\TaintKind::INPUT_TEXT];
0 ignored issues
show
Documentation Bug introduced by
It seems like array(\Psalm\Type\TaintKind::INPUT_TEXT) of type array<integer,?> is incompatible with the declared type array<integer,string> of property $taints.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
120
121
            $codebase->taint->addSink($include_param_sink);
122
123
            foreach ($stmt_expr_type->parent_nodes as $parent_node) {
124
                $codebase->taint->addPath($parent_node, $include_param_sink, 'arg');
125
            }
126
        }
127
128
        if ($path_to_file) {
129
            $path_to_file = self::normalizeFilePath($path_to_file);
130
131
            // if the file is already included, we can't check much more
132
            if (in_array(realpath($path_to_file), get_included_files(), true)) {
133
                return true;
134
            }
135
136
            $current_file_analyzer = $statements_analyzer->getFileAnalyzer();
137
138
            if ($current_file_analyzer->project_analyzer->fileExists($path_to_file)) {
139
                if ($statements_analyzer->hasParentFilePath($path_to_file)
140
                    || !$codebase->file_storage_provider->has($path_to_file)
141
                    || ($statements_analyzer->hasAlreadyRequiredFilePath($path_to_file)
142
                        && !$codebase->file_storage_provider->get($path_to_file)->has_extra_statements)
143
                ) {
144
                    return true;
145
                }
146
147
                $current_file_analyzer->addRequiredFilePath($path_to_file);
148
149
                $file_name = $config->shortenFileName($path_to_file);
150
151
                $nesting = $statements_analyzer->getRequireNesting() + 1;
152
                $current_file_analyzer->project_analyzer->progress->debug(
153
                    str_repeat('  ', $nesting) . 'checking ' . $file_name . PHP_EOL
154
                );
155
156
                $include_file_analyzer = new \Psalm\Internal\Analyzer\FileAnalyzer(
157
                    $current_file_analyzer->project_analyzer,
158
                    $path_to_file,
159
                    $file_name
160
                );
161
162
                $include_file_analyzer->setRootFilePath(
163
                    $current_file_analyzer->getRootFilePath(),
164
                    $current_file_analyzer->getRootFileName()
165
                );
166
167
                $include_file_analyzer->addParentFilePath($current_file_analyzer->getFilePath());
168
                $include_file_analyzer->addRequiredFilePath($current_file_analyzer->getFilePath());
169
170
                foreach ($current_file_analyzer->getRequiredFilePaths() as $required_file_path) {
171
                    $include_file_analyzer->addRequiredFilePath($required_file_path);
172
                }
173
174
                foreach ($current_file_analyzer->getParentFilePaths() as $parent_file_path) {
175
                    $include_file_analyzer->addParentFilePath($parent_file_path);
176
                }
177
178
                try {
179
                    $include_file_analyzer->analyze(
180
                        $context,
181
                        false,
182
                        $global_context
183
                    );
184
                } catch (\Psalm\Exception\UnpreparedAnalysisException $e) {
185
                    if ($config->skip_checks_on_unresolvable_includes) {
186
                        $context->check_classes = false;
187
                        $context->check_variables = false;
188
                        $context->check_functions = false;
189
                    }
190
                }
191
192
                $included_return_type = $include_file_analyzer->getReturnType();
193
194
                if ($included_return_type) {
195
                    $statements_analyzer->node_data->setType($stmt, $included_return_type);
196
                }
197
198
                $context->has_returned = false;
199
200
                foreach ($include_file_analyzer->getRequiredFilePaths() as $required_file_path) {
201
                    $current_file_analyzer->addRequiredFilePath($required_file_path);
202
                }
203
204
                $include_file_analyzer->clearSourceBeforeDestruction();
205
206
                return true;
207
            }
208
209
            $source = $statements_analyzer->getSource();
210
211
            if (IssueBuffer::accepts(
212
                new MissingFile(
213
                    'Cannot find file ' . $path_to_file . ' to include',
214
                    new CodeLocation($source, $stmt)
215
                ),
216
                $source->getSuppressedIssues()
217
            )) {
218
                // fall through
219
            }
220
        } else {
221
            $var_id = ExpressionIdentifier::getArrayVarId($stmt->expr, null);
222
223
            if (!$var_id || !isset($context->phantom_files[$var_id])) {
224
                $source = $statements_analyzer->getSource();
225
226
                if (IssueBuffer::accepts(
227
                    new UnresolvableInclude(
228
                        'Cannot resolve the given expression to a file path',
229
                        new CodeLocation($source, $stmt)
230
                    ),
231
                    $source->getSuppressedIssues()
232
                )) {
233
                    // fall through
234
                }
235
            }
236
        }
237
238
        if ($config->skip_checks_on_unresolvable_includes) {
239
            $context->check_classes = false;
240
            $context->check_variables = false;
241
            $context->check_functions = false;
242
        }
243
244
        return true;
245
    }
246
247
    /**
248
     * @param  PhpParser\Node\Expr $stmt
249
     * @param  string              $file_name
250
     *
251
     * @return string|null
252
     * @psalm-suppress MixedAssignment
253
     */
254
    public static function getPathTo(
255
        PhpParser\Node\Expr $stmt,
256
        ?\Psalm\Internal\Provider\NodeDataProvider $type_provider,
257
        ?StatementsAnalyzer $statements_analyzer,
258
        $file_name,
259
        Config $config
260
    ) {
261
        if (DIRECTORY_SEPARATOR === '/') {
262
            $is_path_relative = $file_name[0] !== DIRECTORY_SEPARATOR;
263
        } else {
264
            $is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $file_name);
265
        }
266
267
        if ($is_path_relative) {
268
            $file_name = $config->base_dir . DIRECTORY_SEPARATOR . $file_name;
269
        }
270
271
        if ($stmt instanceof PhpParser\Node\Scalar\String_) {
272
            if (DIRECTORY_SEPARATOR !== '/') {
273
                return str_replace('/', DIRECTORY_SEPARATOR, $stmt->value);
274
            }
275
            return $stmt->value;
276
        }
277
278
        $stmt_type = $type_provider ? $type_provider->getType($stmt) : null;
279
280
        if ($stmt_type && $stmt_type->isSingleStringLiteral()) {
281
            if (DIRECTORY_SEPARATOR !== '/') {
282
                return str_replace(
283
                    '/',
284
                    DIRECTORY_SEPARATOR,
285
                    $stmt_type->getSingleStringLiteral()->value
286
                );
287
            }
288
289
            return $stmt_type->getSingleStringLiteral()->value;
290
        }
291
292
        if ($stmt instanceof PhpParser\Node\Expr\ArrayDimFetch) {
293
            if ($stmt->var instanceof PhpParser\Node\Expr\Variable
294
                && $stmt->var->name === 'GLOBALS'
295
                && $stmt->dim instanceof PhpParser\Node\Scalar\String_
296
            ) {
297
                if (isset($GLOBALS[$stmt->dim->value]) && is_string($GLOBALS[$stmt->dim->value])) {
298
                    /** @var string */
299
                    return $GLOBALS[$stmt->dim->value];
300
                }
301
            }
302
        } elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat) {
303
            $left_string = self::getPathTo($stmt->left, $type_provider, $statements_analyzer, $file_name, $config);
304
            $right_string = self::getPathTo($stmt->right, $type_provider, $statements_analyzer, $file_name, $config);
305
306
            if ($left_string && $right_string) {
307
                return $left_string . $right_string;
308
            }
309
        } elseif ($stmt instanceof PhpParser\Node\Expr\FuncCall &&
310
            $stmt->name instanceof PhpParser\Node\Name &&
311
            $stmt->name->parts === ['dirname']
312
        ) {
313
            if ($stmt->args) {
314
                $dir_level = 1;
315
316
                if (isset($stmt->args[1])) {
317
                    if ($stmt->args[1]->value instanceof PhpParser\Node\Scalar\LNumber) {
318
                        $dir_level = $stmt->args[1]->value->value;
319
                    } else {
320
                        return null;
321
                    }
322
                }
323
324
                $evaled_path = self::getPathTo(
325
                    $stmt->args[0]->value,
326
                    $type_provider,
327
                    $statements_analyzer,
328
                    $file_name,
329
                    $config
330
                );
331
332
                if (!$evaled_path) {
333
                    return null;
334
                }
335
336
                return dirname($evaled_path, $dir_level);
337
            }
338
        } elseif ($stmt instanceof PhpParser\Node\Expr\ConstFetch) {
339
            $const_name = implode('', $stmt->name->parts);
340
341
            if (defined($const_name)) {
342
                $constant_value = constant($const_name);
343
344
                if (is_string($constant_value)) {
345
                    return $constant_value;
346
                }
347
            }
348
        } elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\Dir) {
349
            return dirname($file_name);
350
        } elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\File) {
351
            return $file_name;
352
        }
353
354
        return null;
355
    }
356
357
    /**
358
     * @param   string  $file_name
359
     * @param   string  $current_directory
360
     *
361
     * @return  string|null
362
     */
363
    public static function resolveIncludePath($file_name, $current_directory)
364
    {
365
        if (!$current_directory) {
366
            return $file_name;
367
        }
368
369
        $paths = PATH_SEPARATOR == ':'
370
            ? preg_split('#(?<!phar):#', get_include_path())
371
            : explode(PATH_SEPARATOR, get_include_path());
372
373
        foreach ($paths as $prefix) {
374
            $ds = substr($prefix, -1) == DIRECTORY_SEPARATOR ? '' : DIRECTORY_SEPARATOR;
375
376
            if ($prefix === '.') {
377
                $prefix = $current_directory;
378
            }
379
380
            $file = $prefix . $ds . $file_name;
381
382
            if (file_exists($file)) {
383
                return $file;
384
            }
385
        }
386
387
        return null;
388
    }
389
390
    public static function normalizeFilePath(string $path_to_file) : string
391
    {
392
        // replace all \ with / for normalization
393
        $path_to_file = str_replace('\\', '/', $path_to_file);
394
        $path_to_file = str_replace('/./', '/', $path_to_file);
395
396
        // first remove unnecessary / duplicates
397
        $path_to_file = preg_replace('/\/[\/]+/', '/', $path_to_file);
398
399
        $reduce_pattern = '/\/[^\/]+\/\.\.\//';
400
401
        while (preg_match($reduce_pattern, $path_to_file)) {
402
            $path_to_file = preg_replace($reduce_pattern, '/', $path_to_file, 1);
403
        }
404
405
        if (DIRECTORY_SEPARATOR !== '/') {
406
            $path_to_file = str_replace('/', DIRECTORY_SEPARATOR, $path_to_file);
407
        }
408
409
        return $path_to_file;
410
    }
411
}
412