1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-15 19:07:00 +01:00
psalm/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php

350 lines
12 KiB
PHP
Raw Normal View History

2018-01-14 18:09:40 +01:00
<?php
2018-11-06 03:57:36 +01:00
namespace Psalm\Internal\Analyzer\Statements\Expression;
2018-01-14 18:09:40 +01:00
use PhpParser;
2018-11-06 03:57:36 +01:00
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
2018-01-14 18:09:40 +01:00
use Psalm\CodeLocation;
use Psalm\Config;
use Psalm\Context;
use Psalm\Exception\FileIncludeException;
use Psalm\Issue\MissingFile;
use Psalm\Issue\UnresolvableInclude;
use Psalm\IssueBuffer;
use function str_replace;
use const DIRECTORY_SEPARATOR;
use function dirname;
use function preg_match;
use function in_array;
use function realpath;
use function get_included_files;
use function str_repeat;
use const PHP_EOL;
use function is_string;
use function implode;
use function defined;
use function constant;
use const PATH_SEPARATOR;
use function preg_split;
use function get_include_path;
use function explode;
use function substr;
use function file_exists;
use function preg_replace;
2018-01-14 18:09:40 +01:00
/**
* @internal
*/
2018-11-06 03:57:36 +01:00
class IncludeAnalyzer
2018-01-14 18:09:40 +01:00
{
/**
* @return false|null
*/
public static function analyze(
2018-11-11 18:01:14 +01:00
StatementsAnalyzer $statements_analyzer,
2018-01-14 18:09:40 +01:00
PhpParser\Node\Expr\Include_ $stmt,
Context $context,
Context $global_context = null
2018-01-14 18:09:40 +01:00
) {
2018-11-11 18:01:14 +01:00
$codebase = $statements_analyzer->getCodebase();
2018-11-06 03:57:36 +01:00
$config = $codebase->config;
2018-01-14 18:09:40 +01:00
if (!$config->allow_includes) {
throw new FileIncludeException(
'File includes are not allowed per your Psalm config - check the allowFileIncludes flag.'
);
}
2018-11-11 18:01:14 +01:00
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) {
2018-01-14 18:09:40 +01:00
return false;
}
if ($stmt->expr instanceof PhpParser\Node\Scalar\String_
|| (isset($stmt->expr->inferredType) && $stmt->expr->inferredType->isSingleStringLiteral())
) {
if ($stmt->expr instanceof PhpParser\Node\Scalar\String_) {
$path_to_file = $stmt->expr->value;
} else {
$path_to_file = $stmt->expr->inferredType->getSingleStringLiteral()->value;
}
$path_to_file = str_replace('/', DIRECTORY_SEPARATOR, $path_to_file);
2018-01-14 18:09:40 +01:00
// attempts to resolve using get_include_path dirs
$include_path = self::resolveIncludePath($path_to_file, dirname($statements_analyzer->getFilePath()));
2018-01-14 18:09:40 +01:00
$path_to_file = $include_path ? $include_path : $path_to_file;
if (DIRECTORY_SEPARATOR === '/') {
$is_path_relative = $path_to_file[0] !== DIRECTORY_SEPARATOR;
} else {
$is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $path_to_file);
}
if ($is_path_relative) {
$path_to_file = $config->base_dir . DIRECTORY_SEPARATOR . $path_to_file;
2018-01-14 18:09:40 +01:00
}
} else {
$path_to_file = self::getPathTo($stmt->expr, $statements_analyzer->getFileName(), $config);
2018-01-14 18:09:40 +01:00
}
if ($path_to_file) {
$path_to_file = self::normalizeFilePath($path_to_file);
2018-01-14 18:09:40 +01:00
// if the file is already included, we can't check much more
if (in_array(realpath($path_to_file), get_included_files(), true)) {
2018-01-14 18:09:40 +01:00
return null;
}
2018-11-11 18:01:14 +01:00
$current_file_analyzer = $statements_analyzer->getFileAnalyzer();
2018-01-14 18:09:40 +01:00
2018-11-11 18:01:14 +01:00
if ($current_file_analyzer->project_analyzer->fileExists($path_to_file)) {
if ($statements_analyzer->hasParentFilePath($path_to_file)
|| !$codebase->file_storage_provider->has($path_to_file)
2018-11-11 18:01:14 +01:00
|| ($statements_analyzer->hasAlreadyRequiredFilePath($path_to_file)
&& !$codebase->file_storage_provider->get($path_to_file)->has_extra_statements)
) {
return null;
}
2018-11-11 18:01:14 +01:00
$current_file_analyzer->addRequiredFilePath($path_to_file);
$file_name = $config->shortenFileName($path_to_file);
$nesting = $statements_analyzer->getRequireNesting() + 1;
$current_file_analyzer->project_analyzer->progress->debug(
str_repeat(' ', $nesting) . 'checking ' . $file_name . PHP_EOL
);
2018-11-11 18:01:14 +01:00
$include_file_analyzer = new \Psalm\Internal\Analyzer\FileAnalyzer(
$current_file_analyzer->project_analyzer,
$path_to_file,
$file_name
);
2018-11-11 18:01:14 +01:00
$include_file_analyzer->setRootFilePath(
$current_file_analyzer->getRootFilePath(),
$current_file_analyzer->getRootFileName()
);
2018-11-11 18:01:14 +01:00
$include_file_analyzer->addParentFilePath($current_file_analyzer->getFilePath());
$include_file_analyzer->addRequiredFilePath($current_file_analyzer->getFilePath());
2018-11-11 18:01:14 +01:00
foreach ($current_file_analyzer->getRequiredFilePaths() as $required_file_path) {
$include_file_analyzer->addRequiredFilePath($required_file_path);
}
2018-11-11 18:01:14 +01:00
foreach ($current_file_analyzer->getParentFilePaths() as $parent_file_path) {
$include_file_analyzer->addParentFilePath($parent_file_path);
}
try {
2018-11-11 18:01:14 +01:00
$include_file_analyzer->analyze(
$context,
false,
$global_context
);
} catch (\Psalm\Exception\UnpreparedAnalysisException $e) {
$context->check_classes = false;
$context->check_variables = false;
$context->check_functions = false;
}
2018-11-11 18:01:14 +01:00
foreach ($include_file_analyzer->getRequiredFilePaths() as $required_file_path) {
$current_file_analyzer->addRequiredFilePath($required_file_path);
2018-01-14 18:09:40 +01:00
}
return null;
}
2018-11-11 18:01:14 +01:00
$source = $statements_analyzer->getSource();
2018-01-14 18:09:40 +01:00
if (IssueBuffer::accepts(
new MissingFile(
'Cannot find file ' . $path_to_file . ' to include',
new CodeLocation($source, $stmt)
),
$source->getSuppressedIssues()
)) {
// fall through
}
} else {
2018-11-06 03:57:36 +01:00
$var_id = ExpressionAnalyzer::getArrayVarId($stmt->expr, null);
if (!$var_id || !isset($context->phantom_files[$var_id])) {
2018-11-11 18:01:14 +01:00
$source = $statements_analyzer->getSource();
if (IssueBuffer::accepts(
new UnresolvableInclude(
'Cannot resolve the given expression to a file path',
new CodeLocation($source, $stmt)
),
$source->getSuppressedIssues()
)) {
// fall through
}
2018-01-14 18:09:40 +01:00
}
}
$context->check_classes = false;
$context->check_variables = false;
$context->check_functions = false;
return null;
}
/**
* @param PhpParser\Node\Expr $stmt
* @param string $file_name
*
* @return string|null
* @psalm-suppress MixedAssignment
*/
public static function getPathTo(PhpParser\Node\Expr $stmt, $file_name, Config $config)
2018-01-14 18:09:40 +01:00
{
if (DIRECTORY_SEPARATOR === '/') {
$is_path_relative = $file_name[0] !== DIRECTORY_SEPARATOR;
} else {
$is_path_relative = !preg_match('~^[A-Z]:\\\\~i', $file_name);
}
if ($is_path_relative) {
$file_name = $config->base_dir . DIRECTORY_SEPARATOR . $file_name;
2018-01-14 18:09:40 +01:00
}
if ($stmt instanceof PhpParser\Node\Scalar\String_) {
2018-11-18 23:06:11 +01:00
if (DIRECTORY_SEPARATOR !== '/') {
return str_replace('/', DIRECTORY_SEPARATOR, $stmt->value);
}
2018-01-14 18:09:40 +01:00
return $stmt->value;
}
if (isset($stmt->inferredType) && $stmt->inferredType->isSingleStringLiteral()) {
2018-11-18 23:06:11 +01:00
if (DIRECTORY_SEPARATOR !== '/') {
return str_replace(
'/',
DIRECTORY_SEPARATOR,
$stmt->inferredType->getSingleStringLiteral()->value
);
}
return $stmt->inferredType->getSingleStringLiteral()->value;
}
2018-10-01 19:50:55 +02:00
if ($stmt instanceof PhpParser\Node\Expr\ArrayDimFetch) {
if ($stmt->var instanceof PhpParser\Node\Expr\Variable
&& $stmt->var->name === 'GLOBALS'
&& $stmt->dim instanceof PhpParser\Node\Scalar\String_
) {
2018-10-01 21:14:39 +02:00
if (isset($GLOBALS[$stmt->dim->value]) && is_string($GLOBALS[$stmt->dim->value])) {
/** @var string */
2018-10-01 19:50:55 +02:00
return $GLOBALS[$stmt->dim->value];
}
}
} elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat) {
$left_string = self::getPathTo($stmt->left, $file_name, $config);
$right_string = self::getPathTo($stmt->right, $file_name, $config);
2018-01-14 18:09:40 +01:00
if ($left_string && $right_string) {
return $left_string . $right_string;
}
} elseif ($stmt instanceof PhpParser\Node\Expr\FuncCall &&
$stmt->name instanceof PhpParser\Node\Name &&
$stmt->name->parts === ['dirname']
) {
if ($stmt->args) {
$dir_level = 1;
if (isset($stmt->args[1])) {
if ($stmt->args[1]->value instanceof PhpParser\Node\Scalar\LNumber) {
$dir_level = $stmt->args[1]->value->value;
} else {
return null;
}
}
$evaled_path = self::getPathTo($stmt->args[0]->value, $file_name, $config);
2018-01-14 18:09:40 +01:00
if (!$evaled_path) {
return null;
}
return dirname($evaled_path, $dir_level);
2018-01-14 18:09:40 +01:00
}
} elseif ($stmt instanceof PhpParser\Node\Expr\ConstFetch) {
2018-01-14 18:09:40 +01:00
$const_name = implode('', $stmt->name->parts);
if (defined($const_name)) {
$constant_value = constant($const_name);
if (is_string($constant_value)) {
return $constant_value;
}
}
} elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\Dir) {
return dirname($file_name);
} elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\File) {
return $file_name;
}
return null;
}
/**
* @param string $file_name
* @param string $current_directory
*
* @return string|null
*/
public static function resolveIncludePath($file_name, $current_directory)
{
if (!$current_directory) {
return $file_name;
}
$paths = PATH_SEPARATOR == ':'
? preg_split('#(?<!phar):#', get_include_path())
: explode(PATH_SEPARATOR, get_include_path());
foreach ($paths as $prefix) {
$ds = substr($prefix, -1) == DIRECTORY_SEPARATOR ? '' : DIRECTORY_SEPARATOR;
if ($prefix === '.') {
$prefix = $current_directory;
}
$file = $prefix . $ds . $file_name;
if (file_exists($file)) {
return $file;
}
}
return null;
}
public static function normalizeFilePath(string $path_to_file) : string
{
// replace all \ with / for normalization
$path_to_file = str_replace('\\', '/', $path_to_file);
$path_to_file = str_replace('/./', '/', $path_to_file);
// first remove unnecessary / duplicates
$path_to_file = preg_replace('/\/[\/]+/', '/', $path_to_file);
$path_to_file = preg_replace('/\/[\/]+/', '/', $path_to_file);
$reduce_pattern = '/\/[^\/]+\/\.\.\//';
while (preg_match($reduce_pattern, $path_to_file)) {
$path_to_file = preg_replace($reduce_pattern, DIRECTORY_SEPARATOR, $path_to_file, 1);
}
$path_to_file = str_replace('/./', '/', $path_to_file);
if (DIRECTORY_SEPARATOR !== '/') {
$path_to_file = str_replace('/', DIRECTORY_SEPARATOR, $path_to_file);
}
return $path_to_file;
}
2018-01-14 18:09:40 +01:00
}