2018-10-26 06:59:14 +02:00
|
|
|
<?php
|
2020-03-15 04:54:42 +01:00
|
|
|
namespace Psalm\Internal\PhpVisitor;
|
2018-10-26 06:59:14 +02:00
|
|
|
|
2019-07-05 22:24:00 +02:00
|
|
|
use PhpParser;
|
2021-06-08 04:55:21 +02:00
|
|
|
|
|
|
|
use function count;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function preg_replace;
|
|
|
|
use function reset;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function strlen;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function strpos;
|
2019-07-05 22:24:00 +02:00
|
|
|
use function strrpos;
|
|
|
|
use function substr;
|
|
|
|
use function substr_count;
|
2018-10-26 06:59:14 +02:00
|
|
|
|
2018-10-27 18:27:21 +02:00
|
|
|
/**
|
|
|
|
* Given a list of file diffs, this scans an AST to find the sections it can replace, and parses
|
|
|
|
* just those methods.
|
|
|
|
*/
|
2020-10-15 19:23:35 +02:00
|
|
|
class PartialParserVisitor extends PhpParser\NodeVisitorAbstract
|
2018-10-26 06:59:14 +02:00
|
|
|
{
|
2021-02-28 07:36:06 +01:00
|
|
|
/** @var array<int, array{int, int, int, int, int}> */
|
2018-10-26 06:59:14 +02:00
|
|
|
private $offset_map;
|
|
|
|
|
|
|
|
/** @var bool */
|
|
|
|
private $must_rescan = false;
|
|
|
|
|
|
|
|
/** @var int */
|
|
|
|
private $non_method_changes;
|
|
|
|
|
|
|
|
/** @var string */
|
|
|
|
private $a_file_contents;
|
|
|
|
|
|
|
|
/** @var string */
|
|
|
|
private $b_file_contents;
|
|
|
|
|
|
|
|
/** @var int */
|
|
|
|
private $a_file_contents_length;
|
|
|
|
|
|
|
|
/** @var PhpParser\Parser */
|
|
|
|
private $parser;
|
|
|
|
|
|
|
|
/** @var PhpParser\ErrorHandler\Collecting */
|
|
|
|
private $error_handler;
|
|
|
|
|
2021-02-28 07:36:06 +01:00
|
|
|
/** @param array<int, array{int, int, int, int, int}> $offset_map */
|
2018-10-26 06:59:14 +02:00
|
|
|
public function __construct(
|
|
|
|
PhpParser\Parser $parser,
|
|
|
|
PhpParser\ErrorHandler\Collecting $error_handler,
|
|
|
|
array $offset_map,
|
|
|
|
string $a_file_contents,
|
|
|
|
string $b_file_contents
|
|
|
|
) {
|
|
|
|
$this->parser = $parser;
|
|
|
|
$this->error_handler = $error_handler;
|
|
|
|
$this->offset_map = $offset_map;
|
|
|
|
$this->a_file_contents = $a_file_contents;
|
|
|
|
$this->a_file_contents_length = strlen($a_file_contents);
|
|
|
|
$this->b_file_contents = $b_file_contents;
|
|
|
|
$this->non_method_changes = count($offset_map);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param bool $traverseChildren
|
|
|
|
*
|
|
|
|
* @return null|int|PhpParser\Node
|
|
|
|
*/
|
|
|
|
public function enterNode(PhpParser\Node $node, &$traverseChildren = true)
|
|
|
|
{
|
2020-02-22 06:29:59 +01:00
|
|
|
/** @var array{startFilePos: int, endFilePos: int, startLine: int} */
|
2018-10-26 06:59:14 +02:00
|
|
|
$attrs = $node->getAttributes();
|
|
|
|
|
2018-10-26 17:22:43 +02:00
|
|
|
if ($cs = $node->getComments()) {
|
2020-09-20 14:56:49 +02:00
|
|
|
$stmt_start_pos = $cs[0]->getStartFilePos();
|
2018-10-26 06:59:14 +02:00
|
|
|
} else {
|
|
|
|
$stmt_start_pos = $attrs['startFilePos'];
|
|
|
|
}
|
|
|
|
|
|
|
|
$stmt_end_pos = $attrs['endFilePos'];
|
|
|
|
|
|
|
|
$start_offset = 0;
|
|
|
|
$end_offset = 0;
|
|
|
|
|
|
|
|
$line_offset = 0;
|
|
|
|
|
2020-09-02 06:17:41 +02:00
|
|
|
foreach ($this->offset_map as [$a_s, $a_e, $b_s, $b_e, $line_diff]) {
|
2018-10-26 06:59:14 +02:00
|
|
|
if ($a_s > $stmt_end_pos) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
$end_offset = $b_e - $a_e;
|
|
|
|
|
|
|
|
if ($a_s < $stmt_start_pos) {
|
|
|
|
$start_offset = $b_s - $a_s;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($a_e < $stmt_start_pos) {
|
|
|
|
$start_offset = $end_offset;
|
|
|
|
|
|
|
|
$line_offset = $line_diff;
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($node instanceof PhpParser\Node\Stmt\ClassMethod
|
|
|
|
|| $node instanceof PhpParser\Node\Stmt\Namespace_
|
|
|
|
|| $node instanceof PhpParser\Node\Stmt\ClassLike
|
|
|
|
) {
|
|
|
|
if ($node instanceof PhpParser\Node\Stmt\ClassMethod) {
|
|
|
|
if ($a_s >= $stmt_start_pos && $a_e <= $stmt_end_pos) {
|
2020-09-02 06:17:41 +02:00
|
|
|
foreach ($this->offset_map as [$a_s2, $a_e2, $b_s2, $b_e2]) {
|
2018-10-26 06:59:14 +02:00
|
|
|
if ($a_s2 > $stmt_end_pos) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2018-10-29 22:12:20 +01:00
|
|
|
// we have a diff that goes outside the bounds that we care about
|
|
|
|
if ($a_e2 > $stmt_end_pos) {
|
|
|
|
$this->must_rescan = true;
|
2019-07-05 22:24:00 +02:00
|
|
|
|
2018-10-29 22:12:20 +01:00
|
|
|
return PhpParser\NodeTraverser::STOP_TRAVERSAL;
|
|
|
|
}
|
|
|
|
|
2018-10-26 06:59:14 +02:00
|
|
|
$end_offset = $b_e2 - $a_e2;
|
|
|
|
|
|
|
|
if ($a_s2 < $stmt_start_pos) {
|
|
|
|
$start_offset = $b_s2 - $a_s2;
|
|
|
|
}
|
|
|
|
|
2018-10-26 19:22:41 +02:00
|
|
|
if ($a_e2 < $stmt_start_pos) {
|
|
|
|
$start_offset = $end_offset;
|
|
|
|
|
|
|
|
$line_offset = $line_diff;
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-10-26 06:59:14 +02:00
|
|
|
if ($a_s2 >= $stmt_start_pos && $a_e2 <= $stmt_end_pos) {
|
2019-07-05 22:24:00 +02:00
|
|
|
--$this->non_method_changes;
|
2018-10-26 06:59:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$stmt_start_pos += $start_offset;
|
|
|
|
$stmt_end_pos += $end_offset;
|
|
|
|
|
|
|
|
$current_line = substr_count(substr($this->b_file_contents, 0, $stmt_start_pos), "\n");
|
|
|
|
|
|
|
|
$method_contents = substr(
|
|
|
|
$this->b_file_contents,
|
|
|
|
$stmt_start_pos,
|
|
|
|
$stmt_end_pos - $stmt_start_pos + 1
|
|
|
|
);
|
|
|
|
|
2018-10-29 22:12:20 +01:00
|
|
|
if (!$method_contents) {
|
|
|
|
$this->must_rescan = true;
|
2019-07-05 22:24:00 +02:00
|
|
|
|
2018-10-29 22:12:20 +01:00
|
|
|
return PhpParser\NodeTraverser::STOP_TRAVERSAL;
|
|
|
|
}
|
|
|
|
|
2018-10-26 06:59:14 +02:00
|
|
|
$error_handler = new \PhpParser\ErrorHandler\Collecting();
|
|
|
|
|
2019-07-05 22:24:00 +02:00
|
|
|
$fake_class = '<?php class _ {' . $method_contents . '}';
|
2018-10-26 06:59:14 +02:00
|
|
|
|
2021-02-15 21:32:35 +01:00
|
|
|
$extra_characters = [];
|
|
|
|
|
|
|
|
// To avoid a parser error during completion we replace
|
|
|
|
//
|
|
|
|
// Foo::
|
|
|
|
// if (...) {}
|
|
|
|
//
|
|
|
|
// with
|
|
|
|
//
|
|
|
|
// Foo::;
|
|
|
|
// if (...) {}
|
|
|
|
//
|
|
|
|
// When we insert the extra semicolon we have to keep track of the places
|
|
|
|
// we inserted it, and then shift the AST node offsets accordingly after parsing
|
|
|
|
// is complete.
|
|
|
|
//
|
|
|
|
// If anyone's unlucky enough to have a static method named "if" with a newline
|
|
|
|
// before the method name e.g.
|
|
|
|
//
|
|
|
|
// Foo::
|
|
|
|
// if(...);
|
|
|
|
//
|
|
|
|
// This transformation will break that.
|
2021-02-17 22:54:11 +01:00
|
|
|
\preg_match_all(
|
2021-02-15 21:32:35 +01:00
|
|
|
'/(->|::)(\n\s*(if|list)\s*\()/',
|
|
|
|
$fake_class,
|
2021-02-17 22:54:11 +01:00
|
|
|
$matches,
|
|
|
|
\PREG_OFFSET_CAPTURE | \PREG_SET_ORDER
|
2021-02-15 21:32:35 +01:00
|
|
|
);
|
|
|
|
|
2021-02-17 22:54:11 +01:00
|
|
|
foreach ($matches as $match) {
|
|
|
|
$fake_class = \substr_replace(
|
|
|
|
$fake_class,
|
|
|
|
$match[1][0] . ';' . $match[2][0],
|
|
|
|
$match[0][1],
|
|
|
|
\strlen($match[0][0])
|
|
|
|
);
|
|
|
|
|
|
|
|
$extra_characters[] = $match[2][1];
|
|
|
|
}
|
|
|
|
|
2018-10-26 06:59:14 +02:00
|
|
|
$replacement_stmts = $this->parser->parse(
|
|
|
|
$fake_class,
|
|
|
|
$error_handler
|
|
|
|
) ?: [];
|
|
|
|
|
|
|
|
if (!$replacement_stmts
|
|
|
|
|| !$replacement_stmts[0] instanceof PhpParser\Node\Stmt\ClassLike
|
2018-10-29 22:12:20 +01:00
|
|
|
|| count($replacement_stmts[0]->stmts) !== 1
|
2018-10-26 06:59:14 +02:00
|
|
|
) {
|
2019-06-29 06:22:41 +02:00
|
|
|
$hacky_class_fix = self::balanceBrackets($fake_class);
|
|
|
|
|
2018-10-26 19:22:41 +02:00
|
|
|
if ($replacement_stmts
|
|
|
|
&& $replacement_stmts[0] instanceof PhpParser\Node\Stmt\ClassLike
|
2018-10-29 22:12:20 +01:00
|
|
|
&& count($replacement_stmts[0]->stmts) !== 1
|
2018-10-26 19:22:41 +02:00
|
|
|
) {
|
|
|
|
$this->must_rescan = true;
|
2019-07-05 22:24:00 +02:00
|
|
|
|
2018-10-26 19:22:41 +02:00
|
|
|
return PhpParser\NodeTraverser::STOP_TRAVERSAL;
|
|
|
|
}
|
|
|
|
|
2019-05-09 17:20:13 +02:00
|
|
|
// changes "): {" to ") {"
|
2019-06-29 06:22:41 +02:00
|
|
|
$hacky_class_fix = preg_replace('/(\)[\s]*):([\s]*\{)/', '$1 $2', $hacky_class_fix);
|
2018-10-26 06:59:14 +02:00
|
|
|
|
2019-05-09 17:20:13 +02:00
|
|
|
if ($hacky_class_fix !== $fake_class) {
|
|
|
|
$replacement_stmts = $this->parser->parse(
|
|
|
|
$hacky_class_fix,
|
|
|
|
$error_handler
|
|
|
|
) ?: [];
|
|
|
|
}
|
2018-10-26 06:59:14 +02:00
|
|
|
|
|
|
|
if (!$replacement_stmts
|
|
|
|
|| !$replacement_stmts[0] instanceof PhpParser\Node\Stmt\ClassLike
|
2018-10-26 17:48:21 +02:00
|
|
|
|| count($replacement_stmts[0]->stmts) > 1
|
2018-10-26 06:59:14 +02:00
|
|
|
) {
|
|
|
|
$this->must_rescan = true;
|
2019-07-05 22:24:00 +02:00
|
|
|
|
2018-10-26 06:59:14 +02:00
|
|
|
return PhpParser\NodeTraverser::STOP_TRAVERSAL;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$replacement_stmts = $replacement_stmts[0]->stmts;
|
|
|
|
|
2021-02-15 17:12:04 +01:00
|
|
|
$extra_offsets = [];
|
|
|
|
|
|
|
|
foreach ($extra_characters as $extra_offset) {
|
|
|
|
$l = strlen($fake_class);
|
|
|
|
|
|
|
|
for ($i = $extra_offset; $i < $l; $i++) {
|
|
|
|
if (isset($extra_offsets[$i])) {
|
|
|
|
$extra_offsets[$i]--;
|
|
|
|
} else {
|
|
|
|
$extra_offsets[$i] = -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-26 06:59:14 +02:00
|
|
|
$renumbering_traverser = new PhpParser\NodeTraverser;
|
2020-03-15 04:54:42 +01:00
|
|
|
$position_shifter = new \Psalm\Internal\PhpVisitor\OffsetShifterVisitor(
|
2018-10-26 06:59:14 +02:00
|
|
|
$stmt_start_pos - 15,
|
2021-02-15 17:12:04 +01:00
|
|
|
$current_line,
|
|
|
|
$extra_offsets
|
2018-10-26 06:59:14 +02:00
|
|
|
);
|
|
|
|
$renumbering_traverser->addVisitor($position_shifter);
|
2018-10-29 20:01:05 +01:00
|
|
|
$replacement_stmts = $renumbering_traverser->traverse($replacement_stmts);
|
2018-10-26 06:59:14 +02:00
|
|
|
|
|
|
|
if ($error_handler->hasErrors()) {
|
|
|
|
foreach ($error_handler->getErrors() as $error) {
|
|
|
|
if ($error->hasColumnInfo()) {
|
2019-10-01 21:44:43 +02:00
|
|
|
/** @var array{startFilePos: int, endFilePos: int} */
|
2018-10-26 06:59:14 +02:00
|
|
|
$error_attrs = $error->getAttributes();
|
|
|
|
$error = new PhpParser\Error(
|
|
|
|
$error->getRawMessage(),
|
|
|
|
[
|
2019-07-05 22:24:00 +02:00
|
|
|
'startFilePos' => $stmt_start_pos + $error_attrs['startFilePos'] - 15,
|
|
|
|
'endFilePos' => $stmt_start_pos + $error_attrs['endFilePos'] - 15,
|
|
|
|
'startLine' => $error->getStartLine() + $current_line + $line_offset,
|
2018-10-26 06:59:14 +02:00
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->error_handler->handleError($error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-30 03:32:26 +02:00
|
|
|
$error_handler->clearErrors();
|
|
|
|
|
2018-10-26 06:59:14 +02:00
|
|
|
$traverseChildren = false;
|
|
|
|
|
2018-10-29 22:12:20 +01:00
|
|
|
return reset($replacement_stmts);
|
2018-10-26 06:59:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$this->must_rescan = true;
|
2019-07-05 22:24:00 +02:00
|
|
|
|
2018-10-26 06:59:14 +02:00
|
|
|
return PhpParser\NodeTraverser::STOP_TRAVERSAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($node->stmts) {
|
|
|
|
/** @var int */
|
|
|
|
$stmt_inner_start_pos = $node->stmts[0]->getAttribute('startFilePos');
|
|
|
|
|
|
|
|
/** @var int */
|
|
|
|
$stmt_inner_end_pos = $node->stmts[count($node->stmts) - 1]->getAttribute('endFilePos');
|
|
|
|
|
|
|
|
if ($node instanceof PhpParser\Node\Stmt\ClassLike) {
|
|
|
|
/** @psalm-suppress PossiblyFalseOperand */
|
|
|
|
$stmt_inner_start_pos = strrpos(
|
|
|
|
$this->a_file_contents,
|
|
|
|
'{',
|
|
|
|
$stmt_inner_start_pos - $this->a_file_contents_length
|
|
|
|
) + 1;
|
|
|
|
|
|
|
|
if ($stmt_inner_end_pos < $this->a_file_contents_length) {
|
|
|
|
$stmt_inner_end_pos = strpos($this->a_file_contents, '}', $stmt_inner_end_pos + 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($a_s > $stmt_inner_start_pos && $a_e < $stmt_inner_end_pos) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->must_rescan = true;
|
2019-07-05 22:24:00 +02:00
|
|
|
|
2018-10-26 06:59:14 +02:00
|
|
|
return PhpParser\NodeTraverser::STOP_TRAVERSAL;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($start_offset !== 0 || $end_offset !== 0 || $line_offset !== 0) {
|
|
|
|
if ($start_offset !== 0) {
|
2018-10-26 17:22:43 +02:00
|
|
|
if ($cs) {
|
|
|
|
$new_comments = [];
|
|
|
|
|
|
|
|
foreach ($cs as $c) {
|
|
|
|
if ($c instanceof PhpParser\Comment\Doc) {
|
|
|
|
$new_comments[] = new PhpParser\Comment\Doc(
|
|
|
|
$c->getText(),
|
2020-09-20 14:56:49 +02:00
|
|
|
$c->getStartLine() + $line_offset,
|
|
|
|
$c->getStartFilePos() + $start_offset
|
2018-10-26 17:22:43 +02:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
$new_comments[] = new PhpParser\Comment(
|
|
|
|
$c->getText(),
|
2020-09-20 14:56:49 +02:00
|
|
|
$c->getStartLine() + $line_offset,
|
|
|
|
$c->getStartFilePos() + $start_offset
|
2018-10-26 17:22:43 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$node->setAttribute('comments', $new_comments);
|
2018-10-26 06:59:14 +02:00
|
|
|
|
|
|
|
$node->setAttribute('startFilePos', $attrs['startFilePos'] + $start_offset);
|
|
|
|
} else {
|
|
|
|
$node->setAttribute('startFilePos', $stmt_start_pos + $start_offset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($end_offset !== 0) {
|
|
|
|
$node->setAttribute('endFilePos', $stmt_end_pos + $end_offset);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($line_offset !== 0) {
|
|
|
|
$node->setAttribute('startLine', $attrs['startLine'] + $line_offset);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $node;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function mustRescan() : bool
|
|
|
|
{
|
|
|
|
return $this->must_rescan || $this->non_method_changes;
|
|
|
|
}
|
2019-06-29 06:22:41 +02:00
|
|
|
|
2020-08-24 00:53:34 +02:00
|
|
|
/**
|
|
|
|
* @psalm-pure
|
|
|
|
*/
|
2020-09-20 00:24:36 +02:00
|
|
|
private static function balanceBrackets(string $fake_class) : string
|
2019-06-29 06:22:41 +02:00
|
|
|
{
|
2019-06-29 06:28:31 +02:00
|
|
|
$tokens = \token_get_all($fake_class);
|
2019-06-29 06:22:41 +02:00
|
|
|
|
|
|
|
$brace_count = 0;
|
|
|
|
|
|
|
|
foreach ($tokens as $token) {
|
|
|
|
if ($token === '{') {
|
2019-07-05 22:24:00 +02:00
|
|
|
++$brace_count;
|
2019-06-29 06:22:41 +02:00
|
|
|
} elseif ($token === '}') {
|
2019-07-05 22:24:00 +02:00
|
|
|
--$brace_count;
|
2019-06-29 06:22:41 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($brace_count > 0) {
|
2019-06-29 06:28:31 +02:00
|
|
|
$fake_class .= \str_repeat('}', $brace_count);
|
2019-06-29 06:22:41 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $fake_class;
|
|
|
|
}
|
2018-10-26 06:59:14 +02:00
|
|
|
}
|