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;
|
2018-01-14 18:09:40 +01:00
|
|
|
use Psalm\Checker\Statements\Block\DoChecker;
|
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;
|
2018-01-14 18:09:40 +01:00
|
|
|
use Psalm\Checker\Statements\Expression\Assignment\PropertyAssignmentChecker;
|
|
|
|
use Psalm\Checker\Statements\Expression\BinaryOpChecker;
|
2016-12-09 18:48:02 +01:00
|
|
|
use Psalm\Checker\Statements\Expression\CallChecker;
|
2017-05-19 06:48:26 +02:00
|
|
|
use Psalm\Checker\Statements\ExpressionChecker;
|
2018-01-14 18:09:40 +01:00
|
|
|
use Psalm\Checker\Statements\ReturnChecker;
|
|
|
|
use Psalm\Checker\Statements\ThrowChecker;
|
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;
|
2017-11-15 03:43:31 +01:00
|
|
|
use Psalm\Exception\DocblockParseException;
|
2018-01-21 22:24:20 +01:00
|
|
|
use Psalm\FileManipulation\FileManipulation;
|
2017-11-07 20:46:53 +01:00
|
|
|
use Psalm\FileManipulation\FileManipulationBuffer;
|
2016-10-17 22:42:23 +02:00
|
|
|
use Psalm\Issue\ContinueOutsideLoop;
|
2017-11-15 03:43:31 +01:00
|
|
|
use Psalm\Issue\InvalidDocblock;
|
2016-11-05 22:53:30 +01:00
|
|
|
use Psalm\Issue\InvalidGlobal;
|
2017-02-12 22:49:32 +01:00
|
|
|
use Psalm\Issue\UnevaluatedCode;
|
2016-11-06 01:53:39 +01:00
|
|
|
use Psalm\Issue\UnrecognizedStatement;
|
2018-01-25 07:04:26 +01:00
|
|
|
use Psalm\Issue\UnusedVariable;
|
2016-11-02 07:29:00 +01:00
|
|
|
use Psalm\IssueBuffer;
|
2017-12-03 00:28:18 +01:00
|
|
|
use Psalm\Scope\LoopScope;
|
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
|
|
|
|
2017-07-29 21:05:06 +02:00
|
|
|
/**
|
|
|
|
* @var FileChecker
|
|
|
|
*/
|
|
|
|
protected $file_checker;
|
|
|
|
|
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
|
|
|
|
2018-01-21 22:24:20 +01:00
|
|
|
/**
|
|
|
|
* @var array<string, int>
|
|
|
|
*/
|
|
|
|
private $var_branch_points = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Possibly undefined variables should be initialised if we're altering code
|
|
|
|
*
|
|
|
|
* @var array<string, int>|null
|
|
|
|
*/
|
|
|
|
private $vars_to_initialize;
|
|
|
|
|
2017-08-12 01:05:04 +02:00
|
|
|
/**
|
|
|
|
* @var array<string, FunctionChecker>
|
|
|
|
*/
|
|
|
|
private $function_checkers = [];
|
|
|
|
|
2018-01-28 23:28:34 +01:00
|
|
|
/**
|
|
|
|
* @var array<string, array{0: string, 1: CodeLocation}>
|
|
|
|
*/
|
|
|
|
private $unused_var_locations = [];
|
|
|
|
|
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;
|
2017-03-20 04:30:20 +01:00
|
|
|
$this->file_checker = $source->getFileChecker();
|
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 $global_context
|
2018-01-21 22:24:20 +01:00
|
|
|
* @param bool $root_scope
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
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,
|
2017-12-03 00:28:18 +01:00
|
|
|
LoopScope $loop_scope = null,
|
2018-01-21 22:24:20 +01:00
|
|
|
Context $global_context = null,
|
|
|
|
$root_scope = false
|
2017-01-07 20:35:07 +01:00
|
|
|
) {
|
2016-01-20 00:27:06 +01:00
|
|
|
$has_returned = false;
|
|
|
|
|
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);
|
2017-08-14 21:46:01 +02:00
|
|
|
$this->function_checkers[strtolower($stmt->name)] = $function_checker;
|
2016-08-15 06:58:30 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-07-29 21:05:06 +02:00
|
|
|
$project_checker = $this->getFileChecker()->project_checker;
|
2018-01-21 19:38:51 +01:00
|
|
|
$codebase = $project_checker->codebase;
|
2017-07-29 21:05:06 +02:00
|
|
|
|
2017-12-03 00:28:18 +01:00
|
|
|
$original_context = null;
|
|
|
|
|
|
|
|
if ($loop_scope) {
|
|
|
|
$original_context = clone $context;
|
|
|
|
}
|
|
|
|
|
2018-02-12 04:49:19 +01:00
|
|
|
$plugin_classes = $codebase->config->after_statement_checks;
|
2016-12-06 22:41:42 +01:00
|
|
|
|
2017-11-07 20:46:53 +01:00
|
|
|
foreach ($stmts as $stmt) {
|
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-12-03 00:28:18 +01:00
|
|
|
if (isset($context->vars_in_scope['$tag']) && !$stmt instanceof PhpParser\Node\Stmt\Nop) {
|
|
|
|
var_dump($stmt->getLine() . ' ' . $context->vars_in_scope['$tag']);
|
2016-10-09 23:54:58 +02:00
|
|
|
}
|
2017-01-26 01:09:31 +01:00
|
|
|
*/
|
2016-10-09 23:54:58 +02:00
|
|
|
|
2017-10-27 00:19:19 +02:00
|
|
|
$new_issues = null;
|
|
|
|
|
|
|
|
if ($docblock = $stmt->getDocComment()) {
|
|
|
|
$comments = CommentChecker::parseDocComment((string)$docblock);
|
|
|
|
if (isset($comments['specials']['psalm-suppress'])) {
|
|
|
|
$suppressed = array_filter(
|
|
|
|
array_map(
|
|
|
|
/**
|
|
|
|
* @param string $line
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
function ($line) {
|
|
|
|
return explode(' ', trim($line))[0];
|
|
|
|
},
|
|
|
|
$comments['specials']['psalm-suppress']
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($suppressed) {
|
|
|
|
$new_issues = array_diff($suppressed, $this->source->getSuppressedIssues());
|
2017-11-19 19:42:48 +01:00
|
|
|
/** @psalm-suppress MixedTypeCoercion */
|
2017-10-27 00:19:19 +02:00
|
|
|
$this->addSuppressedIssues($new_issues);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-01-20 00:27:06 +01:00
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
|
2018-03-03 19:20:41 +01:00
|
|
|
if (IfChecker::analyze($this, $stmt, $context, $loop_scope) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
|
2018-03-03 19:20:41 +01:00
|
|
|
if (TryChecker::analyze($this, $stmt, $context, $loop_scope) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\For_) {
|
2018-03-03 19:20:41 +01:00
|
|
|
if (ForChecker::analyze($this, $stmt, $context) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Foreach_) {
|
2018-03-03 19:20:41 +01:00
|
|
|
if (ForeachChecker::analyze($this, $stmt, $context) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\While_) {
|
2018-03-03 19:20:41 +01:00
|
|
|
if (WhileChecker::analyze($this, $stmt, $context) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Do_) {
|
2018-03-03 19:21:52 +01:00
|
|
|
DoChecker::analyze($this, $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_) {
|
2018-02-10 16:30:08 +01:00
|
|
|
$context->inside_unset = true;
|
2017-12-14 01:46:58 +01:00
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
2017-12-14 01:46:58 +01:00
|
|
|
|
2018-02-10 16:30:08 +01:00
|
|
|
$context->inside_unset = false;
|
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;
|
2018-01-14 18:09:40 +01:00
|
|
|
ReturnChecker::analyze($this, $project_checker, $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;
|
2018-01-14 18:09:40 +01:00
|
|
|
ThrowChecker::analyze($this, $stmt, $context);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
|
2017-12-03 00:28:18 +01:00
|
|
|
SwitchChecker::analyze($this, $stmt, $context, $loop_scope);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Break_) {
|
2017-12-03 00:28:18 +01:00
|
|
|
if ($loop_scope && $original_context) {
|
|
|
|
$loop_scope->final_actions[] = ScopeChecker::ACTION_BREAK;
|
|
|
|
|
|
|
|
$redefined_vars = $context->getRedefinedVars($loop_scope->loop_parent_context->vars_in_scope);
|
|
|
|
|
|
|
|
if ($loop_scope->possibly_redefined_loop_parent_vars === null) {
|
|
|
|
$loop_scope->possibly_redefined_loop_parent_vars = $redefined_vars;
|
|
|
|
} else {
|
|
|
|
foreach ($redefined_vars as $var => $type) {
|
|
|
|
if ($type->isMixed()) {
|
|
|
|
$loop_scope->possibly_redefined_loop_parent_vars[$var] = $type;
|
|
|
|
} elseif (isset($loop_scope->possibly_redefined_loop_parent_vars[$var])) {
|
|
|
|
$loop_scope->possibly_redefined_loop_parent_vars[$var] = Type::combineUnionTypes(
|
|
|
|
$type,
|
|
|
|
$loop_scope->possibly_redefined_loop_parent_vars[$var]
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$loop_scope->possibly_redefined_loop_parent_vars[$var] = $type;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$has_returned = true;
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
|
2017-12-03 00:28:18 +01:00
|
|
|
if ($loop_scope === null) {
|
2018-01-24 06:01:08 +01:00
|
|
|
if (!$context->inside_case) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new ContinueOutsideLoop(
|
|
|
|
'Continue call outside loop context',
|
|
|
|
new CodeLocation($this->source, $stmt)
|
|
|
|
),
|
|
|
|
$this->source->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-10-17 22:42:23 +02:00
|
|
|
}
|
2017-12-03 00:28:18 +01:00
|
|
|
} elseif ($original_context) {
|
|
|
|
$loop_scope->final_actions[] = ScopeChecker::ACTION_CONTINUE;
|
|
|
|
|
|
|
|
$redefined_vars = $context->getRedefinedVars($original_context->vars_in_scope);
|
|
|
|
|
|
|
|
if ($loop_scope->redefined_loop_vars === null) {
|
|
|
|
$loop_scope->redefined_loop_vars = $redefined_vars;
|
|
|
|
} else {
|
|
|
|
foreach ($loop_scope->redefined_loop_vars as $redefined_var => $type) {
|
|
|
|
if (!isset($redefined_vars[$redefined_var])) {
|
|
|
|
unset($loop_scope->redefined_loop_vars[$redefined_var]);
|
|
|
|
} else {
|
|
|
|
$loop_scope->redefined_loop_vars[$redefined_var] = Type::combineUnionTypes(
|
|
|
|
$redefined_vars[$redefined_var],
|
|
|
|
$type
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-06 19:40:28 +01:00
|
|
|
foreach ($redefined_vars as $var => $type) {
|
|
|
|
if ($type->isMixed()) {
|
|
|
|
$loop_scope->possibly_redefined_loop_vars[$var] = $type;
|
|
|
|
} elseif (isset($loop_scope->possibly_redefined_loop_vars[$var])) {
|
|
|
|
$loop_scope->possibly_redefined_loop_vars[$var] = Type::combineUnionTypes(
|
|
|
|
$type,
|
|
|
|
$loop_scope->possibly_redefined_loop_vars[$var]
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$loop_scope->possibly_redefined_loop_vars[$var] = $type;
|
2017-12-03 00:28:18 +01:00
|
|
|
}
|
|
|
|
}
|
2016-10-17 22:42:23 +02:00
|
|
|
}
|
|
|
|
|
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,
|
2017-12-11 03:14:30 +01:00
|
|
|
new CodeLocation($this->getSource(), $expr),
|
|
|
|
$expr,
|
|
|
|
$context
|
2016-12-09 18:48:02 +01:00
|
|
|
) === 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_) {
|
2018-02-25 16:43:54 +01:00
|
|
|
foreach ($stmt->stmts as $function_stmt) {
|
|
|
|
if ($function_stmt instanceof PhpParser\Node\Stmt\Global_) {
|
|
|
|
foreach ($function_stmt->vars as $var) {
|
|
|
|
if ($var instanceof PhpParser\Node\Expr\Variable) {
|
|
|
|
if (is_string($var->name)) {
|
|
|
|
$var_id = '$' . $var->name;
|
|
|
|
|
|
|
|
// registers variable in global context
|
|
|
|
$context->hasVariable($var_id, $this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} elseif (!$function_stmt instanceof PhpParser\Node\Stmt\Nop) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
if (!$project_checker->codebase->register_global_functions) {
|
2017-08-14 21:46:01 +02:00
|
|
|
$function_id = strtolower($stmt->name);
|
2017-03-19 20:41:24 +01:00
|
|
|
$function_context = new Context($context->self);
|
2018-01-21 19:38:51 +01:00
|
|
|
$function_context->collect_references = $project_checker->codebase->collect_references;
|
2017-08-14 21:46:01 +02:00
|
|
|
$this->function_checkers[$function_id]->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-24 23:34:46 +01:00
|
|
|
if ($config->reportIssueInFile('InvalidReturnType', $this->getFilePath())) {
|
2017-03-19 20:41:24 +01:00
|
|
|
/** @var string */
|
2017-08-14 21:46:01 +02:00
|
|
|
$method_id = $this->function_checkers[$function_id]->getMethodId();
|
2016-12-29 01:57:18 +01:00
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
$function_storage = $codebase->functions->getStorage(
|
2017-08-15 01:30:11 +02:00
|
|
|
$this,
|
|
|
|
$method_id
|
2017-07-29 21:05:06 +02:00
|
|
|
);
|
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;
|
|
|
|
|
2017-08-14 21:46:01 +02:00
|
|
|
$this->function_checkers[$function_id]->verifyReturnType(
|
2017-03-19 20:41:24 +01:00
|
|
|
$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-10-13 02:24:22 +02:00
|
|
|
if (ExpressionChecker::analyze($this, $stmt, $context) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
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] =
|
2018-01-28 23:28:34 +01:00
|
|
|
$global_context && $global_context->hasVariable($var_id, $this)
|
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()) {
|
2018-01-14 18:09:40 +01:00
|
|
|
if (PropertyAssignmentChecker::analyzeInstance(
|
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()) {
|
2018-02-09 00:14:28 +01:00
|
|
|
$codebase->classlikes->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-12-23 01:26:08 +01:00
|
|
|
try {
|
|
|
|
$class_checker = (new ClassChecker($stmt, $this->source, $stmt->name));
|
|
|
|
$class_checker->analyze(null, $global_context);
|
|
|
|
} catch (\InvalidArgumentException $e) {
|
|
|
|
// disregard this exception, we'll likely see it elsewhere in the form
|
|
|
|
// of an issue
|
|
|
|
}
|
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()) {
|
2018-02-08 05:33:31 +01:00
|
|
|
$var_comments = [];
|
2018-01-24 19:52:34 +01:00
|
|
|
|
2017-11-15 03:43:31 +01:00
|
|
|
try {
|
2018-02-08 05:33:31 +01:00
|
|
|
$var_comments = CommentChecker::getTypeFromComment(
|
2017-11-15 03:43:31 +01:00
|
|
|
(string)$stmt->getDocComment(),
|
|
|
|
$this->getSource(),
|
|
|
|
$this->getSource()->getAliases()
|
|
|
|
);
|
|
|
|
} catch (DocblockParseException $e) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new InvalidDocblock(
|
|
|
|
(string)$e->getMessage(),
|
|
|
|
new CodeLocation($this->getSource(), $stmt, null, true)
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
// fall through
|
|
|
|
}
|
|
|
|
}
|
2017-05-25 07:32:34 +02:00
|
|
|
|
2018-02-08 05:33:31 +01:00
|
|
|
foreach ($var_comments as $var_comment) {
|
|
|
|
if (!$var_comment->var_id) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-10-07 16:22:52 +02:00
|
|
|
$comment_type = ExpressionChecker::fleshOutType(
|
|
|
|
$project_checker,
|
2018-01-02 02:04:03 +01:00
|
|
|
$var_comment->type,
|
2017-10-07 16:22:52 +02:00
|
|
|
$context->self
|
|
|
|
);
|
|
|
|
|
|
|
|
$context->vars_in_scope[$var_comment->var_id] = $comment_type;
|
2017-05-25 07:32:34 +02:00
|
|
|
}
|
2017-03-02 04:27:52 +01:00
|
|
|
}
|
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
|
|
|
}
|
2017-10-27 00:19:19 +02:00
|
|
|
|
2017-12-03 00:28:18 +01:00
|
|
|
if ($loop_scope
|
|
|
|
&& $loop_scope->final_actions
|
|
|
|
&& !in_array(ScopeChecker::ACTION_NONE, $loop_scope->final_actions, true)
|
|
|
|
) {
|
|
|
|
//$has_returned = true;
|
|
|
|
}
|
|
|
|
|
2018-02-12 04:49:19 +01:00
|
|
|
if ($plugin_classes) {
|
2017-11-07 20:46:53 +01:00
|
|
|
$file_manipulations = [];
|
|
|
|
$code_location = new CodeLocation($this->source, $stmt);
|
|
|
|
|
2018-02-12 04:49:19 +01:00
|
|
|
foreach ($plugin_classes as $plugin_fq_class_name) {
|
|
|
|
if ($plugin_fq_class_name::afterStatementCheck(
|
2017-11-07 20:46:53 +01:00
|
|
|
$this,
|
|
|
|
$stmt,
|
|
|
|
$context,
|
|
|
|
$code_location,
|
|
|
|
$this->getSuppressedIssues(),
|
|
|
|
$file_manipulations
|
|
|
|
) === false) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($file_manipulations) {
|
2018-02-12 02:56:34 +01:00
|
|
|
/** @psalm-suppress MixedTypeCoercion */
|
2017-11-07 20:46:53 +01:00
|
|
|
FileManipulationBuffer::add($this->getFilePath(), $file_manipulations);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-27 00:19:19 +02:00
|
|
|
if ($new_issues) {
|
2017-11-19 19:42:48 +01:00
|
|
|
/** @psalm-suppress MixedTypeCoercion */
|
2017-10-27 00:19:19 +02:00
|
|
|
$this->removeSuppressedIssues($new_issues);
|
|
|
|
}
|
2016-01-20 00:27:06 +01:00
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
|
2018-01-25 07:04:26 +01:00
|
|
|
if ($root_scope
|
|
|
|
&& $context->collect_references
|
2018-02-17 18:02:31 +01:00
|
|
|
&& !$context->collect_initializations
|
2018-02-17 23:45:30 +01:00
|
|
|
&& $project_checker->codebase->find_unused_code
|
2018-01-25 07:04:26 +01:00
|
|
|
&& $context->check_variables
|
|
|
|
) {
|
2018-01-28 23:28:34 +01:00
|
|
|
$this->checkUnreferencedVars();
|
2018-01-25 07:04:26 +01:00
|
|
|
}
|
|
|
|
|
2018-01-21 22:24:20 +01:00
|
|
|
if ($project_checker->alter_code && $root_scope && $this->vars_to_initialize) {
|
|
|
|
$file_contents = $project_checker->codebase->getFileContents($this->getFilePath());
|
|
|
|
|
|
|
|
foreach ($this->vars_to_initialize as $var_id => $branch_point) {
|
|
|
|
$newline_pos = (int)strrpos($file_contents, "\n", $branch_point - strlen($file_contents)) + 1;
|
|
|
|
$indentation = substr($file_contents, $newline_pos, $branch_point - $newline_pos);
|
|
|
|
FileManipulationBuffer::add($this->getFilePath(), [
|
|
|
|
new FileManipulation($branch_point, $branch_point, $var_id . ' = null;' . PHP_EOL . $indentation),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
return null;
|
2016-01-20 00:27:06 +01:00
|
|
|
}
|
|
|
|
|
2018-01-25 07:04:26 +01:00
|
|
|
/**
|
|
|
|
* @return void
|
|
|
|
*/
|
2018-02-25 16:43:54 +01:00
|
|
|
public function checkUnreferencedVars()
|
2018-01-25 07:04:26 +01:00
|
|
|
{
|
|
|
|
$source = $this->getSource();
|
|
|
|
$function_storage = $source instanceof FunctionLikeChecker ? $source->getFunctionLikeStorage($this) : null;
|
|
|
|
|
2018-01-28 23:28:34 +01:00
|
|
|
foreach ($this->unused_var_locations as list($var_id, $original_location)) {
|
|
|
|
if ($var_id === '$_') {
|
2018-01-25 07:04:26 +01:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-01-28 23:28:34 +01:00
|
|
|
if (!$function_storage || !array_key_exists(substr($var_id, 1), $function_storage->param_types)) {
|
2018-01-25 07:04:26 +01:00
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new UnusedVariable(
|
2018-01-28 23:28:34 +01:00
|
|
|
'Variable ' . $var_id . ' is never referenced',
|
2018-01-25 07:04:26 +01:00
|
|
|
$original_location
|
|
|
|
),
|
|
|
|
$this->getSuppressedIssues()
|
|
|
|
)) {
|
|
|
|
// fall through
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-11-02 07:29:00 +01:00
|
|
|
* @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) {
|
2018-02-27 01:32:26 +01:00
|
|
|
$var_id = '$' . $var->name;
|
|
|
|
|
|
|
|
$context->vars_in_scope[$var_id] = Type::getMixed();
|
|
|
|
$context->vars_possibly_in_scope[$var_id] = true;
|
|
|
|
$context->assigned_var_ids[$var_id] = true;
|
|
|
|
|
|
|
|
$location = new CodeLocation($this, $stmt);
|
|
|
|
|
|
|
|
if ($context->collect_references) {
|
|
|
|
$context->unreferenced_vars[$var_id] = $location;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->registerVariable(
|
|
|
|
$var_id,
|
|
|
|
$location,
|
|
|
|
$context->branch_point
|
|
|
|
);
|
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
|
2017-07-25 22:11:02 +02:00
|
|
|
* @param array<string, Type\Union> $existing_class_constants
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-11-02 07:29:00 +01:00
|
|
|
* @return Type\Union|null
|
2016-10-22 19:23:18 +02:00
|
|
|
*/
|
2017-07-25 22:11:02 +02:00
|
|
|
public static function getSimpleType(
|
|
|
|
PhpParser\Node\Expr $stmt,
|
|
|
|
StatementsSource $statements_source = null,
|
|
|
|
array $existing_class_constants = []
|
|
|
|
) {
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp) {
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat) {
|
|
|
|
return Type::getString();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd
|
|
|
|
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
|
|
|
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd
|
|
|
|
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr
|
|
|
|
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal
|
|
|
|
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\NotEqual
|
|
|
|
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical
|
|
|
|
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical
|
|
|
|
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Greater
|
|
|
|
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual
|
|
|
|
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Smaller
|
|
|
|
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual
|
|
|
|
) {
|
|
|
|
return Type::getBool();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Coalesce) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Spaceship) {
|
|
|
|
return Type::getInt();
|
|
|
|
}
|
|
|
|
|
|
|
|
$stmt->left->inferredType = self::getSimpleType(
|
|
|
|
$stmt->left,
|
|
|
|
$statements_source,
|
|
|
|
$existing_class_constants
|
|
|
|
);
|
|
|
|
$stmt->right->inferredType = self::getSimpleType(
|
|
|
|
$stmt->right,
|
|
|
|
$statements_source,
|
|
|
|
$existing_class_constants
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!$stmt->left->inferredType || !$stmt->right->inferredType) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Plus ||
|
|
|
|
$stmt instanceof PhpParser\Node\Expr\BinaryOp\Minus ||
|
|
|
|
$stmt instanceof PhpParser\Node\Expr\BinaryOp\Mod ||
|
|
|
|
$stmt instanceof PhpParser\Node\Expr\BinaryOp\Mul ||
|
|
|
|
$stmt instanceof PhpParser\Node\Expr\BinaryOp\Pow
|
|
|
|
) {
|
2018-01-14 18:09:40 +01:00
|
|
|
BinaryOpChecker::analyzeNonDivArithmenticOp(
|
2017-07-25 22:11:02 +02:00
|
|
|
$statements_source,
|
|
|
|
$stmt->left,
|
|
|
|
$stmt->right,
|
|
|
|
$stmt,
|
|
|
|
$result_type
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($result_type) {
|
|
|
|
return $result_type;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Div
|
|
|
|
&& ($stmt->left->inferredType->hasInt() || $stmt->left->inferredType->hasFloat())
|
|
|
|
&& ($stmt->right->inferredType->hasInt() || $stmt->right->inferredType->hasFloat())
|
|
|
|
) {
|
|
|
|
return Type::combineUnionTypes(Type::getFloat(), Type::getInt());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
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();
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\ClassConstFetch) {
|
|
|
|
if ($stmt->class instanceof PhpParser\Node\Name
|
|
|
|
&& $stmt->class->parts !== ['static']
|
|
|
|
&& is_string($stmt->name)
|
|
|
|
&& isset($existing_class_constants[$stmt->name])
|
|
|
|
) {
|
|
|
|
return $existing_class_constants[$stmt->name];
|
|
|
|
}
|
|
|
|
|
2018-03-05 22:06:06 +01:00
|
|
|
if (strtolower($stmt->name) === 'class') {
|
|
|
|
return Type::getClassString();
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Scalar\String_) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getString();
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Scalar\LNumber) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getInt();
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Scalar\DNumber) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getFloat();
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\Array_) {
|
2017-01-02 07:07:44 +01:00
|
|
|
if (count($stmt->items) === 0) {
|
|
|
|
return Type::getEmptyArray();
|
|
|
|
}
|
|
|
|
|
2017-12-19 15:48:01 +01:00
|
|
|
$item_key_type = null;
|
|
|
|
$item_value_type = null;
|
|
|
|
|
|
|
|
$property_types = [];
|
|
|
|
|
|
|
|
$can_create_objectlike = true;
|
|
|
|
|
|
|
|
foreach ($stmt->items as $int_offset => $item) {
|
2018-01-14 00:33:32 +01:00
|
|
|
if ($item === null) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2017-12-19 15:48:01 +01:00
|
|
|
if ($item->key) {
|
|
|
|
$single_item_key_type = self::getSimpleType(
|
|
|
|
$item->key,
|
|
|
|
$statements_source,
|
|
|
|
$existing_class_constants
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($single_item_key_type) {
|
|
|
|
if ($item_key_type) {
|
|
|
|
$item_key_type = Type::combineUnionTypes($single_item_key_type, $item_key_type);
|
|
|
|
} else {
|
|
|
|
$item_key_type = $single_item_key_type;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$item_key_type = Type::getInt();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($item_value_type && $item_value_type->isMixed() && !$can_create_objectlike) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$single_item_value_type = self::getSimpleType(
|
|
|
|
$item->value,
|
|
|
|
$statements_source,
|
|
|
|
$existing_class_constants
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($single_item_value_type) {
|
|
|
|
if ($item->key instanceof PhpParser\Node\Scalar\String_
|
|
|
|
|| $item->key instanceof PhpParser\Node\Scalar\LNumber
|
|
|
|
|| !$item->key
|
|
|
|
) {
|
|
|
|
$property_types[$item->key ? $item->key->value : $int_offset] = $single_item_value_type;
|
|
|
|
} else {
|
|
|
|
$can_create_objectlike = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($item_value_type) {
|
|
|
|
$item_value_type = Type::combineUnionTypes($single_item_value_type, $item_value_type);
|
|
|
|
} else {
|
|
|
|
$item_value_type = $single_item_value_type;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$item_value_type = Type::getMixed();
|
|
|
|
|
|
|
|
if ($item->key instanceof PhpParser\Node\Scalar\String_
|
|
|
|
|| $item->key instanceof PhpParser\Node\Scalar\LNumber
|
|
|
|
|| !$item->key
|
|
|
|
) {
|
|
|
|
$property_types[$item->key ? $item->key->value : $int_offset] = $item_value_type;
|
|
|
|
} else {
|
|
|
|
$can_create_objectlike = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if this array looks like an object-like array, let's return that instead
|
|
|
|
if ($item_value_type
|
|
|
|
&& $item_key_type
|
|
|
|
&& ($item_key_type->hasString() || $item_key_type->hasInt())
|
|
|
|
&& $can_create_objectlike
|
|
|
|
) {
|
|
|
|
return new Type\Union([new Type\Atomic\ObjectLike($property_types)]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Type\Union([
|
|
|
|
new Type\Atomic\TArray([
|
|
|
|
$item_key_type ?: Type::getMixed(),
|
|
|
|
$item_value_type ?: Type::getMixed(),
|
|
|
|
]),
|
|
|
|
]);
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\Cast\Int_) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getInt();
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\Cast\Double) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getFloat();
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\Cast\Bool_) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getBool();
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\Cast\String_) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getString();
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\Cast\Object_) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getObject();
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\Cast\Array_) {
|
2016-10-22 19:23:18 +02:00
|
|
|
return Type::getArray();
|
2017-07-25 22:11:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\UnaryMinus || $stmt instanceof PhpParser\Node\Expr\UnaryPlus) {
|
|
|
|
return self::getSimpleType($stmt->expr, $statements_source, $existing_class_constants);
|
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-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Stmt\Const_ $stmt
|
|
|
|
* @param Context $context
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-11-02 07:29:00 +01:00
|
|
|
* @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
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-11-05 02:14:04 +01:00
|
|
|
* @return Type\Union|null
|
2016-11-02 07:29:00 +01:00
|
|
|
*/
|
2017-07-28 16:42:30 +02:00
|
|
|
public function getConstType(
|
|
|
|
StatementsChecker $statements_checker,
|
|
|
|
$const_name,
|
|
|
|
$is_fully_qualified,
|
|
|
|
Context $context
|
|
|
|
) {
|
2016-11-21 03:49:06 +01:00
|
|
|
$fq_const_name = null;
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
$aliased_constants = $this->getAliases()->constants;
|
2017-01-07 20:35:07 +01:00
|
|
|
|
|
|
|
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, '\\')) {
|
2018-02-04 18:23:32 +01:00
|
|
|
$fq_const_name = Type::getFQCLNFromString($const_name, $this->getAliases());
|
2016-11-21 03:49:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($fq_const_name) {
|
|
|
|
$const_name_parts = explode('\\', $fq_const_name);
|
2017-11-28 06:46:41 +01:00
|
|
|
/** @var string */
|
2016-11-21 03:49:06 +01:00
|
|
|
$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];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-28 23:28:34 +01:00
|
|
|
if ($context->hasVariable($const_name, $statements_checker)) {
|
2017-01-02 21:31:18 +01:00
|
|
|
return $context->vars_in_scope[$const_name];
|
2016-11-21 03:49:06 +01:00
|
|
|
}
|
|
|
|
|
2017-07-28 16:42:30 +02:00
|
|
|
$file_path = $statements_checker->getFilePath();
|
2018-01-21 19:38:51 +01:00
|
|
|
$project_checker = $statements_checker->getFileChecker()->project_checker;
|
2017-07-28 16:42:30 +02:00
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
$file_storage_provider = $project_checker->file_storage_provider;
|
2017-07-29 21:05:06 +02:00
|
|
|
|
|
|
|
$file_storage = $file_storage_provider->get($file_path);
|
2017-07-28 16:42:30 +02:00
|
|
|
|
|
|
|
if (isset($file_storage->declaring_constants[$const_name])) {
|
|
|
|
$constant_file_path = $file_storage->declaring_constants[$const_name];
|
|
|
|
|
2017-07-29 21:05:06 +02:00
|
|
|
return $file_storage_provider->get($constant_file_path)->constants[$const_name];
|
2017-07-28 16:42:30 +02:00
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
$stubbed_const_type = $project_checker->codebase->getStubbedConstantType(
|
|
|
|
$fq_const_name ?: $const_name
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($stubbed_const_type) {
|
|
|
|
return $stubbed_const_type;
|
2017-07-27 01:54:22 +02:00
|
|
|
}
|
|
|
|
|
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
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
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);
|
|
|
|
}
|
2016-01-20 00:27:06 +01:00
|
|
|
}
|
|
|
|
|
2016-10-09 23:54:58 +02:00
|
|
|
/**
|
2017-02-08 00:09:12 +01:00
|
|
|
* @param string $var_name
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-02-08 00:09:12 +01:00
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function hasVariable($var_name)
|
|
|
|
{
|
|
|
|
return isset($this->all_vars[$var_name]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-01-21 22:24:20 +01:00
|
|
|
* @param string $var_id
|
2017-02-08 00:09:12 +01:00
|
|
|
* @param CodeLocation $location
|
2018-01-21 22:24:20 +01:00
|
|
|
* @param int|null $branch_point
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-10-09 23:54:58 +02:00
|
|
|
* @return void
|
|
|
|
*/
|
2018-01-21 22:24:20 +01:00
|
|
|
public function registerVariable($var_id, CodeLocation $location, $branch_point)
|
2016-01-20 00:27:06 +01:00
|
|
|
{
|
2018-01-21 22:24:20 +01:00
|
|
|
$this->all_vars[$var_id] = $location;
|
|
|
|
|
|
|
|
if ($branch_point) {
|
|
|
|
$this->var_branch_points[$var_id] = $branch_point;
|
|
|
|
}
|
2018-01-28 23:28:34 +01:00
|
|
|
|
|
|
|
$this->registerVariableAssignment($var_id, $location);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $var_id
|
|
|
|
* @param CodeLocation $location
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function registerVariableAssignment($var_id, CodeLocation $location)
|
|
|
|
{
|
|
|
|
$this->unused_var_locations[spl_object_hash($location)] = [$var_id, $location];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function registerVariableUse(CodeLocation $location)
|
|
|
|
{
|
|
|
|
unset($this->unused_var_locations[spl_object_hash($location)]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array<string, array{0: string, 1: CodeLocation}>
|
|
|
|
*/
|
|
|
|
public function getUnusedVarLocations()
|
|
|
|
{
|
|
|
|
return $this->unused_var_locations;
|
2016-01-20 00:27:06 +01: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
|
|
|
*
|
2018-01-21 22:24:20 +01:00
|
|
|
* @param string $var_id
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-02-08 00:09:12 +01:00
|
|
|
* @return CodeLocation|null
|
2016-10-22 19:23:18 +02:00
|
|
|
*/
|
2018-01-21 22:24:20 +01:00
|
|
|
public function getFirstAppearance($var_id)
|
|
|
|
{
|
|
|
|
return isset($this->all_vars[$var_id]) ? $this->all_vars[$var_id] : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $var_id
|
|
|
|
*
|
|
|
|
* @return int|null
|
|
|
|
*/
|
|
|
|
public function getBranchPoint($var_id)
|
|
|
|
{
|
|
|
|
return isset($this->var_branch_points[$var_id]) ? $this->var_branch_points[$var_id] : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $var_id
|
|
|
|
* @param int $branch_point
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function addVariableInitialization($var_id, $branch_point)
|
2016-10-02 19:05:49 +02:00
|
|
|
{
|
2018-01-21 22:24:20 +01:00
|
|
|
$this->vars_to_initialize[$var_id] = $branch_point;
|
2016-10-02 19:05:49 +02:00
|
|
|
}
|
2016-10-21 00:16:17 +02:00
|
|
|
|
2017-11-26 22:03:17 +01:00
|
|
|
/**
|
|
|
|
* @return FileChecker
|
|
|
|
*/
|
2017-07-29 21:05:06 +02:00
|
|
|
public function getFileChecker()
|
|
|
|
{
|
|
|
|
return $this->file_checker;
|
|
|
|
}
|
|
|
|
|
2017-08-12 01:05:04 +02:00
|
|
|
/**
|
|
|
|
* @return array<string, FunctionChecker>
|
|
|
|
*/
|
|
|
|
public function getFunctionCheckers()
|
|
|
|
{
|
|
|
|
return $this->function_checkers;
|
|
|
|
}
|
2016-01-20 16:57:56 +01:00
|
|
|
}
|