1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-04 10:38:49 +01:00
psalm/src/Psalm/CodeLocation.php

409 lines
12 KiB
PHP
Raw Normal View History

<?php
namespace Psalm;
2021-12-03 21:40:18 +01:00
use Exception;
use LogicException;
2021-06-08 04:55:21 +02:00
use PhpParser;
use Psalm\Internal\Analyzer\CommentAnalyzer;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Storage\ImmutableNonCloneableTrait;
2021-12-03 21:40:18 +01:00
use UnexpectedValueException;
2021-06-08 04:55:21 +02:00
2019-07-05 22:24:00 +02:00
use function explode;
use function max;
2021-06-08 04:55:21 +02:00
use function mb_strcut;
2019-07-05 22:24:00 +02:00
use function min;
use function preg_match;
use function preg_quote;
use function preg_replace;
use function str_replace;
use function strlen;
use function strpos;
use function strrpos;
use function substr_count;
2019-07-05 22:24:00 +02:00
use function trim;
2021-06-08 04:55:21 +02:00
use const PREG_OFFSET_CAPTURE;
/**
* @psalm-immutable
*/
class CodeLocation
{
use ImmutableNonCloneableTrait;
/** @var string */
public $file_path;
/** @var string */
public $file_name;
/** @var int */
2019-08-06 23:11:25 +02:00
public $raw_line_number;
2016-12-08 04:38:57 +01:00
2022-12-11 23:26:05 +01:00
private int $end_line_number = -1;
/** @var int */
public $raw_file_start;
/** @var int */
public $raw_file_end;
2016-12-08 04:38:57 +01:00
/** @var int */
2018-09-29 06:15:39 +02:00
protected $file_start;
2016-12-08 04:38:57 +01:00
/** @var int */
2018-09-29 06:15:39 +02:00
protected $file_end;
/** @var bool */
2018-09-29 06:15:39 +02:00
protected $single_line;
/** @var int */
2018-09-29 06:15:39 +02:00
protected $preview_start;
2016-12-08 04:38:57 +01:00
2022-12-11 23:26:05 +01:00
private int $preview_end = -1;
2016-12-08 04:38:57 +01:00
2022-12-11 23:26:05 +01:00
private int $selection_start = -1;
2016-12-08 04:38:57 +01:00
2022-12-11 23:26:05 +01:00
private int $selection_end = -1;
2016-12-08 04:38:57 +01:00
2022-12-11 23:26:05 +01:00
private int $column_from = -1;
2022-12-11 23:26:05 +01:00
private int $column_to = -1;
2017-01-16 04:39:26 +01:00
2022-12-11 23:26:05 +01:00
private string $snippet = '';
2022-12-16 19:58:47 +01:00
private ?string $text = null;
/** @var int|null */
public $docblock_start;
2022-12-16 19:58:47 +01:00
private ?int $docblock_start_line_number = null;
/** @var int|null */
protected $docblock_line_number;
2022-12-16 19:58:47 +01:00
private ?int $regex_type = null;
2016-12-08 04:38:57 +01:00
2022-12-11 23:26:05 +01:00
private bool $have_recalculated = false;
/** @var null|CodeLocation */
public $previous_location;
2020-09-20 18:54:46 +02:00
public const VAR_TYPE = 0;
public const FUNCTION_RETURN_TYPE = 1;
public const FUNCTION_PARAM_TYPE = 2;
public const FUNCTION_PHPDOC_RETURN_TYPE = 3;
public const FUNCTION_PHPDOC_PARAM_TYPE = 4;
public const FUNCTION_PARAM_VAR = 5;
public const CATCH_VAR = 6;
public const FUNCTION_PHPDOC_METHOD = 7;
2016-12-08 04:38:57 +01:00
public function __construct(
2018-01-21 18:44:46 +01:00
FileSource $file_source,
2018-09-29 06:15:39 +02:00
PhpParser\Node $stmt,
?CodeLocation $previous_location = null,
bool $single_line = false,
?int $regex_type = null,
?string $selected_text = null,
?int $comment_line = null
2016-12-08 04:38:57 +01:00
) {
/** @psalm-suppress ImpureMethodCall Actually mutation-free just not marked */
$this->file_start = (int)$stmt->getAttribute('startFilePos');
/** @psalm-suppress ImpureMethodCall Actually mutation-free just not marked */
$this->file_end = (int)$stmt->getAttribute('endFilePos');
$this->raw_file_start = $this->file_start;
$this->raw_file_end = $this->file_end;
$this->file_path = $file_source->getFilePath();
$this->file_name = $file_source->getFileName();
$this->single_line = $single_line;
$this->regex_type = $regex_type;
$this->previous_location = $previous_location;
$this->text = $selected_text;
/** @psalm-suppress ImpureMethodCall Actually mutation-free just not marked */
$doc_comment = $stmt->getDocComment();
$this->docblock_start = $doc_comment ? $doc_comment->getStartFilePos() : null;
$this->docblock_start_line_number = $doc_comment ? $doc_comment->getStartLine() : null;
$this->preview_start = $this->docblock_start ?: $this->file_start;
/** @psalm-suppress ImpureMethodCall Actually mutation-free just not marked */
2019-08-06 23:11:25 +02:00
$this->raw_line_number = $stmt->getLine();
$this->docblock_line_number = $comment_line;
}
/**
* @psalm-suppress PossiblyUnusedMethod Part of public API
* @return static
*/
public function setCommentLine(?int $line): self
{
if ($line === $this->docblock_line_number) {
return $this;
}
$cloned = clone $this;
$cloned->docblock_line_number = $line;
return $cloned;
2016-12-08 04:38:57 +01:00
}
/**
* @psalm-external-mutation-free
* @psalm-suppress InaccessibleProperty Mainly used for caching
*/
private function calculateRealLocation(): void
2016-12-08 04:38:57 +01:00
{
if ($this->have_recalculated) {
return;
}
$this->have_recalculated = true;
2016-12-08 04:38:57 +01:00
$this->selection_start = $this->file_start;
$this->selection_end = $this->file_end + 1;
2016-12-08 04:38:57 +01:00
$project_analyzer = ProjectAnalyzer::getInstance();
2016-12-08 04:38:57 +01:00
2018-11-11 18:01:14 +01:00
$codebase = $project_analyzer->getCodebase();
2018-11-06 03:57:36 +01:00
2023-03-02 19:02:23 +01:00
/** @psalm-suppress ImpureMethodCall */
2018-11-06 03:57:36 +01:00
$file_contents = $codebase->getFileContents($this->file_path);
2016-12-08 04:38:57 +01:00
$file_length = strlen($file_contents);
$search_limit = $this->single_line ? $this->selection_start : $this->selection_end;
if ($search_limit <= $file_length) {
$preview_end = strpos(
$file_contents,
"\n",
2022-12-18 17:15:15 +01:00
$search_limit,
);
} else {
$preview_end = false;
}
2016-12-08 04:38:57 +01:00
// if the string didn't contain a newline
if ($preview_end === false) {
$preview_end = $this->selection_end;
}
$this->preview_end = $preview_end;
if ($this->docblock_line_number &&
$this->docblock_start_line_number &&
$this->preview_start < $this->selection_start
) {
2016-12-08 04:38:57 +01:00
$preview_lines = explode(
"\n",
mb_strcut(
2016-12-08 04:38:57 +01:00
$file_contents,
$this->preview_start,
2022-12-18 17:15:15 +01:00
$this->selection_start - $this->preview_start - 1,
),
2016-12-08 04:38:57 +01:00
);
$preview_offset = 0;
$comment_line_offset = $this->docblock_line_number - $this->docblock_start_line_number;
2016-12-08 04:38:57 +01:00
2017-05-27 02:05:57 +02:00
for ($i = 0; $i < $comment_line_offset; ++$i) {
2016-12-08 04:38:57 +01:00
$preview_offset += strlen($preview_lines[$i]) + 1;
}
if (!isset($preview_lines[$i])) {
2021-12-03 21:40:18 +01:00
throw new Exception('Should have offset');
}
$key_line = $preview_lines[$i];
2016-12-08 04:38:57 +01:00
$indentation = (int)strpos($key_line, '@');
$key_line = trim(preg_replace('@\**/\s*@', '', mb_strcut($key_line, $indentation)));
$this->selection_start = $preview_offset + $indentation + $this->preview_start;
$this->selection_end = $this->selection_start + strlen($key_line);
}
if ($this->regex_type !== null) {
switch ($this->regex_type) {
case self::VAR_TYPE:
2021-10-13 16:20:36 +02:00
$regex = '/@(?:psalm-)?var[ \t]+' . CommentAnalyzer::TYPE_REGEX . '/';
break;
case self::FUNCTION_RETURN_TYPE:
$regex = '/\\:\s+(\\??\s*[A-Za-z0-9_\\\\\[\]]+)/';
break;
case self::FUNCTION_PARAM_TYPE:
$regex = '/^(\\??\s*[A-Za-z0-9_\\\\\[\]]+)\s/';
break;
case self::FUNCTION_PHPDOC_RETURN_TYPE:
2021-10-13 16:20:36 +02:00
$regex = '/@(?:psalm-)?return[ \t]+' . CommentAnalyzer::TYPE_REGEX . '/';
break;
case self::FUNCTION_PHPDOC_METHOD:
$regex = '/@(?:psalm-)?method[ \t]+(.*)/';
break;
case self::FUNCTION_PHPDOC_PARAM_TYPE:
2021-10-13 16:20:36 +02:00
$regex = '/@(?:psalm-)?param[ \t]+' . CommentAnalyzer::TYPE_REGEX . '/';
break;
case self::FUNCTION_PARAM_VAR:
$regex = '/(\$[^ ]*)/';
break;
case self::CATCH_VAR:
$regex = '/(\$[^ ^\)]*)/';
break;
default:
2021-12-03 21:40:18 +01:00
throw new UnexpectedValueException('Unrecognised regex type ' . $this->regex_type);
}
$preview_snippet = mb_strcut(
2016-12-08 04:38:57 +01:00
$file_contents,
$this->selection_start,
2022-12-18 17:15:15 +01:00
$this->selection_end - $this->selection_start,
2016-12-08 04:38:57 +01:00
);
if ($this->text) {
2018-11-01 17:39:48 +01:00
$regex = '/(' . str_replace(',', ',[ ]*', preg_quote($this->text, '/')) . ')/';
}
if (preg_match($regex, $preview_snippet, $matches, PREG_OFFSET_CAPTURE)) {
2021-11-30 05:08:56 +01:00
if (!isset($matches[1]) || $matches[1][1] === -1) {
2021-12-03 21:40:18 +01:00
throw new LogicException(
2021-10-13 16:49:42 +02:00
"Failed to match anything to 1st capturing group, "
2022-12-18 17:15:15 +01:00
. "or regex doesn't contain 1st capturing group, regex type " . $this->regex_type,
2021-10-13 16:49:42 +02:00
);
2021-10-13 16:32:40 +02:00
}
2021-11-30 05:08:56 +01:00
$this->selection_start = $this->selection_start + $matches[1][1];
$this->selection_end = $this->selection_start + strlen($matches[1][0]);
2016-12-08 04:38:57 +01:00
}
}
// reset preview start to beginning of line
$this->preview_start = (int)strrpos(
$file_contents,
"\n",
2022-12-18 17:15:15 +01:00
min($this->preview_start, $this->selection_start) - strlen($file_contents),
2016-12-08 04:38:57 +01:00
) + 1;
$this->selection_start = max($this->preview_start, $this->selection_start);
$this->selection_end = min($this->preview_end, $this->selection_end);
2017-01-28 06:39:16 +01:00
if ($this->preview_end - $this->selection_end > 200) {
$this->preview_end = (int)strrpos(
$file_contents,
"\n",
2022-12-18 17:15:15 +01:00
$this->selection_end + 200 - strlen($file_contents),
2017-01-28 06:39:16 +01:00
);
2017-02-08 07:39:49 +01:00
// if the line is over 200 characters long
if ($this->preview_end < $this->selection_end) {
$this->preview_end = $this->selection_end + 50;
}
2017-01-28 06:39:16 +01:00
}
$this->snippet = mb_strcut($file_contents, $this->preview_start, $this->preview_end - $this->preview_start);
// text is within snippet. It's 50% faster to cut it from the snippet than from the full text
$selection_length = $this->selection_end - $this->selection_start;
$this->text = mb_strcut($this->snippet, $this->selection_start - $this->preview_start, $selection_length);
2018-03-29 16:26:26 +02:00
2017-01-16 04:39:26 +01:00
// reset preview start to beginning of line
if ($file_contents !== '') {
$this->column_from = $this->selection_start -
(int)strrpos($file_contents, "\n", $this->selection_start - strlen($file_contents));
} else {
$this->column_from = $this->selection_start;
}
2017-01-16 04:39:26 +01:00
2018-03-29 16:26:26 +02:00
$newlines = substr_count($this->text, "\n");
2018-03-29 16:26:26 +02:00
if ($newlines) {
$last_newline_pos = strrpos($file_contents, "\n", $this->selection_end - strlen($file_contents) - 1);
$this->column_to = $this->selection_end - (int)$last_newline_pos;
2018-03-29 16:26:26 +02:00
} else {
$this->column_to = $this->column_from + strlen($this->text);
}
2018-03-29 16:26:26 +02:00
$this->end_line_number = $this->getLineNumber() + $newlines;
2016-12-08 04:38:57 +01:00
}
public function getLineNumber(): int
2016-12-08 04:38:57 +01:00
{
2019-08-06 23:11:25 +02:00
return $this->docblock_line_number ?: $this->raw_line_number;
2016-12-08 04:38:57 +01:00
}
public function getEndLineNumber(): int
{
$this->calculateRealLocation();
return $this->end_line_number;
}
public function getSnippet(): string
2016-12-08 04:38:57 +01:00
{
$this->calculateRealLocation();
return $this->snippet;
}
public function getSelectedText(): string
{
$this->calculateRealLocation();
return (string)$this->text;
}
public function getColumn(): int
2017-01-16 04:39:26 +01:00
{
$this->calculateRealLocation();
return $this->column_from;
}
public function getEndColumn(): int
{
$this->calculateRealLocation();
return $this->column_to;
2017-01-16 04:39:26 +01:00
}
2016-12-08 04:38:57 +01:00
/**
* @return array{0: int, 1: int}
2016-12-08 04:38:57 +01:00
*/
public function getSelectionBounds(): array
2016-12-08 04:38:57 +01:00
{
$this->calculateRealLocation();
return [$this->selection_start, $this->selection_end];
}
/**
* @return array{0: int, 1: int}
2016-12-08 04:38:57 +01:00
*/
public function getSnippetBounds(): array
2016-12-08 04:38:57 +01:00
{
$this->calculateRealLocation();
return [$this->preview_start, $this->preview_end];
}
2018-06-17 02:01:33 +02:00
public function getHash(): string
2018-06-17 02:01:33 +02:00
{
return $this->file_name . ' ' . $this->raw_file_start . $this->raw_file_end;
2018-06-17 02:01:33 +02:00
}
public function getShortSummary(): string
{
return $this->file_name . ':' . $this->getLineNumber() . ':' . $this->getColumn();
}
}