mirror of
https://github.com/danog/psalm.git
synced 2024-12-13 09:47:29 +01:00
360 lines
13 KiB
PHP
360 lines
13 KiB
PHP
<?php
|
|
|
|
namespace Psalm\Internal\Analyzer\Statements;
|
|
|
|
use PhpParser;
|
|
use Psalm\CodeLocation;
|
|
use Psalm\Codebase;
|
|
use Psalm\FileManipulation;
|
|
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
|
use Psalm\Internal\PhpVisitor\CheckTrivialExprVisitor;
|
|
|
|
use function array_key_exists;
|
|
use function array_slice;
|
|
use function count;
|
|
use function is_array;
|
|
use function is_string;
|
|
use function strlen;
|
|
use function substr;
|
|
use function token_get_all;
|
|
use function trim;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
final class UnusedAssignmentRemover
|
|
{
|
|
/**
|
|
* @var array<string, CodeLocation>
|
|
*/
|
|
private array $removed_unref_vars = [];
|
|
|
|
/**
|
|
* @param array<PhpParser\Node\Stmt> $stmts
|
|
* @param array<string, CodeLocation> $var_loc_map
|
|
*/
|
|
public function findUnusedAssignment(
|
|
Codebase $codebase,
|
|
array $stmts,
|
|
array $var_loc_map,
|
|
string $var_id,
|
|
CodeLocation $original_location
|
|
): void {
|
|
$search_result = $this->findAssignStmt($stmts, $var_id, $original_location);
|
|
[$assign_stmt, $assign_exp] = $search_result;
|
|
$chain_assignment = false;
|
|
|
|
if ($assign_stmt !== null && $assign_exp !== null) {
|
|
// Check if we have to remove assignment statement as expression (i.e. just "$var = ")
|
|
|
|
// Consider chain of assignments
|
|
$rhs_exp = $assign_exp->expr;
|
|
if ($rhs_exp instanceof PhpParser\Node\Expr\Assign
|
|
|| $rhs_exp instanceof PhpParser\Node\Expr\AssignOp
|
|
|| $rhs_exp instanceof PhpParser\Node\Expr\AssignRef
|
|
) {
|
|
$chain_assignment = true;
|
|
$removable_stmt = $this->checkRemovableChainAssignment($assign_exp, $var_loc_map);
|
|
} else {
|
|
$removable_stmt = true;
|
|
}
|
|
|
|
if ($removable_stmt) {
|
|
$traverser = new PhpParser\NodeTraverser();
|
|
$visitor = new CheckTrivialExprVisitor();
|
|
$traverser->addVisitor($visitor);
|
|
$traverser->traverse([$rhs_exp]);
|
|
|
|
$rhs_exp_trivial = (count($visitor->getNonTrivialExpr()) === 0);
|
|
|
|
if ($rhs_exp_trivial) {
|
|
$treat_as_expr = false;
|
|
} else {
|
|
$treat_as_expr = true;
|
|
}
|
|
} else {
|
|
$treat_as_expr = true;
|
|
}
|
|
|
|
if ($treat_as_expr) {
|
|
$is_assign_ref = $assign_exp instanceof PhpParser\Node\Expr\AssignRef;
|
|
$new_file_manipulation = self::getPartialRemovalBounds(
|
|
$codebase,
|
|
$original_location,
|
|
$assign_stmt->getEndFilePos(),
|
|
$is_assign_ref,
|
|
);
|
|
$this->removed_unref_vars[$var_id] = $original_location;
|
|
} else {
|
|
// Remove whole assignment statement
|
|
$new_file_manipulation = new FileManipulation(
|
|
$assign_stmt->getStartFilePos(),
|
|
$assign_stmt->getEndFilePos() + 1,
|
|
"",
|
|
false,
|
|
true,
|
|
);
|
|
|
|
// If statement we are removing is a chain of assignments, mark other variables as removed
|
|
if ($chain_assignment) {
|
|
$this->markRemovedChainAssignVar($assign_exp, $var_loc_map);
|
|
} else {
|
|
$this->removed_unref_vars[$var_id] = $original_location;
|
|
}
|
|
}
|
|
|
|
FileManipulationBuffer::add($original_location->file_path, [$new_file_manipulation]);
|
|
} elseif ($assign_exp !== null) {
|
|
$is_assign_ref = $assign_exp instanceof PhpParser\Node\Expr\AssignRef;
|
|
$new_file_manipulation = self::getPartialRemovalBounds(
|
|
$codebase,
|
|
$original_location,
|
|
$assign_exp->getEndFilePos(),
|
|
$is_assign_ref,
|
|
);
|
|
|
|
FileManipulationBuffer::add($original_location->file_path, [$new_file_manipulation]);
|
|
$this->removed_unref_vars[$var_id] = $original_location;
|
|
}
|
|
}
|
|
|
|
private static function getPartialRemovalBounds(
|
|
Codebase $codebase,
|
|
CodeLocation $var_loc,
|
|
int $end_bound,
|
|
bool $assign_ref = false
|
|
): FileManipulation {
|
|
$var_start_loc= $var_loc->raw_file_start;
|
|
$stmt_content = $codebase->file_provider->getContents(
|
|
$var_loc->file_path,
|
|
);
|
|
$str_for_token = "<?php\n" . substr($stmt_content, $var_start_loc, $end_bound - $var_start_loc + 1);
|
|
$token_list = array_slice(token_get_all($str_for_token), 1); //Ignore "<?php"
|
|
|
|
$offset_count = strlen($token_list[0][1]);
|
|
$iter = 1;
|
|
|
|
// Check if second token is just whitespace
|
|
if (is_array($token_list[$iter]) && trim($token_list[$iter][1]) === '') {
|
|
$offset_count += strlen($token_list[1][1]);
|
|
$iter++;
|
|
}
|
|
|
|
// Add offset for assignment operator
|
|
if (is_string($token_list[$iter])) {
|
|
$offset_count += 1;
|
|
} else {
|
|
$offset_count += strlen($token_list[$iter][1]);
|
|
}
|
|
$iter++;
|
|
|
|
// Remove any whitespace following assignment operator token (e.g "=", "+=")
|
|
if (is_array($token_list[$iter]) && trim($token_list[$iter][1]) === '') {
|
|
$offset_count += strlen($token_list[$iter][1]);
|
|
$iter++;
|
|
}
|
|
|
|
// If we are dealing with assignment by reference, we need to handle "&" and any whitespace after
|
|
if ($assign_ref) {
|
|
$offset_count += 1;
|
|
$iter++;
|
|
// Handle any whitespace after "&"
|
|
if (is_array($token_list[$iter]) && trim($token_list[$iter][1]) === '') {
|
|
$offset_count += strlen($token_list[$iter][1]);
|
|
}
|
|
}
|
|
|
|
$file_man_start = $var_start_loc;
|
|
$file_man_end = $var_start_loc + $offset_count;
|
|
|
|
return new FileManipulation($file_man_start, $file_man_end, "", false);
|
|
}
|
|
|
|
/**
|
|
* @param PhpParser\Node\Expr\Assign|PhpParser\Node\Expr\AssignOp|PhpParser\Node\Expr\AssignRef $cur_assign
|
|
* @param array<string, CodeLocation> $var_loc_map
|
|
*/
|
|
private function markRemovedChainAssignVar(PhpParser\Node\Expr $cur_assign, array $var_loc_map): void
|
|
{
|
|
$var = $cur_assign->var;
|
|
if ($var instanceof PhpParser\Node\Expr\Variable && is_string($var->name)) {
|
|
$var_name = "$" . $var->name;
|
|
$var_loc = $var_loc_map[$var_name];
|
|
$this->removed_unref_vars[$var_name] = $var_loc;
|
|
|
|
$rhs_exp = $cur_assign->expr;
|
|
if ($rhs_exp instanceof PhpParser\Node\Expr\Assign
|
|
|| $rhs_exp instanceof PhpParser\Node\Expr\AssignOp
|
|
|| $rhs_exp instanceof PhpParser\Node\Expr\AssignRef
|
|
) {
|
|
$this->markRemovedChainAssignVar($rhs_exp, $var_loc_map);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param PhpParser\Node\Expr\Assign|PhpParser\Node\Expr\AssignOp|PhpParser\Node\Expr\AssignRef $cur_assign
|
|
* @param array<string, CodeLocation> $var_loc_map
|
|
*/
|
|
private function checkRemovableChainAssignment(PhpParser\Node\Expr $cur_assign, array $var_loc_map): bool
|
|
{
|
|
// Check if current assignment expr's variable is removable
|
|
$var = $cur_assign->var;
|
|
if ($var instanceof PhpParser\Node\Expr\Variable && is_string($var->name)) {
|
|
$var_loc = $cur_assign->var->getStartFilePos();
|
|
$var_name = "$" . $var->name;
|
|
|
|
if (array_key_exists($var_name, $var_loc_map) &&
|
|
$var_loc_map[$var_name]->raw_file_start === $var_loc) {
|
|
$curr_removable = true;
|
|
} else {
|
|
$curr_removable = false;
|
|
}
|
|
|
|
if ($curr_removable) {
|
|
$rhs_exp = $cur_assign->expr;
|
|
|
|
if ($rhs_exp instanceof PhpParser\Node\Expr\Assign
|
|
|| $rhs_exp instanceof PhpParser\Node\Expr\AssignOp
|
|
|| $rhs_exp instanceof PhpParser\Node\Expr\AssignRef
|
|
) {
|
|
return $this->checkRemovableChainAssignment($rhs_exp, $var_loc_map);
|
|
}
|
|
}
|
|
return $curr_removable;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param array<PhpParser\Node\Stmt> $stmts
|
|
* @return array{
|
|
* 0: PhpParser\Node\Stmt|null,
|
|
* 1: PhpParser\Node\Expr\Assign|PhpParser\Node\Expr\AssignOp|PhpParser\Node\Expr\AssignRef|null
|
|
* }
|
|
*/
|
|
private function findAssignStmt(array $stmts, string $var_id, CodeLocation $original_location): array
|
|
{
|
|
$assign_stmt = null;
|
|
$assign_exp = null;
|
|
$assign_exp_found = false;
|
|
|
|
$i = 0;
|
|
|
|
while ($i < count($stmts) && !$assign_exp_found) {
|
|
$stmt = $stmts[$i];
|
|
if ($stmt instanceof PhpParser\Node\Stmt\Expression) {
|
|
$search_result = $this->findAssignExp($stmt->expr, $var_id, $original_location->raw_file_start);
|
|
|
|
[$target_exp, $levels_taken] = $search_result;
|
|
|
|
if ($target_exp !== null) {
|
|
$assign_exp_found = true;
|
|
$assign_exp = $target_exp;
|
|
$assign_stmt = $levels_taken === 1 ? $stmt : null;
|
|
}
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
|
|
$search_result = $this->findAssignStmt($stmt->stmts, $var_id, $original_location);
|
|
|
|
if ($search_result[0] && $search_result[1]) {
|
|
return $search_result;
|
|
}
|
|
|
|
foreach ($stmt->catches as $catch_stmt) {
|
|
$search_result = $this->findAssignStmt($catch_stmt->stmts, $var_id, $original_location);
|
|
|
|
if ($search_result[0] && $search_result[1]) {
|
|
return $search_result;
|
|
}
|
|
}
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Do_
|
|
|| $stmt instanceof PhpParser\Node\Stmt\While_
|
|
) {
|
|
$search_result = $this->findAssignStmt($stmt->stmts, $var_id, $original_location);
|
|
|
|
if ($search_result[0] && $search_result[1]) {
|
|
return $search_result;
|
|
}
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Foreach_) {
|
|
$search_result = $this->findAssignStmt($stmt->stmts, $var_id, $original_location);
|
|
|
|
if ($search_result[0] && $search_result[1]) {
|
|
return $search_result;
|
|
}
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\For_) {
|
|
$search_result = $this->findAssignStmt($stmt->stmts, $var_id, $original_location);
|
|
|
|
if ($search_result[0] && $search_result[1]) {
|
|
return $search_result;
|
|
}
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\If_) {
|
|
$search_result = $this->findAssignStmt($stmt->stmts, $var_id, $original_location);
|
|
|
|
if ($search_result[0] && $search_result[1]) {
|
|
return $search_result;
|
|
}
|
|
|
|
foreach ($stmt->elseifs as $elseif_stmt) {
|
|
$search_result = $this->findAssignStmt($elseif_stmt->stmts, $var_id, $original_location);
|
|
|
|
if ($search_result[0] && $search_result[1]) {
|
|
return $search_result;
|
|
}
|
|
}
|
|
|
|
if ($stmt->else) {
|
|
$search_result = $this->findAssignStmt($stmt->else->stmts, $var_id, $original_location);
|
|
|
|
if ($search_result[0] && $search_result[1]) {
|
|
return $search_result;
|
|
}
|
|
}
|
|
}
|
|
|
|
$i++;
|
|
}
|
|
|
|
return [$assign_stmt, $assign_exp];
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* 0: PhpParser\Node\Expr\Assign|PhpParser\Node\Expr\AssignOp|PhpParser\Node\Expr\AssignRef|null,
|
|
* 1: int
|
|
* }
|
|
*/
|
|
private function findAssignExp(
|
|
PhpParser\Node\Expr $current_node,
|
|
string $var_id,
|
|
int $var_start_loc,
|
|
int $search_level = 1
|
|
): array {
|
|
if ($current_node instanceof PhpParser\Node\Expr\Assign
|
|
|| $current_node instanceof PhpParser\Node\Expr\AssignOp
|
|
|| $current_node instanceof PhpParser\Node\Expr\AssignRef
|
|
) {
|
|
$var = $current_node->var;
|
|
|
|
if ($var instanceof PhpParser\Node\Expr\Variable
|
|
&& $var->name === substr($var_id, 1)
|
|
&& $var->getStartFilePos() === $var_start_loc
|
|
) {
|
|
return [$current_node, $search_level];
|
|
}
|
|
|
|
$rhs_exp = $current_node->expr;
|
|
$rhs_search_result = $this->findAssignExp($rhs_exp, $var_id, $var_start_loc, $search_level + 1);
|
|
return [$rhs_search_result[0], $rhs_search_result[1]];
|
|
}
|
|
|
|
return [null, $search_level];
|
|
}
|
|
|
|
public function checkIfVarRemoved(string $var_id, CodeLocation $var_loc): bool
|
|
{
|
|
return array_key_exists($var_id, $this->removed_unref_vars)
|
|
&& $this->removed_unref_vars[$var_id] === $var_loc;
|
|
}
|
|
}
|