2016-01-20 00:27:06 +01:00
|
|
|
<?php
|
2016-08-13 20:20:46 +02:00
|
|
|
namespace Psalm\Checker;
|
2016-01-20 00:27:06 +01:00
|
|
|
|
2016-02-04 15:22:46 +01:00
|
|
|
use PhpParser;
|
2016-10-22 23:35:59 +02:00
|
|
|
use Psalm\Checker\Statements\Block\ForChecker;
|
2016-10-22 19:23:18 +02:00
|
|
|
use Psalm\Checker\Statements\Block\ForeachChecker;
|
|
|
|
use Psalm\Checker\Statements\Block\IfChecker;
|
2016-10-22 19:37:06 +02:00
|
|
|
use Psalm\Checker\Statements\Block\SwitchChecker;
|
2016-10-22 23:35:59 +02:00
|
|
|
use Psalm\Checker\Statements\Block\TryChecker;
|
|
|
|
use Psalm\Checker\Statements\Block\WhileChecker;
|
2016-10-22 19:23:18 +02:00
|
|
|
use Psalm\Checker\Statements\ExpressionChecker;
|
2016-11-01 16:37:58 +01:00
|
|
|
use Psalm\Checker\Statements\Expression\AssignmentChecker;
|
2016-12-09 18:48:02 +01:00
|
|
|
use Psalm\Checker\Statements\Expression\CallChecker;
|
2016-12-04 01:11:30 +01:00
|
|
|
use Psalm\CodeLocation;
|
2016-11-02 07:29:00 +01:00
|
|
|
use Psalm\Config;
|
|
|
|
use Psalm\Context;
|
2016-12-06 22:41:42 +01:00
|
|
|
use Psalm\Exception\FileIncludeException;
|
2016-10-17 22:42:23 +02:00
|
|
|
use Psalm\Issue\ContinueOutsideLoop;
|
2016-11-05 22:53:30 +01:00
|
|
|
use Psalm\Issue\InvalidGlobal;
|
2016-10-09 23:54:58 +02:00
|
|
|
use Psalm\Issue\InvalidNamespace;
|
2017-02-12 22:49:32 +01:00
|
|
|
use Psalm\Issue\UnevaluatedCode;
|
2016-11-06 01:53:39 +01:00
|
|
|
use Psalm\Issue\UnrecognizedStatement;
|
2016-11-02 07:29:00 +01:00
|
|
|
use Psalm\IssueBuffer;
|
2016-08-13 20:20:46 +02:00
|
|
|
use Psalm\StatementsSource;
|
2016-10-22 19:23:18 +02:00
|
|
|
use Psalm\Type;
|
2016-08-13 20:20:46 +02:00
|
|
|
|
2017-01-07 20:35:07 +01:00
|
|
|
class StatementsChecker extends SourceChecker implements StatementsSource
|
2016-01-20 00:27:06 +01:00
|
|
|
{
|
2016-10-09 23:54:58 +02:00
|
|
|
/**
|
|
|
|
* @var StatementsSource
|
|
|
|
*/
|
2016-08-11 00:10:12 +02:00
|
|
|
protected $source;
|
2016-10-09 23:54:58 +02:00
|
|
|
|
2016-11-01 05:39:41 +01:00
|
|
|
/**
|
2017-02-08 00:09:12 +01:00
|
|
|
* @var array<string, CodeLocation>
|
2016-11-01 05:39:41 +01:00
|
|
|
*/
|
2017-02-12 00:56:38 +01:00
|
|
|
private $all_vars = [];
|
2016-11-01 05:39:41 +01:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @var array<string, array<string, Type\Union>>
|
|
|
|
*/
|
2017-01-02 21:31:18 +01:00
|
|
|
public static $user_constants = [];
|
2016-08-14 06:38:29 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @param StatementsSource $source
|
|
|
|
*/
|
2016-10-18 22:28:51 +02:00
|
|
|
public function __construct(StatementsSource $source)
|
2016-01-20 00:27:06 +01:00
|
|
|
{
|
2016-08-11 00:10:12 +02:00
|
|
|
$this->source = $source;
|
2016-01-20 00:27:06 +01:00
|
|
|
}
|
|
|
|
|
2016-06-18 20:45:55 +02:00
|
|
|
/**
|
|
|
|
* Checks an array of statements for validity
|
|
|
|
*
|
2016-12-12 19:50:46 +01:00
|
|
|
* @param array<PhpParser\Node\Stmt|PhpParser\Node\Expr> $stmts
|
|
|
|
* @param Context $context
|
|
|
|
* @param Context|null $loop_context
|
|
|
|
* @param Context|null $global_context
|
2016-06-18 20:45:55 +02:00
|
|
|
* @return null|false
|
|
|
|
*/
|
2017-01-07 21:09:47 +01:00
|
|
|
public function analyze(
|
2017-01-07 20:35:07 +01:00
|
|
|
array $stmts,
|
|
|
|
Context $context,
|
|
|
|
Context $loop_context = null,
|
|
|
|
Context $global_context = null
|
|
|
|
) {
|
2016-01-20 00:27:06 +01:00
|
|
|
$has_returned = false;
|
|
|
|
|
2016-08-14 19:13:53 +02:00
|
|
|
$function_checkers = [];
|
|
|
|
|
2016-08-15 06:58:30 +02:00
|
|
|
// hoist functions to the top
|
|
|
|
foreach ($stmts as $stmt) {
|
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\Function_) {
|
2017-01-06 07:07:11 +01:00
|
|
|
$function_checker = new FunctionChecker($stmt, $this->source);
|
2016-08-15 06:58:30 +02:00
|
|
|
$function_checkers[$stmt->name] = $function_checker;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-20 00:27:06 +01:00
|
|
|
foreach ($stmts as $stmt) {
|
2016-12-04 01:11:30 +01:00
|
|
|
$plugins = Config::getInstance()->getPlugins();
|
|
|
|
|
|
|
|
if ($plugins) {
|
|
|
|
$code_location = new CodeLocation($this->source, $stmt);
|
2016-12-06 22:41:42 +01:00
|
|
|
|
2016-12-04 01:11:30 +01:00
|
|
|
foreach ($plugins as $plugin) {
|
|
|
|
if ($plugin->checkStatement(
|
2017-01-02 21:31:18 +01:00
|
|
|
$this,
|
2016-12-04 01:11:30 +01:00
|
|
|
$stmt,
|
|
|
|
$context,
|
|
|
|
$code_location,
|
|
|
|
$this->getSuppressedIssues()
|
|
|
|
) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-06-18 20:45:55 +02:00
|
|
|
}
|
|
|
|
}
|
2016-12-06 22:41:42 +01:00
|
|
|
|
2016-06-18 20:45:55 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
if ($has_returned && !($stmt instanceof PhpParser\Node\Stmt\Nop) &&
|
2017-01-07 20:35:07 +01:00
|
|
|
!($stmt instanceof PhpParser\Node\Stmt\InlineHTML)
|
|
|
|
) {
|
2017-02-27 05:09:18 +01:00
|
|
|
if ($context->collect_references) {
|
2017-02-12 22:49:32 +01:00
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new UnevaluatedCode(
|
|
|
|
'Expressions after return/throw/continue',
|
|
|
|
new CodeLocation($this->source, $stmt)
|
|
|
|
),
|
|
|
|
$this->source->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2016-01-20 16:57:56 +01:00
|
|
|
break;
|
2016-01-20 00:27:06 +01:00
|
|
|
}
|
|
|
|
|
2017-01-26 01:09:31 +01:00
|
|
|
/*
|
2017-03-13 23:06:56 +01:00
|
|
|
if (isset($context->vars_in_scope['$failed_reconciliation']) && !$stmt instanceof PhpParser\Node\Stmt\Nop) {
|
|
|
|
var_dump($stmt->getLine() . ' ' . $context->vars_in_scope['$failed_reconciliation']);
|
2016-10-09 23:54:58 +02:00
|
|
|
}
|
2017-01-26 01:09:31 +01:00
|
|
|
*/
|
2016-10-09 23:54:58 +02:00
|
|
|
|
2016-01-20 00:27:06 +01:00
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
|
2017-01-07 21:09:47 +01:00
|
|
|
IfChecker::analyze($this, $stmt, $context, $loop_context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
|
2017-01-07 21:09:47 +01:00
|
|
|
TryChecker::analyze($this, $stmt, $context, $loop_context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\For_) {
|
2017-01-07 21:09:47 +01:00
|
|
|
ForChecker::analyze($this, $stmt, $context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Foreach_) {
|
2017-01-07 21:09:47 +01:00
|
|
|
ForeachChecker::analyze($this, $stmt, $context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\While_) {
|
2017-01-07 21:09:47 +01:00
|
|
|
WhileChecker::analyze($this, $stmt, $context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Do_) {
|
2017-01-07 21:09:47 +01:00
|
|
|
$this->analyzeDo($stmt, $context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Const_) {
|
2017-01-07 21:09:47 +01:00
|
|
|
$this->analyzeConstAssignment($stmt, $context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Unset_) {
|
2016-09-17 17:57:44 +02:00
|
|
|
foreach ($stmt->vars as $var) {
|
2017-02-08 06:28:26 +01:00
|
|
|
ExpressionChecker::analyze($this, $var, $context);
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
$var_id = ExpressionChecker::getArrayVarId(
|
|
|
|
$var,
|
2017-01-07 20:35:07 +01:00
|
|
|
$this->getFQCLN(),
|
|
|
|
$this
|
2016-11-02 07:29:00 +01:00
|
|
|
);
|
2016-09-17 17:57:44 +02:00
|
|
|
|
|
|
|
if ($var_id) {
|
|
|
|
$context->remove($var_id);
|
|
|
|
}
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Return_) {
|
2016-01-20 00:27:06 +01:00
|
|
|
$has_returned = true;
|
2017-01-07 21:09:47 +01:00
|
|
|
$this->analyzeReturn($stmt, $context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Throw_) {
|
2016-04-03 02:27:36 +02:00
|
|
|
$has_returned = true;
|
2017-01-07 21:09:47 +01:00
|
|
|
$this->analyzeThrow($stmt, $context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
|
2017-01-07 21:09:47 +01:00
|
|
|
SwitchChecker::analyze($this, $stmt, $context, $loop_context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Break_) {
|
2016-01-20 00:27:06 +01:00
|
|
|
// do nothing
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
|
2016-10-17 22:42:23 +02:00
|
|
|
if ($loop_context === null) {
|
|
|
|
if (IssueBuffer::accepts(
|
2016-11-02 07:29:00 +01:00
|
|
|
new ContinueOutsideLoop(
|
|
|
|
'Continue call outside loop context',
|
2016-12-04 01:11:30 +01:00
|
|
|
new CodeLocation($this->source, $stmt)
|
2016-11-02 07:29:00 +01:00
|
|
|
),
|
2017-01-07 20:35:07 +01:00
|
|
|
$this->source->getSuppressedIssues()
|
2016-10-17 22:42:23 +02:00
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-03 02:27:36 +02:00
|
|
|
$has_returned = true;
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Static_) {
|
2017-01-07 21:09:47 +01:00
|
|
|
$this->analyzeStatic($stmt, $context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Echo_) {
|
2016-12-09 18:48:02 +01:00
|
|
|
foreach ($stmt->exprs as $i => $expr) {
|
2017-01-07 21:09:47 +01:00
|
|
|
ExpressionChecker::analyze($this, $expr, $context);
|
2016-12-09 18:48:02 +01:00
|
|
|
|
|
|
|
if (isset($expr->inferredType)) {
|
|
|
|
if (CallChecker::checkFunctionArgumentType(
|
|
|
|
$this,
|
|
|
|
$expr->inferredType,
|
|
|
|
Type::getString(),
|
|
|
|
'echo',
|
2016-12-09 19:27:59 +01:00
|
|
|
(int)$i,
|
2016-12-09 18:48:02 +01:00
|
|
|
new CodeLocation($this->getSource(), $expr)
|
|
|
|
) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2016-01-20 00:27:06 +01:00
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) {
|
2017-03-19 20:41:24 +01:00
|
|
|
$project_checker = $this->getFileChecker()->project_checker;
|
2016-12-29 01:57:18 +01:00
|
|
|
|
2017-03-19 20:41:24 +01:00
|
|
|
if (!$project_checker->register_global_functions) {
|
|
|
|
$function_context = new Context($context->self);
|
|
|
|
$function_context->collect_references = $project_checker->collect_references;
|
|
|
|
$function_checkers[$stmt->name]->analyze($function_context, $context);
|
2016-12-29 01:57:18 +01:00
|
|
|
|
2017-03-19 20:41:24 +01:00
|
|
|
$config = Config::getInstance();
|
2016-12-29 01:57:18 +01:00
|
|
|
|
2017-03-19 20:41:24 +01:00
|
|
|
if (!$config->excludeIssueInFile('InvalidReturnType', $this->getFilePath())) {
|
|
|
|
/** @var string */
|
|
|
|
$method_id = $function_checkers[$stmt->name]->getMethodId();
|
2016-12-29 01:57:18 +01:00
|
|
|
|
2017-03-19 20:41:24 +01:00
|
|
|
$function_storage = FunctionChecker::getStorage($method_id, $this->getFilePath());
|
2016-12-29 01:57:18 +01:00
|
|
|
|
2017-03-19 20:41:24 +01:00
|
|
|
$return_type = $function_storage->return_type;
|
|
|
|
$return_type_location = $function_storage->return_type_location;
|
|
|
|
|
|
|
|
$function_checkers[$stmt->name]->verifyReturnType(
|
|
|
|
false,
|
|
|
|
$return_type,
|
|
|
|
$this->getFQCLN(),
|
|
|
|
$return_type_location
|
|
|
|
);
|
|
|
|
}
|
2016-12-29 01:57:18 +01:00
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr) {
|
2017-01-07 21:09:47 +01:00
|
|
|
ExpressionChecker::analyze($this, $stmt, $context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\InlineHTML) {
|
2016-01-20 00:27:06 +01:00
|
|
|
// do nothing
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Global_) {
|
2017-02-12 19:27:02 +01:00
|
|
|
if (!$context->collect_initializations && !$global_context) {
|
2016-11-05 22:53:30 +01:00
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new InvalidGlobal(
|
|
|
|
'Cannot use global scope here',
|
2016-12-04 01:11:30 +01:00
|
|
|
new CodeLocation($this->source, $stmt)
|
2016-11-05 22:53:30 +01:00
|
|
|
),
|
2017-01-07 20:35:07 +01:00
|
|
|
$this->source->getSuppressedIssues()
|
2016-11-05 22:53:30 +01:00
|
|
|
)) {
|
2017-02-12 18:23:05 +01:00
|
|
|
// fall through
|
2016-11-05 22:53:30 +01:00
|
|
|
}
|
2017-02-12 19:27:02 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($stmt->vars as $var) {
|
|
|
|
if ($var instanceof PhpParser\Node\Expr\Variable) {
|
|
|
|
if (is_string($var->name)) {
|
|
|
|
$var_id = '$' . $var->name;
|
2016-11-05 22:53:30 +01:00
|
|
|
|
2017-02-12 19:27:02 +01:00
|
|
|
$context->vars_in_scope[$var_id] =
|
|
|
|
$global_context && $global_context->hasVariable($var_id)
|
2016-11-05 22:53:30 +01:00
|
|
|
? clone $global_context->vars_in_scope[$var_id]
|
|
|
|
: Type::getMixed();
|
|
|
|
|
2017-02-12 19:27:02 +01:00
|
|
|
$context->vars_possibly_in_scope[$var_id] = true;
|
|
|
|
} else {
|
|
|
|
ExpressionChecker::analyze($this, $var, $context);
|
2016-01-20 00:27:06 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Property) {
|
2016-01-26 20:11:56 +01:00
|
|
|
foreach ($stmt->props as $prop) {
|
|
|
|
if ($prop->default) {
|
2017-01-07 21:09:47 +01:00
|
|
|
ExpressionChecker::analyze($this, $prop->default, $context);
|
2016-06-24 00:45:46 +02:00
|
|
|
|
|
|
|
if (isset($prop->default->inferredType)) {
|
2016-08-07 21:15:21 +02:00
|
|
|
if (!$stmt->isStatic()) {
|
2017-01-07 21:09:47 +01:00
|
|
|
if (AssignmentChecker::analyzePropertyAssignment(
|
2016-11-02 07:29:00 +01:00
|
|
|
$this,
|
|
|
|
$prop,
|
|
|
|
$prop->name,
|
2016-12-07 00:27:22 +01:00
|
|
|
$prop->default,
|
2016-11-02 07:29:00 +01:00
|
|
|
$prop->default->inferredType,
|
|
|
|
$context
|
|
|
|
) === false) {
|
2017-02-12 19:27:02 +01:00
|
|
|
// fall through
|
2016-08-07 21:15:21 +02:00
|
|
|
}
|
2016-06-24 00:45:46 +02:00
|
|
|
}
|
|
|
|
}
|
2016-01-26 20:11:56 +01:00
|
|
|
}
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\ClassConst) {
|
2016-12-04 07:44:33 +01:00
|
|
|
$const_visibility = \ReflectionProperty::IS_PUBLIC;
|
|
|
|
|
|
|
|
if ($stmt->isProtected()) {
|
|
|
|
$const_visibility = \ReflectionProperty::IS_PROTECTED;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt->isPrivate()) {
|
|
|
|
$const_visibility = \ReflectionProperty::IS_PRIVATE;
|
|
|
|
}
|
2016-12-06 22:41:42 +01:00
|
|
|
|
2016-08-14 03:14:32 +02:00
|
|
|
foreach ($stmt->consts as $const) {
|
2017-01-07 21:09:47 +01:00
|
|
|
ExpressionChecker::analyze($this, $const->value, $context);
|
2016-08-13 20:20:46 +02:00
|
|
|
|
2016-08-14 03:14:32 +02:00
|
|
|
if (isset($const->value->inferredType) && !$const->value->inferredType->isMixed()) {
|
2016-11-02 07:29:00 +01:00
|
|
|
ClassLikeChecker::setConstantType(
|
2017-01-07 20:35:07 +01:00
|
|
|
(string)$this->getFQCLN(),
|
2016-11-02 07:29:00 +01:00
|
|
|
$const->name,
|
2016-12-04 07:44:33 +01:00
|
|
|
$const->value->inferredType,
|
|
|
|
$const_visibility
|
2016-11-02 07:29:00 +01:00
|
|
|
);
|
2016-08-14 03:14:32 +02:00
|
|
|
}
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Class_) {
|
2017-01-29 19:20:12 +01:00
|
|
|
$class_checker = (new ClassChecker($stmt, $this->source, $stmt->name));
|
|
|
|
$class_checker->visit();
|
|
|
|
$class_checker->analyze(null, $global_context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Nop) {
|
2017-03-02 04:27:52 +01:00
|
|
|
if ((string)$stmt->getDocComment()) {
|
|
|
|
CommentChecker::getTypeFromComment(
|
|
|
|
(string)$stmt->getDocComment(),
|
|
|
|
$context,
|
|
|
|
$this->getSource()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Goto_) {
|
2016-10-22 19:23:18 +02:00
|
|
|
// do nothing
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Label) {
|
2016-10-22 19:23:18 +02:00
|
|
|
// do nothing
|
2016-11-20 08:52:34 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Declare_) {
|
|
|
|
// do nothing
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2016-11-06 01:53:39 +01:00
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new UnrecognizedStatement(
|
|
|
|
'Psalm does not understand ' . get_class($stmt),
|
2016-12-04 01:11:30 +01:00
|
|
|
new CodeLocation($this->source, $stmt)
|
2016-11-06 01:53:39 +01:00
|
|
|
),
|
|
|
|
$this->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-01-20 00:27:06 +01:00
|
|
|
}
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
|
|
|
|
return null;
|
2016-01-20 00:27:06 +01:00
|
|
|
}
|
|
|
|
|
2017-03-13 23:06:56 +01:00
|
|
|
/**
|
|
|
|
* Checks an array of statements in a loop
|
|
|
|
*
|
|
|
|
* @param array<PhpParser\Node\Stmt|PhpParser\Node\Expr> $stmts
|
2017-03-15 01:14:25 +01:00
|
|
|
* @param array<int, string> $asserted_vars
|
2017-03-13 23:06:56 +01:00
|
|
|
* @param Context $loop_context
|
|
|
|
* @param Context $outer_context
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function analyzeLoop(
|
|
|
|
array $stmts,
|
2017-03-15 01:14:25 +01:00
|
|
|
array $asserted_vars,
|
2017-03-13 23:06:56 +01:00
|
|
|
Context $loop_context,
|
|
|
|
Context $outer_context
|
|
|
|
) {
|
2017-03-15 01:14:25 +01:00
|
|
|
$traverser = new PhpParser\NodeTraverser;
|
2017-03-13 23:06:56 +01:00
|
|
|
|
2017-03-15 01:14:25 +01:00
|
|
|
$assignment_mapper = new \Psalm\Visitor\AssignmentMapVisitor($loop_context->self);
|
|
|
|
$traverser->addVisitor($assignment_mapper);
|
2017-03-13 23:06:56 +01:00
|
|
|
|
2017-03-15 01:14:25 +01:00
|
|
|
$traverser->traverse($stmts);
|
|
|
|
|
|
|
|
$assignment_map = $assignment_mapper->getAssignmentMap();
|
|
|
|
|
|
|
|
$assignment_depth = 0;
|
|
|
|
|
|
|
|
if ($assignment_map) {
|
|
|
|
$first_var_id = array_keys($assignment_map)[0];
|
|
|
|
|
|
|
|
$assignment_depth = self::getAssignmentMapDepth($first_var_id, $assignment_map);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($assignment_depth === 0) {
|
|
|
|
$this->analyze($stmts, $loop_context, $outer_context);
|
|
|
|
} else {
|
|
|
|
// record all the vars that existed before we did the first pass through the loop
|
|
|
|
$pre_loop_context = clone $loop_context;
|
|
|
|
$pre_outer_context = clone $outer_context;
|
|
|
|
|
|
|
|
IssueBuffer::startRecording();
|
|
|
|
$this->analyze($stmts, $loop_context, $outer_context);
|
|
|
|
$recorded_issues = IssueBuffer::clearRecordingLevel();
|
|
|
|
IssueBuffer::stopRecording();
|
|
|
|
|
|
|
|
for ($i = 0; $i < $assignment_depth; $i++) {
|
2017-03-13 23:06:56 +01:00
|
|
|
$vars_to_remove = [];
|
|
|
|
|
2017-03-15 01:14:25 +01:00
|
|
|
$has_changes = false;
|
|
|
|
|
2017-03-13 23:06:56 +01:00
|
|
|
foreach ($loop_context->vars_in_scope as $var_id => $type) {
|
2017-03-15 01:14:25 +01:00
|
|
|
if (in_array($var_id, $asserted_vars)) {
|
|
|
|
// set the vars to whatever the while/foreach loop expects them to be
|
|
|
|
if ((string)$type !== (string)$pre_loop_context->vars_in_scope[$var_id]) {
|
|
|
|
$loop_context->vars_in_scope[$var_id] = $pre_loop_context->vars_in_scope[$var_id];
|
|
|
|
$has_changes = true;
|
|
|
|
}
|
|
|
|
} elseif (isset($pre_outer_context->vars_in_scope[$var_id])) {
|
|
|
|
$pre_outer = (string)$pre_outer_context->vars_in_scope[$var_id];
|
|
|
|
|
|
|
|
if ((string)$type !== $pre_outer ||
|
|
|
|
(string)$outer_context->vars_in_scope[$var_id] !== $pre_outer
|
|
|
|
) {
|
|
|
|
$has_changes = true;
|
2017-03-13 23:06:56 +01:00
|
|
|
|
2017-03-15 01:14:25 +01:00
|
|
|
// widen the foreach context type with the initial context type
|
2017-03-13 23:06:56 +01:00
|
|
|
$loop_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
|
|
|
$loop_context->vars_in_scope[$var_id],
|
|
|
|
$outer_context->vars_in_scope[$var_id]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$vars_to_remove[] = $var_id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-15 16:38:23 +01:00
|
|
|
foreach ($asserted_vars as $var_id) {
|
|
|
|
if (!isset($loop_context->vars_in_scope[$var_id])) {
|
|
|
|
$loop_context->vars_in_scope[$var_id] = $pre_loop_context->vars_in_scope[$var_id];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-15 01:14:25 +01:00
|
|
|
// if there are no changes to the types, no need to re-examine
|
|
|
|
if (!$has_changes) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2017-03-13 23:06:56 +01:00
|
|
|
// remove vars that were defined in the foreach
|
|
|
|
foreach ($vars_to_remove as $var_id) {
|
|
|
|
unset($loop_context->vars_in_scope[$var_id]);
|
|
|
|
}
|
|
|
|
|
2017-03-16 16:46:07 +01:00
|
|
|
$loop_context->clauses = $pre_loop_context->clauses;
|
|
|
|
|
2017-03-13 23:06:56 +01:00
|
|
|
IssueBuffer::startRecording();
|
|
|
|
$this->analyze($stmts, $loop_context, $outer_context);
|
|
|
|
$recorded_issues = IssueBuffer::clearRecordingLevel();
|
|
|
|
IssueBuffer::stopRecording();
|
2017-03-15 01:14:25 +01:00
|
|
|
}
|
2017-03-13 23:06:56 +01:00
|
|
|
|
|
|
|
if ($recorded_issues) {
|
|
|
|
foreach ($recorded_issues as $recorded_issue) {
|
|
|
|
// if we're not in any loops then this will just result in the issue being emitted
|
|
|
|
IssueBuffer::bubbleUp($recorded_issue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-15 01:14:25 +01:00
|
|
|
/**
|
|
|
|
* @param string $first_var_id
|
|
|
|
* @param array<string, array<string, bool>> $assignment_map
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
private static function getAssignmentMapDepth($first_var_id, array $assignment_map)
|
|
|
|
{
|
|
|
|
$max_depth = 0;
|
|
|
|
|
|
|
|
$assignment_var_ids = $assignment_map[$first_var_id];
|
|
|
|
unset($assignment_map[$first_var_id]);
|
|
|
|
|
|
|
|
foreach ($assignment_var_ids as $assignment_var_id => $_) {
|
|
|
|
$depth = 1;
|
|
|
|
|
|
|
|
if (isset($assignment_map[$assignment_var_id])) {
|
|
|
|
$depth = 1 + self::getAssignmentMapDepth($assignment_var_id, $assignment_map);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($depth > $max_depth) {
|
|
|
|
$max_depth = $depth;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $max_depth;
|
|
|
|
}
|
|
|
|
|
2016-06-17 00:52:12 +02:00
|
|
|
/**
|
2016-11-02 07:29:00 +01:00
|
|
|
* @param PhpParser\Node\Stmt\Static_ $stmt
|
|
|
|
* @param Context $context
|
|
|
|
* @return false|null
|
2016-06-17 00:52:12 +02:00
|
|
|
*/
|
2017-02-12 00:56:38 +01:00
|
|
|
private function analyzeStatic(PhpParser\Node\Stmt\Static_ $stmt, Context $context)
|
2016-01-20 00:27:06 +01:00
|
|
|
{
|
2016-10-22 19:23:18 +02:00
|
|
|
foreach ($stmt->vars as $var) {
|
|
|
|
if ($var->default) {
|
2017-01-07 21:09:47 +01:00
|
|
|
if (ExpressionChecker::analyze($this, $var->default, $context) === false) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2016-06-20 06:38:13 +02:00
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
if ($context->check_variables) {
|
2017-03-15 20:21:00 +01:00
|
|
|
$context->vars_in_scope['$' . $var->name] = Type::getMixed();
|
2016-10-22 19:23:18 +02:00
|
|
|
$context->vars_possibly_in_scope['$' . $var->name] = true;
|
2017-02-08 00:09:12 +01:00
|
|
|
$this->registerVariable('$' . $var->name, new CodeLocation($this, $stmt));
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-06-06 02:25:16 +02:00
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
|
|
|
|
return null;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-01-20 00:27:06 +01:00
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
/**
|
2016-11-02 07:29:00 +01:00
|
|
|
* @param PhpParser\Node\Expr $stmt
|
|
|
|
* @return Type\Union|null
|
2016-10-22 19:23:18 +02:00
|
|
|
*/
|
|
|
|
public static function getSimpleType(PhpParser\Node\Expr $stmt)
|
|
|
|
{
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\ConstFetch) {
|
2017-01-19 07:12:19 +01:00
|
|
|
if (strtolower($stmt->name->parts[0]) === 'false') {
|
|
|
|
return Type::getFalse();
|
|
|
|
} elseif (strtolower($stmt->name->parts[0]) === 'true') {
|
2017-01-13 18:26:10 +01:00
|
|
|
return Type::getBool();
|
|
|
|
} elseif (strtolower($stmt->name->parts[0]) === 'null') {
|
|
|
|
return Type::getNull();
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr\ClassConstFetch) {
|
2016-10-22 19:23:18 +02:00
|
|
|
// @todo support this as well
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Scalar\String_) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getString();
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Scalar\LNumber) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getInt();
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Scalar\DNumber) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getFloat();
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr\Array_) {
|
2017-01-02 07:07:44 +01:00
|
|
|
if (count($stmt->items) === 0) {
|
|
|
|
return Type::getEmptyArray();
|
|
|
|
}
|
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getArray();
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr\Cast\Int_) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getInt();
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr\Cast\Double) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getFloat();
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr\Cast\Bool_) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getBool();
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr\Cast\String_) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getString();
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr\Cast\Object_) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getObject();
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr\Cast\Array_) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getArray();
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr\UnaryMinus || $stmt instanceof PhpParser\Node\Expr\UnaryPlus) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return self::getSimpleType($stmt->expr);
|
2016-06-28 21:28:05 +02:00
|
|
|
}
|
2016-10-30 17:46:18 +01:00
|
|
|
|
|
|
|
return null;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-01-20 00:27:06 +01:00
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
/**
|
2016-11-02 07:29:00 +01:00
|
|
|
* @param PhpParser\Node\Stmt\Do_ $stmt
|
|
|
|
* @param Context $context
|
|
|
|
* @return false|null
|
2016-10-22 19:23:18 +02:00
|
|
|
*/
|
2017-02-12 00:56:38 +01:00
|
|
|
private function analyzeDo(PhpParser\Node\Stmt\Do_ $stmt, Context $context)
|
2016-10-22 19:23:18 +02:00
|
|
|
{
|
2017-02-18 23:49:34 +01:00
|
|
|
$do_context = clone $context;
|
|
|
|
|
2017-03-15 01:14:25 +01:00
|
|
|
if ($this->analyzeLoop($stmt->stmts, [], $do_context, $context) === false) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return false;
|
2016-01-20 00:27:06 +01:00
|
|
|
}
|
2016-02-10 21:20:53 +01:00
|
|
|
|
2017-02-18 23:49:34 +01:00
|
|
|
foreach ($context->vars_in_scope as $var => $type) {
|
|
|
|
if ($type->isMixed()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($do_context->hasVariable($var)) {
|
|
|
|
if ($do_context->vars_in_scope[$var]->isMixed()) {
|
|
|
|
$context->vars_in_scope[$var] = $do_context->vars_in_scope[$var];
|
|
|
|
}
|
|
|
|
|
|
|
|
if ((string)$do_context->vars_in_scope[$var] !== (string)$type) {
|
|
|
|
$context->vars_in_scope[$var] = Type::combineUnionTypes($do_context->vars_in_scope[$var], $type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-13 23:06:56 +01:00
|
|
|
foreach ($do_context->vars_in_scope as $var_id => $type) {
|
|
|
|
if (!isset($context->vars_in_scope[$var_id])) {
|
|
|
|
$context->vars_in_scope[$var_id] = $type;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-21 23:48:12 +01:00
|
|
|
$context->vars_possibly_in_scope = array_merge(
|
|
|
|
$context->vars_possibly_in_scope,
|
|
|
|
$do_context->vars_possibly_in_scope
|
|
|
|
);
|
|
|
|
|
2017-02-27 05:09:18 +01:00
|
|
|
if ($context->collect_references) {
|
|
|
|
$context->referenced_vars = array_merge(
|
|
|
|
$context->referenced_vars,
|
|
|
|
$do_context->referenced_vars
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-01-07 21:09:47 +01:00
|
|
|
return ExpressionChecker::analyze($this, $stmt->cond, $context);
|
2016-01-20 00:27:06 +01:00
|
|
|
}
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Stmt\Const_ $stmt
|
|
|
|
* @param Context $context
|
|
|
|
* @return void
|
|
|
|
*/
|
2017-02-12 00:56:38 +01:00
|
|
|
private function analyzeConstAssignment(PhpParser\Node\Stmt\Const_ $stmt, Context $context)
|
2016-10-22 19:23:18 +02:00
|
|
|
{
|
|
|
|
foreach ($stmt->consts as $const) {
|
2017-01-07 21:09:47 +01:00
|
|
|
ExpressionChecker::analyze($this, $const->value, $context);
|
2016-10-19 03:54:08 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
$this->setConstType(
|
|
|
|
$const->name,
|
2017-01-02 21:31:18 +01:00
|
|
|
isset($const->value->inferredType) ? $const->value->inferredType : Type::getMixed(),
|
|
|
|
$context
|
2016-11-02 07:29:00 +01:00
|
|
|
);
|
2016-08-11 23:36:22 +02:00
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-08-11 23:36:22 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @param string $const_name
|
2016-11-21 03:49:06 +01:00
|
|
|
* @param bool $is_fully_qualified
|
2017-01-02 21:31:18 +01:00
|
|
|
* @param Context $context
|
2016-11-05 02:14:04 +01:00
|
|
|
* @return Type\Union|null
|
2016-11-02 07:29:00 +01:00
|
|
|
*/
|
2017-01-02 21:31:18 +01:00
|
|
|
public function getConstType($const_name, $is_fully_qualified, Context $context)
|
2016-10-22 19:23:18 +02:00
|
|
|
{
|
2016-11-21 03:49:06 +01:00
|
|
|
$fq_const_name = null;
|
|
|
|
|
2017-01-07 20:35:07 +01:00
|
|
|
$aliased_constants = $this->getAliasedConstants();
|
|
|
|
|
|
|
|
if (isset($aliased_constants[$const_name])) {
|
|
|
|
$fq_const_name = $aliased_constants[$const_name];
|
|
|
|
} elseif ($is_fully_qualified) {
|
2016-11-21 03:49:06 +01:00
|
|
|
$fq_const_name = $const_name;
|
2017-01-07 20:35:07 +01:00
|
|
|
} elseif (strpos($const_name, '\\')) {
|
|
|
|
$fq_const_name = ClassLikeChecker::getFQCLNFromString($const_name, $this);
|
2016-11-21 03:49:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($fq_const_name) {
|
|
|
|
$const_name_parts = explode('\\', $fq_const_name);
|
|
|
|
$const_name = array_pop($const_name_parts);
|
|
|
|
$namespace_name = implode('\\', $const_name_parts);
|
2017-01-07 20:35:07 +01:00
|
|
|
$namespace_constants = NamespaceChecker::getConstantsForNamespace(
|
|
|
|
$namespace_name,
|
|
|
|
\ReflectionProperty::IS_PUBLIC
|
|
|
|
);
|
2016-11-21 03:49:06 +01:00
|
|
|
|
|
|
|
if (isset($namespace_constants[$const_name])) {
|
|
|
|
return $namespace_constants[$const_name];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-01 05:24:33 +01:00
|
|
|
if ($context->hasVariable($const_name)) {
|
2017-01-02 21:31:18 +01:00
|
|
|
return $context->vars_in_scope[$const_name];
|
2016-11-21 03:49:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$predefined_constants = Config::getInstance()->getPredefinedConstants();
|
|
|
|
|
2016-11-21 04:40:19 +01:00
|
|
|
if (isset($predefined_constants[$fq_const_name ?: $const_name])) {
|
|
|
|
return ClassLikeChecker::getTypeFromValue($predefined_constants[$fq_const_name ?: $const_name]);
|
2016-11-21 03:49:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-08-10 00:10:46 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @param string $const_name
|
|
|
|
* @param Type\Union $const_type
|
2017-01-02 21:31:18 +01:00
|
|
|
* @param Context $context
|
2016-11-02 07:29:00 +01:00
|
|
|
* @return void
|
|
|
|
*/
|
2017-01-02 21:31:18 +01:00
|
|
|
public function setConstType($const_name, Type\Union $const_type, Context $context)
|
2016-10-22 19:23:18 +02:00
|
|
|
{
|
2017-01-15 21:58:40 +01:00
|
|
|
$context->vars_in_scope[$const_name] = $const_type;
|
|
|
|
$context->constants[$const_name] = $const_type;
|
|
|
|
|
2016-11-21 03:49:06 +01:00
|
|
|
if ($this->source instanceof NamespaceChecker) {
|
|
|
|
$this->source->setConstType($const_name, $const_type);
|
2017-01-02 21:31:18 +01:00
|
|
|
} else {
|
2017-01-07 20:35:07 +01:00
|
|
|
self::$user_constants[$this->getFilePath()][$const_name] = $const_type;
|
2016-11-21 03:49:06 +01:00
|
|
|
}
|
2016-01-20 00:27:06 +01:00
|
|
|
}
|
|
|
|
|
2016-06-17 22:58:15 +02:00
|
|
|
/**
|
2016-10-22 19:23:18 +02:00
|
|
|
* @param PhpParser\Node\Stmt\Return_ $stmt
|
|
|
|
* @param Context $context
|
2016-06-17 22:58:15 +02:00
|
|
|
* @return false|null
|
|
|
|
*/
|
2017-02-12 00:56:38 +01:00
|
|
|
private function analyzeReturn(PhpParser\Node\Stmt\Return_ $stmt, Context $context)
|
2016-10-22 19:23:18 +02:00
|
|
|
{
|
2017-03-02 04:27:52 +01:00
|
|
|
$doc_comment_text = (string)$stmt->getDocComment();
|
|
|
|
|
|
|
|
if ($doc_comment_text) {
|
|
|
|
$type_in_comments = CommentChecker::getTypeFromComment(
|
|
|
|
$doc_comment_text,
|
|
|
|
$context,
|
|
|
|
$this->source
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$type_in_comments = null;
|
|
|
|
}
|
2016-06-17 22:58:15 +02:00
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
if ($stmt->expr) {
|
2017-01-07 21:09:47 +01:00
|
|
|
if (ExpressionChecker::analyze($this, $stmt->expr, $context) === false) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return false;
|
|
|
|
}
|
2016-10-03 04:00:42 +02:00
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
if ($type_in_comments) {
|
|
|
|
$stmt->inferredType = $type_in_comments;
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif (isset($stmt->expr->inferredType)) {
|
2016-10-22 19:23:18 +02:00
|
|
|
$stmt->inferredType = $stmt->expr->inferredType;
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2016-10-22 19:23:18 +02:00
|
|
|
$stmt->inferredType = Type::getMixed();
|
2016-09-21 03:45:49 +02:00
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2016-10-22 19:23:18 +02:00
|
|
|
$stmt->inferredType = Type::getVoid();
|
2016-09-21 03:45:49 +02:00
|
|
|
}
|
2016-06-17 22:58:15 +02:00
|
|
|
|
2017-01-12 03:37:53 +01:00
|
|
|
|
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
if ($this->source instanceof FunctionLikeChecker) {
|
|
|
|
$this->source->addReturnTypes($stmt->expr ? (string) $stmt->inferredType : '', $context);
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
|
|
|
|
return null;
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-09-21 03:45:49 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Stmt\Throw_ $stmt
|
|
|
|
* @param Context $context
|
|
|
|
* @return false|null
|
|
|
|
*/
|
2017-02-12 00:56:38 +01:00
|
|
|
private function analyzeThrow(PhpParser\Node\Stmt\Throw_ $stmt, Context $context)
|
2016-10-22 19:23:18 +02:00
|
|
|
{
|
2017-01-07 21:09:47 +01:00
|
|
|
return ExpressionChecker::analyze($this, $stmt->expr, $context);
|
2016-10-22 19:23:18 +02:00
|
|
|
}
|
2016-09-21 03:45:49 +02:00
|
|
|
|
2016-10-09 23:54:58 +02:00
|
|
|
/**
|
2017-02-08 00:09:12 +01:00
|
|
|
* @param string $var_name
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function hasVariable($var_name)
|
|
|
|
{
|
|
|
|
return isset($this->all_vars[$var_name]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $var_name
|
|
|
|
* @param CodeLocation $location
|
2016-10-09 23:54:58 +02:00
|
|
|
* @return void
|
|
|
|
*/
|
2017-02-08 00:09:12 +01:00
|
|
|
public function registerVariable($var_name, CodeLocation $location)
|
2016-01-20 00:27:06 +01:00
|
|
|
{
|
2017-02-08 00:09:12 +01:00
|
|
|
$this->all_vars[$var_name] = $location;
|
2016-01-20 00:27:06 +01:00
|
|
|
}
|
|
|
|
|
2016-10-09 23:54:58 +02:00
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Expr\Include_ $stmt
|
|
|
|
* @param Context $context
|
|
|
|
* @return false|null
|
|
|
|
*/
|
2017-01-07 21:09:47 +01:00
|
|
|
public function analyzeInclude(PhpParser\Node\Expr\Include_ $stmt, Context $context)
|
2016-07-25 21:05:58 +02:00
|
|
|
{
|
2016-12-06 22:41:42 +01:00
|
|
|
$config = Config::getInstance();
|
|
|
|
|
|
|
|
if (!$config->allow_includes) {
|
|
|
|
throw new FileIncludeException('File includes are not allowed per your Psalm config - check the allowFileIncludes flag.');
|
|
|
|
}
|
|
|
|
|
2017-01-07 21:09:47 +01:00
|
|
|
if (ExpressionChecker::analyze($this, $stmt->expr, $context) === false) {
|
2016-07-25 21:05:58 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$path_to_file = null;
|
|
|
|
|
|
|
|
if ($stmt->expr instanceof PhpParser\Node\Scalar\String_) {
|
|
|
|
$path_to_file = $stmt->expr->value;
|
|
|
|
|
|
|
|
// attempts to resolve using get_include_path dirs
|
2017-01-07 20:35:07 +01:00
|
|
|
$include_path = self::resolveIncludePath($path_to_file, dirname($this->getCheckedFileName()));
|
2016-07-25 21:05:58 +02:00
|
|
|
$path_to_file = $include_path ? $include_path : $path_to_file;
|
|
|
|
|
2017-01-18 04:10:21 +01:00
|
|
|
if ($path_to_file[0] !== DIRECTORY_SEPARATOR) {
|
|
|
|
$path_to_file = getcwd() . DIRECTORY_SEPARATOR . $path_to_file;
|
2016-07-25 21:05:58 +02:00
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2017-01-08 01:33:33 +01:00
|
|
|
$path_to_file = self::getPathTo($stmt->expr, $this->getFileName());
|
2016-07-25 21:05:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($path_to_file) {
|
|
|
|
$reduce_pattern = '/\/[^\/]+\/\.\.\//';
|
|
|
|
|
|
|
|
while (preg_match($reduce_pattern, $path_to_file)) {
|
2017-01-18 04:10:21 +01:00
|
|
|
$path_to_file = preg_replace($reduce_pattern, DIRECTORY_SEPARATOR, $path_to_file);
|
2016-07-25 21:05:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// if the file is already included, we can't check much more
|
|
|
|
if (in_array($path_to_file, get_included_files())) {
|
2016-11-02 07:29:00 +01:00
|
|
|
return null;
|
2016-07-25 21:05:58 +02:00
|
|
|
}
|
|
|
|
|
2017-01-08 01:07:58 +01:00
|
|
|
$current_file_checker = $this->getFileChecker();
|
|
|
|
|
|
|
|
if ($this->getFileChecker()->fileExists($path_to_file)) {
|
2017-02-18 19:41:27 +01:00
|
|
|
$include_stmts = \Psalm\Provider\FileProvider::getStatementsForFile(
|
|
|
|
$current_file_checker->project_checker,
|
|
|
|
$path_to_file
|
|
|
|
);
|
2017-01-19 05:35:23 +01:00
|
|
|
|
|
|
|
if (is_subclass_of($current_file_checker, 'Psalm\\Checker\\FileChecker')) {
|
|
|
|
$this->analyze($include_stmts, $context);
|
|
|
|
} else {
|
|
|
|
$include_file_checker = new FileChecker(
|
|
|
|
$path_to_file,
|
|
|
|
$current_file_checker->project_checker,
|
|
|
|
$include_stmts
|
|
|
|
);
|
|
|
|
$include_file_checker->setFileName($this->getFileName(), $this->getFilePath());
|
|
|
|
$include_file_checker->visit($context);
|
|
|
|
$include_file_checker->analyze();
|
|
|
|
}
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
return null;
|
2016-07-25 21:05:58 +02:00
|
|
|
}
|
|
|
|
}
|
2016-08-14 06:38:29 +02:00
|
|
|
|
2016-10-18 22:14:52 +02:00
|
|
|
$context->check_classes = false;
|
|
|
|
$context->check_variables = false;
|
|
|
|
$context->check_functions = false;
|
2016-11-02 07:29:00 +01:00
|
|
|
return null;
|
2016-07-25 21:05:58 +02:00
|
|
|
}
|
|
|
|
|
2016-10-09 23:54:58 +02:00
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Expr $stmt
|
|
|
|
* @param string $file_name
|
|
|
|
* @return string|null
|
2016-12-17 06:48:31 +01:00
|
|
|
* @psalm-suppress MixedAssignment
|
2016-10-09 23:54:58 +02:00
|
|
|
*/
|
2016-08-11 00:10:12 +02:00
|
|
|
protected static function getPathTo(PhpParser\Node\Expr $stmt, $file_name)
|
2016-03-23 18:05:25 +01:00
|
|
|
{
|
2017-01-18 04:10:21 +01:00
|
|
|
if ($file_name[0] !== DIRECTORY_SEPARATOR) {
|
|
|
|
$file_name = getcwd() . DIRECTORY_SEPARATOR . $file_name;
|
2016-03-23 18:05:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Scalar\String_) {
|
|
|
|
return $stmt->value;
|
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat) {
|
2016-08-11 00:10:12 +02:00
|
|
|
$left_string = self::getPathTo($stmt->left, $file_name);
|
|
|
|
$right_string = self::getPathTo($stmt->right, $file_name);
|
2016-03-23 18:05:25 +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 &&
|
2016-11-02 07:29:00 +01:00
|
|
|
$stmt->name->parts === ['dirname']
|
|
|
|
) {
|
2016-03-23 18:05:25 +01:00
|
|
|
if ($stmt->args) {
|
2016-08-11 00:10:12 +02:00
|
|
|
$evaled_path = self::getPathTo($stmt->args[0]->value, $file_name);
|
2016-03-23 18:05:25 +01:00
|
|
|
|
|
|
|
if (!$evaled_path) {
|
2016-11-02 07:29:00 +01:00
|
|
|
return null;
|
2016-03-23 18:05:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return dirname($evaled_path);
|
|
|
|
}
|
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr\ConstFetch && $stmt->name instanceof PhpParser\Node\Name) {
|
|
|
|
$const_name = implode('', $stmt->name->parts);
|
|
|
|
|
|
|
|
if (defined($const_name)) {
|
2016-11-05 22:53:30 +01:00
|
|
|
$constant_value = constant($const_name);
|
|
|
|
|
|
|
|
if (is_string($constant_value)) {
|
|
|
|
return $constant_value;
|
|
|
|
}
|
2016-03-23 18:05:25 +01:00
|
|
|
}
|
|
|
|
} 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;
|
|
|
|
}
|
|
|
|
|
2016-04-27 00:42:48 +02:00
|
|
|
/**
|
2016-06-20 07:05:44 +02:00
|
|
|
* @return string|null
|
2016-04-27 00:42:48 +02:00
|
|
|
*/
|
2016-11-02 07:29:00 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $file_name
|
|
|
|
* @param string $current_directory
|
|
|
|
* @return string|null
|
|
|
|
*/
|
2016-08-11 00:10:12 +02:00
|
|
|
protected static function resolveIncludePath($file_name, $current_directory)
|
2016-03-23 18:05:25 +01:00
|
|
|
{
|
2017-01-08 01:07:58 +01:00
|
|
|
if (!$current_directory) {
|
|
|
|
return $file_name;
|
|
|
|
}
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
$paths = PATH_SEPARATOR == ':'
|
|
|
|
? preg_split('#(?<!phar):#', get_include_path())
|
|
|
|
: explode(PATH_SEPARATOR, get_include_path());
|
2016-03-23 18:05:25 +01:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
|
|
|
|
return null;
|
2016-03-23 18:05:25 +01:00
|
|
|
}
|
2016-04-01 16:52:43 +02:00
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
/**
|
|
|
|
* The first appearance of the variable in this set of statements being evaluated
|
2016-11-02 07:29:00 +01:00
|
|
|
*
|
2016-10-22 19:23:18 +02:00
|
|
|
* @param string $var_name
|
2017-02-08 00:09:12 +01:00
|
|
|
* @return CodeLocation|null
|
2016-10-22 19:23:18 +02:00
|
|
|
*/
|
|
|
|
public function getFirstAppearance($var_name)
|
2016-10-02 19:05:49 +02:00
|
|
|
{
|
2016-10-22 19:23:18 +02:00
|
|
|
return isset($this->all_vars[$var_name]) ? $this->all_vars[$var_name] : null;
|
2016-10-02 19:05:49 +02:00
|
|
|
}
|
2016-10-21 00:16:17 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @return void
|
|
|
|
*/
|
2016-10-21 00:16:17 +02:00
|
|
|
public static function clearCache()
|
|
|
|
{
|
|
|
|
self::$user_constants = [];
|
2016-10-22 23:35:59 +02:00
|
|
|
|
|
|
|
ExpressionChecker::clearCache();
|
2016-10-21 00:16:17 +02:00
|
|
|
}
|
2016-01-20 16:57:56 +01:00
|
|
|
}
|