Install psalm as dev, sync scripts updates
This commit is contained in:
76
vendor/vimeo/psalm/src/Psalm/Aliases.php
vendored
Normal file
76
vendor/vimeo/psalm/src/Psalm/Aliases.php
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm;
|
||||
|
||||
final class Aliases
|
||||
{
|
||||
/**
|
||||
* @var array<lowercase-string, string>
|
||||
*/
|
||||
public $uses;
|
||||
|
||||
/**
|
||||
* @var array<lowercase-string, string>
|
||||
*/
|
||||
public $uses_flipped;
|
||||
|
||||
/**
|
||||
* @var array<lowercase-string, non-empty-string>
|
||||
*/
|
||||
public $functions;
|
||||
|
||||
/**
|
||||
* @var array<lowercase-string, string>
|
||||
*/
|
||||
public $functions_flipped;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $constants;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $constants_flipped;
|
||||
|
||||
/** @var string|null */
|
||||
public $namespace;
|
||||
|
||||
/** @var ?int */
|
||||
public $namespace_first_stmt_start;
|
||||
|
||||
/** @var ?int */
|
||||
public $uses_start;
|
||||
|
||||
/** @var ?int */
|
||||
public $uses_end;
|
||||
|
||||
/**
|
||||
* @param array<lowercase-string, string> $uses
|
||||
* @param array<lowercase-string, non-empty-string> $functions
|
||||
* @param array<string, string> $constants
|
||||
* @param array<lowercase-string, string> $uses_flipped
|
||||
* @param array<lowercase-string, string> $functions_flipped
|
||||
* @param array<string, string> $constants_flipped
|
||||
* @internal
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function __construct(
|
||||
?string $namespace = null,
|
||||
array $uses = [],
|
||||
array $functions = [],
|
||||
array $constants = [],
|
||||
array $uses_flipped = [],
|
||||
array $functions_flipped = [],
|
||||
array $constants_flipped = []
|
||||
) {
|
||||
$this->namespace = $namespace;
|
||||
$this->uses = $uses;
|
||||
$this->functions = $functions;
|
||||
$this->constants = $constants;
|
||||
$this->uses_flipped = $uses_flipped;
|
||||
$this->functions_flipped = $functions_flipped;
|
||||
$this->constants_flipped = $constants_flipped;
|
||||
}
|
||||
}
|
||||
407
vendor/vimeo/psalm/src/Psalm/CodeLocation.php
vendored
Normal file
407
vendor/vimeo/psalm/src/Psalm/CodeLocation.php
vendored
Normal file
@@ -0,0 +1,407 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm;
|
||||
|
||||
use Exception;
|
||||
use LogicException;
|
||||
use PhpParser;
|
||||
use Psalm\Internal\Analyzer\CommentAnalyzer;
|
||||
use Psalm\Internal\Analyzer\ProjectAnalyzer;
|
||||
use Psalm\Storage\ImmutableNonCloneableTrait;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function explode;
|
||||
use function max;
|
||||
use function mb_strcut;
|
||||
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;
|
||||
use function trim;
|
||||
|
||||
use const PREG_OFFSET_CAPTURE;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class CodeLocation
|
||||
{
|
||||
use ImmutableNonCloneableTrait;
|
||||
|
||||
/** @var string */
|
||||
public $file_path;
|
||||
|
||||
/** @var string */
|
||||
public $file_name;
|
||||
|
||||
/** @var int */
|
||||
public $raw_line_number;
|
||||
|
||||
private int $end_line_number = -1;
|
||||
|
||||
/** @var int */
|
||||
public $raw_file_start;
|
||||
|
||||
/** @var int */
|
||||
public $raw_file_end;
|
||||
|
||||
/** @var int */
|
||||
protected $file_start;
|
||||
|
||||
/** @var int */
|
||||
protected $file_end;
|
||||
|
||||
/** @var bool */
|
||||
protected $single_line;
|
||||
|
||||
/** @var int */
|
||||
protected $preview_start;
|
||||
|
||||
private int $preview_end = -1;
|
||||
|
||||
private int $selection_start = -1;
|
||||
|
||||
private int $selection_end = -1;
|
||||
|
||||
private int $column_from = -1;
|
||||
|
||||
private int $column_to = -1;
|
||||
|
||||
private string $snippet = '';
|
||||
|
||||
private ?string $text = null;
|
||||
|
||||
/** @var int|null */
|
||||
public $docblock_start;
|
||||
|
||||
private ?int $docblock_start_line_number = null;
|
||||
|
||||
/** @var int|null */
|
||||
protected $docblock_line_number;
|
||||
|
||||
private ?int $regex_type = null;
|
||||
|
||||
private bool $have_recalculated = false;
|
||||
|
||||
/** @var null|CodeLocation */
|
||||
public $previous_location;
|
||||
|
||||
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;
|
||||
|
||||
public function __construct(
|
||||
FileSource $file_source,
|
||||
PhpParser\Node $stmt,
|
||||
?CodeLocation $previous_location = null,
|
||||
bool $single_line = false,
|
||||
?int $regex_type = null,
|
||||
?string $selected_text = null,
|
||||
?int $comment_line = null
|
||||
) {
|
||||
/** @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 */
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-external-mutation-free
|
||||
* @psalm-suppress InaccessibleProperty Mainly used for caching
|
||||
*/
|
||||
private function calculateRealLocation(): void
|
||||
{
|
||||
if ($this->have_recalculated) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->have_recalculated = true;
|
||||
|
||||
$this->selection_start = $this->file_start;
|
||||
$this->selection_end = $this->file_end + 1;
|
||||
|
||||
$project_analyzer = ProjectAnalyzer::getInstance();
|
||||
|
||||
$codebase = $project_analyzer->getCodebase();
|
||||
|
||||
$file_contents = $codebase->getFileContents($this->file_path);
|
||||
|
||||
$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",
|
||||
$search_limit,
|
||||
);
|
||||
} else {
|
||||
$preview_end = false;
|
||||
}
|
||||
|
||||
// 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
|
||||
) {
|
||||
$preview_lines = explode(
|
||||
"\n",
|
||||
mb_strcut(
|
||||
$file_contents,
|
||||
$this->preview_start,
|
||||
$this->selection_start - $this->preview_start - 1,
|
||||
),
|
||||
);
|
||||
|
||||
$preview_offset = 0;
|
||||
|
||||
$comment_line_offset = $this->docblock_line_number - $this->docblock_start_line_number;
|
||||
|
||||
for ($i = 0; $i < $comment_line_offset; ++$i) {
|
||||
$preview_offset += strlen($preview_lines[$i]) + 1;
|
||||
}
|
||||
|
||||
if (!isset($preview_lines[$i])) {
|
||||
throw new Exception('Should have offset');
|
||||
}
|
||||
|
||||
$key_line = $preview_lines[$i];
|
||||
|
||||
$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:
|
||||
$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:
|
||||
$regex = '/@(?:psalm-)?return[ \t]+' . CommentAnalyzer::TYPE_REGEX . '/';
|
||||
break;
|
||||
|
||||
case self::FUNCTION_PHPDOC_METHOD:
|
||||
$regex = '/@(?:psalm-)?method[ \t]+(.*)/';
|
||||
break;
|
||||
|
||||
case self::FUNCTION_PHPDOC_PARAM_TYPE:
|
||||
$regex = '/@(?:psalm-)?param[ \t]+' . CommentAnalyzer::TYPE_REGEX . '/';
|
||||
break;
|
||||
|
||||
case self::FUNCTION_PARAM_VAR:
|
||||
$regex = '/(\$[^ ]*)/';
|
||||
break;
|
||||
|
||||
case self::CATCH_VAR:
|
||||
$regex = '/(\$[^ ^\)]*)/';
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnexpectedValueException('Unrecognised regex type ' . $this->regex_type);
|
||||
}
|
||||
|
||||
$preview_snippet = mb_strcut(
|
||||
$file_contents,
|
||||
$this->selection_start,
|
||||
$this->selection_end - $this->selection_start,
|
||||
);
|
||||
|
||||
if ($this->text) {
|
||||
$regex = '/(' . str_replace(',', ',[ ]*', preg_quote($this->text, '/')) . ')/';
|
||||
}
|
||||
|
||||
if (preg_match($regex, $preview_snippet, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
if (!isset($matches[1]) || $matches[1][1] === -1) {
|
||||
throw new LogicException(
|
||||
"Failed to match anything to 1st capturing group, "
|
||||
. "or regex doesn't contain 1st capturing group, regex type " . $this->regex_type,
|
||||
);
|
||||
}
|
||||
$this->selection_start = $this->selection_start + $matches[1][1];
|
||||
$this->selection_end = $this->selection_start + strlen($matches[1][0]);
|
||||
}
|
||||
}
|
||||
|
||||
// reset preview start to beginning of line
|
||||
$this->preview_start = (int)strrpos(
|
||||
$file_contents,
|
||||
"\n",
|
||||
min($this->preview_start, $this->selection_start) - strlen($file_contents),
|
||||
) + 1;
|
||||
|
||||
$this->selection_start = max($this->preview_start, $this->selection_start);
|
||||
$this->selection_end = min($this->preview_end, $this->selection_end);
|
||||
|
||||
if ($this->preview_end - $this->selection_end > 200) {
|
||||
$this->preview_end = (int)strrpos(
|
||||
$file_contents,
|
||||
"\n",
|
||||
$this->selection_end + 200 - strlen($file_contents),
|
||||
);
|
||||
|
||||
// if the line is over 200 characters long
|
||||
if ($this->preview_end < $this->selection_end) {
|
||||
$this->preview_end = $this->selection_end + 50;
|
||||
}
|
||||
}
|
||||
|
||||
$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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
$newlines = substr_count($this->text, "\n");
|
||||
|
||||
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;
|
||||
} else {
|
||||
$this->column_to = $this->column_from + strlen($this->text);
|
||||
}
|
||||
|
||||
$this->end_line_number = $this->getLineNumber() + $newlines;
|
||||
}
|
||||
|
||||
public function getLineNumber(): int
|
||||
{
|
||||
return $this->docblock_line_number ?: $this->raw_line_number;
|
||||
}
|
||||
|
||||
public function getEndLineNumber(): int
|
||||
{
|
||||
$this->calculateRealLocation();
|
||||
|
||||
return $this->end_line_number;
|
||||
}
|
||||
|
||||
public function getSnippet(): string
|
||||
{
|
||||
$this->calculateRealLocation();
|
||||
|
||||
return $this->snippet;
|
||||
}
|
||||
|
||||
public function getSelectedText(): string
|
||||
{
|
||||
$this->calculateRealLocation();
|
||||
|
||||
return (string)$this->text;
|
||||
}
|
||||
|
||||
public function getColumn(): int
|
||||
{
|
||||
$this->calculateRealLocation();
|
||||
|
||||
return $this->column_from;
|
||||
}
|
||||
|
||||
public function getEndColumn(): int
|
||||
{
|
||||
$this->calculateRealLocation();
|
||||
|
||||
return $this->column_to;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: int, 1: int}
|
||||
*/
|
||||
public function getSelectionBounds(): array
|
||||
{
|
||||
$this->calculateRealLocation();
|
||||
|
||||
return [$this->selection_start, $this->selection_end];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: int, 1: int}
|
||||
*/
|
||||
public function getSnippetBounds(): array
|
||||
{
|
||||
$this->calculateRealLocation();
|
||||
|
||||
return [$this->preview_start, $this->preview_end];
|
||||
}
|
||||
|
||||
public function getHash(): string
|
||||
{
|
||||
return $this->file_name . ' ' . $this->raw_file_start . $this->raw_file_end;
|
||||
}
|
||||
|
||||
public function getShortSummary(): string
|
||||
{
|
||||
return $this->file_name . ':' . $this->getLineNumber() . ':' . $this->getColumn();
|
||||
}
|
||||
}
|
||||
33
vendor/vimeo/psalm/src/Psalm/CodeLocation/DocblockTypeLocation.php
vendored
Normal file
33
vendor/vimeo/psalm/src/Psalm/CodeLocation/DocblockTypeLocation.php
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\CodeLocation;
|
||||
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\FileSource;
|
||||
|
||||
/** @psalm-immutable */
|
||||
class DocblockTypeLocation extends CodeLocation
|
||||
{
|
||||
public function __construct(
|
||||
FileSource $file_source,
|
||||
int $file_start,
|
||||
int $file_end,
|
||||
int $line_number
|
||||
) {
|
||||
$this->file_start = $file_start;
|
||||
// matches how CodeLocation works
|
||||
$this->file_end = $file_end - 1;
|
||||
|
||||
$this->raw_file_start = $file_start;
|
||||
$this->raw_file_end = $file_end;
|
||||
$this->raw_line_number = $line_number;
|
||||
|
||||
$this->file_path = $file_source->getFilePath();
|
||||
$this->file_name = $file_source->getFileName();
|
||||
$this->single_line = false;
|
||||
|
||||
$this->preview_start = $this->file_start;
|
||||
|
||||
$this->docblock_line_number = $line_number;
|
||||
}
|
||||
}
|
||||
36
vendor/vimeo/psalm/src/Psalm/CodeLocation/ParseErrorLocation.php
vendored
Normal file
36
vendor/vimeo/psalm/src/Psalm/CodeLocation/ParseErrorLocation.php
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\CodeLocation;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
|
||||
use function substr;
|
||||
use function substr_count;
|
||||
|
||||
/** @psalm-immutable */
|
||||
class ParseErrorLocation extends CodeLocation
|
||||
{
|
||||
public function __construct(
|
||||
PhpParser\Error $error,
|
||||
string $file_contents,
|
||||
string $file_path,
|
||||
string $file_name
|
||||
) {
|
||||
/** @psalm-suppress PossiblyUndefinedStringArrayOffset, ImpureMethodCall */
|
||||
$this->file_start = (int)$error->getAttributes()['startFilePos'];
|
||||
/** @psalm-suppress PossiblyUndefinedStringArrayOffset, ImpureMethodCall */
|
||||
$this->file_end = (int)$error->getAttributes()['endFilePos'];
|
||||
$this->raw_file_start = $this->file_start;
|
||||
$this->raw_file_end = $this->file_end;
|
||||
$this->file_path = $file_path;
|
||||
$this->file_name = $file_name;
|
||||
$this->single_line = false;
|
||||
|
||||
$this->preview_start = $this->file_start;
|
||||
$this->raw_line_number = substr_count(
|
||||
substr($file_contents, 0, $this->file_start),
|
||||
"\n",
|
||||
) + 1;
|
||||
}
|
||||
}
|
||||
34
vendor/vimeo/psalm/src/Psalm/CodeLocation/Raw.php
vendored
Normal file
34
vendor/vimeo/psalm/src/Psalm/CodeLocation/Raw.php
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\CodeLocation;
|
||||
|
||||
use Psalm\CodeLocation;
|
||||
|
||||
use function substr;
|
||||
use function substr_count;
|
||||
|
||||
/** @psalm-immutable */
|
||||
class Raw extends CodeLocation
|
||||
{
|
||||
public function __construct(
|
||||
string $file_contents,
|
||||
string $file_path,
|
||||
string $file_name,
|
||||
int $file_start,
|
||||
int $file_end
|
||||
) {
|
||||
$this->file_start = $file_start;
|
||||
$this->file_end = $file_end;
|
||||
$this->raw_file_start = $this->file_start;
|
||||
$this->raw_file_end = $this->file_end;
|
||||
$this->file_path = $file_path;
|
||||
$this->file_name = $file_name;
|
||||
$this->single_line = false;
|
||||
|
||||
$this->preview_start = $this->file_start;
|
||||
$this->raw_line_number = substr_count(
|
||||
substr($file_contents, 0, $this->file_start),
|
||||
"\n",
|
||||
) + 1;
|
||||
}
|
||||
}
|
||||
1989
vendor/vimeo/psalm/src/Psalm/Codebase.php
vendored
Normal file
1989
vendor/vimeo/psalm/src/Psalm/Codebase.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2662
vendor/vimeo/psalm/src/Psalm/Config.php
vendored
Normal file
2662
vendor/vimeo/psalm/src/Psalm/Config.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
309
vendor/vimeo/psalm/src/Psalm/Config/Creator.php
vendored
Normal file
309
vendor/vimeo/psalm/src/Psalm/Config/Creator.php
vendored
Normal file
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Config;
|
||||
|
||||
use JsonException;
|
||||
use Psalm\Config;
|
||||
use Psalm\Exception\ConfigCreationException;
|
||||
use Psalm\Internal\Analyzer\IssueData;
|
||||
use Psalm\Internal\Composer;
|
||||
|
||||
use function array_filter;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_shift;
|
||||
use function array_sum;
|
||||
use function array_unique;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function file_exists;
|
||||
use function file_get_contents;
|
||||
use function glob;
|
||||
use function implode;
|
||||
use function is_array;
|
||||
use function is_dir;
|
||||
use function json_decode;
|
||||
use function ksort;
|
||||
use function max;
|
||||
use function preg_replace;
|
||||
use function sort;
|
||||
use function str_replace;
|
||||
use function strpos;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const GLOB_NOSORT;
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
/** @internal */
|
||||
final class Creator
|
||||
{
|
||||
private const TEMPLATE = '<?xml version="1.0"?>
|
||||
<psalm
|
||||
errorLevel="1"
|
||||
resolveFromConfigFile="true"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://getpsalm.org/schema/config"
|
||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||
findUnusedBaselineEntry="true"
|
||||
>
|
||||
<projectFiles>
|
||||
<directory name="src" />
|
||||
<ignoreFiles>
|
||||
<directory name="vendor" />
|
||||
</ignoreFiles>
|
||||
</projectFiles>
|
||||
</psalm>
|
||||
';
|
||||
|
||||
/**
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public static function getContents(
|
||||
string $current_dir,
|
||||
?string $suggested_dir,
|
||||
int $level,
|
||||
string $vendor_dir
|
||||
): string {
|
||||
$paths = self::getPaths($current_dir, $suggested_dir);
|
||||
|
||||
$template = str_replace(
|
||||
'<directory name="src" />',
|
||||
implode("\n ", $paths),
|
||||
self::TEMPLATE,
|
||||
);
|
||||
|
||||
if (is_dir($current_dir . DIRECTORY_SEPARATOR . $vendor_dir)) {
|
||||
$template = str_replace(
|
||||
'<directory name="vendor" />',
|
||||
'<directory name="' . $vendor_dir . '" />',
|
||||
$template,
|
||||
);
|
||||
} else {
|
||||
$template = str_replace(
|
||||
'<directory name="vendor" />',
|
||||
'',
|
||||
$template,
|
||||
);
|
||||
}
|
||||
|
||||
/** @var non-empty-string */
|
||||
return str_replace(
|
||||
'errorLevel="1"',
|
||||
'errorLevel="' . $level . '"',
|
||||
$template,
|
||||
);
|
||||
}
|
||||
|
||||
public static function createBareConfig(
|
||||
string $current_dir,
|
||||
?string $suggested_dir,
|
||||
string $vendor_dir
|
||||
): Config {
|
||||
$config_contents = self::getContents($current_dir, $suggested_dir, 1, $vendor_dir);
|
||||
|
||||
return Config::loadFromXML($current_dir, $config_contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<IssueData> $issues
|
||||
*/
|
||||
public static function getLevel(array $issues, int $counted_types): int
|
||||
{
|
||||
if ($counted_types === 0) {
|
||||
$counted_types = 1;
|
||||
}
|
||||
|
||||
$issues_at_level = [];
|
||||
|
||||
foreach ($issues as $issue) {
|
||||
$issue_type = $issue->type;
|
||||
$issue_level = $issue->error_level;
|
||||
|
||||
if ($issue_level < 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// exclude some directories that are probably ignorable
|
||||
if (strpos($issue->file_path, 'vendor') || strpos($issue->file_path, 'stub')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($issues_at_level[$issue_level][$issue_type])) {
|
||||
$issues_at_level[$issue_level][$issue_type] = 0;
|
||||
}
|
||||
|
||||
$issues_at_level[$issue_level][$issue_type] += 100 / $counted_types;
|
||||
}
|
||||
|
||||
foreach ($issues_at_level as $level => $issues) {
|
||||
ksort($issues);
|
||||
|
||||
// remove any issues where < 0.1% of expressions are affected
|
||||
$filtered_issues = array_filter(
|
||||
$issues,
|
||||
static fn($amount): bool => $amount > 0.1,
|
||||
);
|
||||
|
||||
if (array_sum($filtered_issues) > 0.5) {
|
||||
$issues_at_level[$level] = $filtered_issues;
|
||||
} else {
|
||||
unset($issues_at_level[$level]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$issues_at_level) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (count($issues_at_level) === 1) {
|
||||
return array_keys($issues_at_level)[0] + 1;
|
||||
}
|
||||
|
||||
return max(...array_keys($issues_at_level)) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-list<string>
|
||||
*/
|
||||
public static function getPaths(string $current_dir, ?string $suggested_dir): array
|
||||
{
|
||||
$replacements = [];
|
||||
|
||||
if ($suggested_dir) {
|
||||
if (is_dir($current_dir . DIRECTORY_SEPARATOR . $suggested_dir)) {
|
||||
$replacements[] = '<directory name="' . $suggested_dir . '" />';
|
||||
} else {
|
||||
$bad_dir_path = $current_dir . DIRECTORY_SEPARATOR . $suggested_dir;
|
||||
|
||||
throw new ConfigCreationException(
|
||||
'The given path "' . $bad_dir_path . '" does not appear to be a directory',
|
||||
);
|
||||
}
|
||||
} elseif (is_dir($current_dir . DIRECTORY_SEPARATOR . 'src')) {
|
||||
$replacements[] = '<directory name="src" />';
|
||||
} else {
|
||||
$composer_json_location = Composer::getJsonFilePath($current_dir);
|
||||
|
||||
if (!file_exists($composer_json_location)) {
|
||||
throw new ConfigCreationException(
|
||||
'Problem during source autodiscovery - could not find composer.json during initialization. '
|
||||
. 'If your project doesn\'t use Composer autoloader you will need to run '
|
||||
. '`psalm --init source_folder`, e.g. `psalm --init library` if your source files '
|
||||
. 'reside in `library` folder',
|
||||
);
|
||||
}
|
||||
try {
|
||||
$composer_json = json_decode(
|
||||
file_get_contents($composer_json_location),
|
||||
true,
|
||||
512,
|
||||
JSON_THROW_ON_ERROR,
|
||||
);
|
||||
} catch (JsonException $e) {
|
||||
throw new ConfigCreationException(
|
||||
'Invalid composer.json at ' . $composer_json_location . ': ' . $e->getMessage(),
|
||||
);
|
||||
}
|
||||
if (!$composer_json) {
|
||||
throw new ConfigCreationException('Invalid composer.json at ' . $composer_json_location);
|
||||
}
|
||||
|
||||
if (!is_array($composer_json)) {
|
||||
throw new ConfigCreationException('Invalid composer.json at ' . $composer_json_location);
|
||||
}
|
||||
|
||||
$replacements = self::getPsr4Or0Paths($current_dir, $composer_json);
|
||||
|
||||
if (!$replacements) {
|
||||
throw new ConfigCreationException(
|
||||
'Could not located any PSR-0 or PSR-4-compatible paths in ' . $composer_json_location,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $replacements;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
* @psalm-suppress MixedAssignment
|
||||
* @psalm-suppress MixedArgument
|
||||
*/
|
||||
private static function getPsr4Or0Paths(string $current_dir, array $composer_json): array
|
||||
{
|
||||
$psr_paths = array_merge(
|
||||
$composer_json['autoload']['psr-4'] ?? [],
|
||||
$composer_json['autoload']['psr-0'] ?? [],
|
||||
);
|
||||
|
||||
if (!$psr_paths) {
|
||||
return self::guessPhpFileDirs($current_dir);
|
||||
}
|
||||
|
||||
$nodes = [];
|
||||
|
||||
foreach ($psr_paths as $paths) {
|
||||
if (!is_array($paths)) {
|
||||
$paths = [$paths];
|
||||
}
|
||||
|
||||
foreach ($paths as $path) {
|
||||
if ($path === '') {
|
||||
$nodes = [...$nodes, ...self::guessPhpFileDirs($current_dir)];
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = preg_replace('@[/\\\]$@', '', $path, 1);
|
||||
|
||||
if ($path !== 'tests') {
|
||||
$nodes[] = '<directory name="' . $path . '" />';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$nodes = array_unique($nodes);
|
||||
|
||||
sort($nodes);
|
||||
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function guessPhpFileDirs(string $current_dir): array
|
||||
{
|
||||
$nodes = [];
|
||||
|
||||
/** @var string[] */
|
||||
$php_files = array_merge(
|
||||
glob($current_dir . DIRECTORY_SEPARATOR . '*.php', GLOB_NOSORT),
|
||||
glob($current_dir . DIRECTORY_SEPARATOR . '**/*.php', GLOB_NOSORT),
|
||||
glob($current_dir . DIRECTORY_SEPARATOR . '**/**/*.php', GLOB_NOSORT),
|
||||
);
|
||||
|
||||
foreach ($php_files as $php_file) {
|
||||
$php_file = str_replace($current_dir . DIRECTORY_SEPARATOR, '', $php_file);
|
||||
|
||||
$parts = explode(DIRECTORY_SEPARATOR, $php_file);
|
||||
|
||||
if (!$parts[0]) {
|
||||
array_shift($parts);
|
||||
}
|
||||
|
||||
if ($parts[0] === 'vendor' || $parts[0] === 'tests') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($parts) === 1) {
|
||||
$nodes[] = '<file name="' . $php_file . '" />';
|
||||
} else {
|
||||
$nodes[] = '<directory name="' . $parts[0] . '" />';
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($nodes));
|
||||
}
|
||||
}
|
||||
66
vendor/vimeo/psalm/src/Psalm/Config/ErrorLevelFileFilter.php
vendored
Normal file
66
vendor/vimeo/psalm/src/Psalm/Config/ErrorLevelFileFilter.php
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Config;
|
||||
|
||||
use Psalm\Config;
|
||||
use Psalm\Exception\ConfigException;
|
||||
use SimpleXMLElement;
|
||||
|
||||
use function in_array;
|
||||
|
||||
/** @internal */
|
||||
final class ErrorLevelFileFilter extends FileFilter
|
||||
{
|
||||
private string $error_level = '';
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function loadFromArray(
|
||||
array $config,
|
||||
string $base_dir,
|
||||
bool $inclusive
|
||||
): ErrorLevelFileFilter {
|
||||
$filter = parent::loadFromArray($config, $base_dir, $inclusive);
|
||||
|
||||
if (isset($config['type'])) {
|
||||
$filter->error_level = (string) $config['type'];
|
||||
|
||||
if (!in_array($filter->error_level, Config::$ERROR_LEVELS, true)) {
|
||||
throw new ConfigException('Unexpected error level ' . $filter->error_level);
|
||||
}
|
||||
} else {
|
||||
throw new ConfigException('<type> element expects a level');
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function loadFromXMLElement(
|
||||
SimpleXMLElement $e,
|
||||
string $base_dir,
|
||||
bool $inclusive
|
||||
): ErrorLevelFileFilter {
|
||||
$filter = parent::loadFromXMLElement($e, $base_dir, $inclusive);
|
||||
|
||||
if (isset($e['type'])) {
|
||||
$filter->error_level = (string) $e['type'];
|
||||
|
||||
if (!in_array($filter->error_level, Config::$ERROR_LEVELS, true)) {
|
||||
throw new ConfigException('Unexpected error level ' . $filter->error_level);
|
||||
}
|
||||
} else {
|
||||
throw new ConfigException('<type> element expects a level');
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
public function getErrorLevel(): string
|
||||
{
|
||||
return $this->error_level;
|
||||
}
|
||||
}
|
||||
591
vendor/vimeo/psalm/src/Psalm/Config/FileFilter.php
vendored
Normal file
591
vendor/vimeo/psalm/src/Psalm/Config/FileFilter.php
vendored
Normal file
@@ -0,0 +1,591 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Config;
|
||||
|
||||
use FilesystemIterator;
|
||||
use Psalm\Exception\ConfigException;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SimpleXMLElement;
|
||||
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function explode;
|
||||
use function glob;
|
||||
use function in_array;
|
||||
use function is_dir;
|
||||
use function is_iterable;
|
||||
use function preg_match;
|
||||
use function readlink;
|
||||
use function realpath;
|
||||
use function restore_error_handler;
|
||||
use function rtrim;
|
||||
use function set_error_handler;
|
||||
use function str_replace;
|
||||
use function stripos;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use const E_WARNING;
|
||||
use const GLOB_NOSORT;
|
||||
use const GLOB_ONLYDIR;
|
||||
|
||||
/**
|
||||
* @psalm-consistent-constructor
|
||||
*/
|
||||
class FileFilter
|
||||
{
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $directories = [];
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $files = [];
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $fq_classlike_names = [];
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $fq_classlike_patterns = [];
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $method_ids = [];
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $property_ids = [];
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $class_constant_ids = [];
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $var_names = [];
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
protected $files_lowercase = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $inclusive;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
protected $ignore_type_stats = [];
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
protected $declare_strict_types = [];
|
||||
|
||||
public function __construct(bool $inclusive)
|
||||
{
|
||||
$this->inclusive = $inclusive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function loadFromArray(
|
||||
array $config,
|
||||
string $base_dir,
|
||||
bool $inclusive
|
||||
) {
|
||||
$allow_missing_files = ($config['allowMissingFiles'] ?? false) === true;
|
||||
|
||||
$filter = new static($inclusive);
|
||||
|
||||
if (isset($config['directory']) && is_iterable($config['directory'])) {
|
||||
/** @var array $directory */
|
||||
foreach ($config['directory'] as $directory) {
|
||||
$directory_path = (string) ($directory['name'] ?? '');
|
||||
$ignore_type_stats = (bool) ($directory['ignoreTypeStats'] ?? false);
|
||||
$resolve_symlinks = (bool) ($directory['resolveSymlinks'] ?? false);
|
||||
$declare_strict_types = (bool) ($directory['useStrictTypes'] ?? false);
|
||||
|
||||
if ($directory_path[0] === '/' && DIRECTORY_SEPARATOR === '/') {
|
||||
$prospective_directory_path = $directory_path;
|
||||
} else {
|
||||
$prospective_directory_path = $base_dir . DIRECTORY_SEPARATOR . $directory_path;
|
||||
}
|
||||
|
||||
if (strpos($prospective_directory_path, '*') !== false) {
|
||||
$globs = array_map(
|
||||
'realpath',
|
||||
glob($prospective_directory_path, GLOB_ONLYDIR),
|
||||
);
|
||||
|
||||
if (empty($globs)) {
|
||||
if ($allow_missing_files) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new ConfigException(
|
||||
'Could not resolve config path to ' . $base_dir
|
||||
. DIRECTORY_SEPARATOR . $directory_path,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($globs as $glob_index => $directory_path) {
|
||||
if (!$directory_path) {
|
||||
if ($allow_missing_files) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new ConfigException(
|
||||
'Could not resolve config path to ' . $base_dir
|
||||
. DIRECTORY_SEPARATOR . $directory_path . ':' . $glob_index,
|
||||
);
|
||||
}
|
||||
|
||||
if ($ignore_type_stats && $filter instanceof ProjectFileFilter) {
|
||||
$filter->ignore_type_stats[$directory_path] = true;
|
||||
}
|
||||
|
||||
if ($declare_strict_types && $filter instanceof ProjectFileFilter) {
|
||||
$filter->declare_strict_types[$directory_path] = true;
|
||||
}
|
||||
|
||||
$filter->addDirectory($directory_path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$directory_path = realpath($prospective_directory_path);
|
||||
|
||||
if (!$directory_path) {
|
||||
if ($allow_missing_files) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new ConfigException(
|
||||
'Could not resolve config path to ' . $prospective_directory_path,
|
||||
);
|
||||
}
|
||||
|
||||
if (!is_dir($directory_path)) {
|
||||
throw new ConfigException(
|
||||
$base_dir . DIRECTORY_SEPARATOR . $directory_path
|
||||
. ' is not a directory',
|
||||
);
|
||||
}
|
||||
|
||||
if ($resolve_symlinks) {
|
||||
/** @var RecursiveDirectoryIterator */
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($directory_path, FilesystemIterator::SKIP_DOTS),
|
||||
);
|
||||
$iterator->rewind();
|
||||
|
||||
while ($iterator->valid()) {
|
||||
if ($iterator->isLink()) {
|
||||
$linked_path = readlink($iterator->getPathname());
|
||||
|
||||
if (stripos($linked_path, $directory_path) !== 0) {
|
||||
if ($ignore_type_stats && $filter instanceof ProjectFileFilter) {
|
||||
$filter->ignore_type_stats[$directory_path] = true;
|
||||
}
|
||||
|
||||
if ($declare_strict_types && $filter instanceof ProjectFileFilter) {
|
||||
$filter->declare_strict_types[$directory_path] = true;
|
||||
}
|
||||
|
||||
if (is_dir($linked_path)) {
|
||||
$filter->addDirectory($linked_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$iterator->next();
|
||||
}
|
||||
|
||||
$iterator->next();
|
||||
}
|
||||
|
||||
if ($ignore_type_stats && $filter instanceof ProjectFileFilter) {
|
||||
$filter->ignore_type_stats[$directory_path] = true;
|
||||
}
|
||||
|
||||
if ($declare_strict_types && $filter instanceof ProjectFileFilter) {
|
||||
$filter->declare_strict_types[$directory_path] = true;
|
||||
}
|
||||
|
||||
$filter->addDirectory($directory_path);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($config['file']) && is_iterable($config['file'])) {
|
||||
/** @var array $file */
|
||||
foreach ($config['file'] as $file) {
|
||||
$file_path = (string) ($file['name'] ?? '');
|
||||
|
||||
if ($file_path[0] === '/' && DIRECTORY_SEPARATOR === '/') {
|
||||
$prospective_file_path = $file_path;
|
||||
} else {
|
||||
$prospective_file_path = $base_dir . DIRECTORY_SEPARATOR . $file_path;
|
||||
}
|
||||
|
||||
if (strpos($prospective_file_path, '*') !== false) {
|
||||
$globs = array_map(
|
||||
'realpath',
|
||||
array_filter(
|
||||
glob($prospective_file_path, GLOB_NOSORT),
|
||||
'file_exists',
|
||||
),
|
||||
);
|
||||
|
||||
if (empty($globs)) {
|
||||
if ($allow_missing_files) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new ConfigException(
|
||||
'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR .
|
||||
$file_path,
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($globs as $glob_index => $file_path) {
|
||||
if (!$file_path && !$allow_missing_files) {
|
||||
throw new ConfigException(
|
||||
'Could not resolve config path to ' . $base_dir . DIRECTORY_SEPARATOR .
|
||||
$file_path . ':' . $glob_index,
|
||||
);
|
||||
}
|
||||
$filter->addFile($file_path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$file_path = realpath($prospective_file_path);
|
||||
|
||||
if (!$file_path && !$allow_missing_files) {
|
||||
throw new ConfigException(
|
||||
'Could not resolve config path to ' . $prospective_file_path,
|
||||
);
|
||||
}
|
||||
|
||||
$filter->addFile($file_path);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($config['referencedClass']) && is_iterable($config['referencedClass'])) {
|
||||
/** @var array $referenced_class */
|
||||
foreach ($config['referencedClass'] as $referenced_class) {
|
||||
$class_name = strtolower((string) ($referenced_class['name'] ?? ''));
|
||||
|
||||
if (strpos($class_name, '*') !== false) {
|
||||
$regex = '/' . str_replace('*', '.*', str_replace('\\', '\\\\', $class_name)) . '/i';
|
||||
$filter->fq_classlike_patterns[] = $regex;
|
||||
} else {
|
||||
$filter->fq_classlike_names[] = $class_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($config['referencedMethod']) && is_iterable($config['referencedMethod'])) {
|
||||
/** @var array $referenced_method */
|
||||
foreach ($config['referencedMethod'] as $referenced_method) {
|
||||
$method_id = (string) ($referenced_method['name'] ?? '');
|
||||
|
||||
if (!preg_match('/^[^:]+::[^:]+$/', $method_id) && !static::isRegularExpression($method_id)) {
|
||||
throw new ConfigException(
|
||||
'Invalid referencedMethod ' . $method_id,
|
||||
);
|
||||
}
|
||||
|
||||
$filter->method_ids[] = strtolower($method_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($config['referencedFunction']) && is_iterable($config['referencedFunction'])) {
|
||||
/** @var array $referenced_function */
|
||||
foreach ($config['referencedFunction'] as $referenced_function) {
|
||||
$filter->method_ids[] = strtolower((string) ($referenced_function['name'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($config['referencedProperty']) && is_iterable($config['referencedProperty'])) {
|
||||
/** @var array $referenced_property */
|
||||
foreach ($config['referencedProperty'] as $referenced_property) {
|
||||
$filter->property_ids[] = strtolower((string) ($referenced_property['name'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($config['referencedConstant']) && is_iterable($config['referencedConstant'])) {
|
||||
/** @var array $referenced_constant */
|
||||
foreach ($config['referencedConstant'] as $referenced_constant) {
|
||||
$filter->class_constant_ids[] = strtolower((string) ($referenced_constant['name'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($config['referencedVariable']) && is_iterable($config['referencedVariable'])) {
|
||||
/** @var array $referenced_variable */
|
||||
foreach ($config['referencedVariable'] as $referenced_variable) {
|
||||
$filter->var_names[] = strtolower((string) ($referenced_variable['name'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function loadFromXMLElement(
|
||||
SimpleXMLElement $e,
|
||||
string $base_dir,
|
||||
bool $inclusive
|
||||
) {
|
||||
$config = [];
|
||||
$config['allowMissingFiles'] = ((string) $e['allowMissingFiles']) === 'true';
|
||||
|
||||
if ($e->directory) {
|
||||
$config['directory'] = [];
|
||||
/** @var SimpleXMLElement $directory */
|
||||
foreach ($e->directory as $directory) {
|
||||
$config['directory'][] = [
|
||||
'name' => (string) $directory['name'],
|
||||
'ignoreTypeStats' => strtolower((string) ($directory['ignoreTypeStats'] ?? '')) === 'true',
|
||||
'resolveSymlinks' => strtolower((string) ($directory['resolveSymlinks'] ?? '')) === 'true',
|
||||
'useStrictTypes' => strtolower((string) ($directory['useStrictTypes'] ?? '')) === 'true',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($e->file) {
|
||||
$config['file'] = [];
|
||||
/** @var SimpleXMLElement $file */
|
||||
foreach ($e->file as $file) {
|
||||
$config['file'][]['name'] = (string) $file['name'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($e->referencedClass) {
|
||||
$config['referencedClass'] = [];
|
||||
/** @var SimpleXMLElement $referenced_class */
|
||||
foreach ($e->referencedClass as $referenced_class) {
|
||||
$config['referencedClass'][]['name'] = strtolower((string)$referenced_class['name']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($e->referencedMethod) {
|
||||
$config['referencedMethod'] = [];
|
||||
/** @var SimpleXMLElement $referenced_method */
|
||||
foreach ($e->referencedMethod as $referenced_method) {
|
||||
$config['referencedMethod'][]['name'] = (string)$referenced_method['name'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($e->referencedFunction) {
|
||||
$config['referencedFunction'] = [];
|
||||
/** @var SimpleXMLElement $referenced_function */
|
||||
foreach ($e->referencedFunction as $referenced_function) {
|
||||
$config['referencedFunction'][]['name'] = strtolower((string)$referenced_function['name']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($e->referencedProperty) {
|
||||
$config['referencedProperty'] = [];
|
||||
/** @var SimpleXMLElement $referenced_property */
|
||||
foreach ($e->referencedProperty as $referenced_property) {
|
||||
$config['referencedProperty'][]['name'] = strtolower((string)$referenced_property['name']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($e->referencedConstant) {
|
||||
$config['referencedConstant'] = [];
|
||||
/** @var SimpleXMLElement $referenced_constant */
|
||||
foreach ($e->referencedConstant as $referenced_constant) {
|
||||
$config['referencedConstant'][]['name'] = strtolower((string)$referenced_constant['name']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($e->referencedVariable) {
|
||||
$config['referencedVariable'] = [];
|
||||
|
||||
/** @var SimpleXMLElement $referenced_variable */
|
||||
foreach ($e->referencedVariable as $referenced_variable) {
|
||||
$config['referencedVariable'][]['name'] = strtolower((string)$referenced_variable['name']);
|
||||
}
|
||||
}
|
||||
|
||||
return self::loadFromArray($config, $base_dir, $inclusive);
|
||||
}
|
||||
|
||||
private static function isRegularExpression(string $string): bool
|
||||
{
|
||||
set_error_handler(
|
||||
static fn(): bool => true,
|
||||
E_WARNING,
|
||||
);
|
||||
$is_regexp = preg_match($string, '') !== false;
|
||||
restore_error_handler();
|
||||
|
||||
return $is_regexp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
protected static function slashify(string $str): string
|
||||
{
|
||||
return rtrim($str, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
public function allows(string $file_name, bool $case_sensitive = false): bool
|
||||
{
|
||||
if ($this->inclusive) {
|
||||
foreach ($this->directories as $include_dir) {
|
||||
if ($case_sensitive) {
|
||||
if (strpos($file_name, $include_dir) === 0) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (stripos($file_name, $include_dir) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($case_sensitive) {
|
||||
if (in_array($file_name, $this->files, true)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (in_array(strtolower($file_name), $this->files_lowercase, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// exclusive
|
||||
foreach ($this->directories as $exclude_dir) {
|
||||
if ($case_sensitive) {
|
||||
if (strpos($file_name, $exclude_dir) === 0) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (stripos($file_name, $exclude_dir) === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($case_sensitive) {
|
||||
if (in_array($file_name, $this->files, true)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (in_array(strtolower($file_name), $this->files_lowercase, true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function allowsClass(string $fq_classlike_name): bool
|
||||
{
|
||||
if ($this->fq_classlike_patterns) {
|
||||
foreach ($this->fq_classlike_patterns as $pattern) {
|
||||
if (preg_match($pattern, $fq_classlike_name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return in_array(strtolower($fq_classlike_name), $this->fq_classlike_names, true);
|
||||
}
|
||||
|
||||
public function allowsMethod(string $method_id): bool
|
||||
{
|
||||
if (!$this->method_ids) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preg_match('/^[^:]+::[^:]+$/', $method_id)) {
|
||||
$method_stub = '*::' . explode('::', $method_id)[1];
|
||||
|
||||
foreach ($this->method_ids as $config_method_id) {
|
||||
if ($config_method_id === $method_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($config_method_id === $method_stub) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($config_method_id[0] === '/' && preg_match($config_method_id, $method_id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($method_id, $this->method_ids, true);
|
||||
}
|
||||
|
||||
public function allowsProperty(string $property_id): bool
|
||||
{
|
||||
return in_array(strtolower($property_id), $this->property_ids, true);
|
||||
}
|
||||
|
||||
public function allowsClassConstant(string $constant_id): bool
|
||||
{
|
||||
return in_array(strtolower($constant_id), $this->class_constant_ids, true);
|
||||
}
|
||||
|
||||
public function allowsVariable(string $var_name): bool
|
||||
{
|
||||
return in_array(strtolower($var_name), $this->var_names, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getDirectories(): array
|
||||
{
|
||||
return $this->directories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getFiles(): array
|
||||
{
|
||||
return $this->files;
|
||||
}
|
||||
|
||||
public function addFile(string $file_name): void
|
||||
{
|
||||
$this->files[] = $file_name;
|
||||
$this->files_lowercase[] = strtolower($file_name);
|
||||
}
|
||||
|
||||
public function addDirectory(string $dir_name): void
|
||||
{
|
||||
$this->directories[] = self::slashify($dir_name);
|
||||
}
|
||||
}
|
||||
180
vendor/vimeo/psalm/src/Psalm/Config/IssueHandler.php
vendored
Normal file
180
vendor/vimeo/psalm/src/Psalm/Config/IssueHandler.php
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Config;
|
||||
|
||||
use Psalm\Config;
|
||||
use Psalm\Exception\ConfigException;
|
||||
use SimpleXMLElement;
|
||||
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function dirname;
|
||||
use function in_array;
|
||||
use function scandir;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
|
||||
use const SCANDIR_SORT_NONE;
|
||||
|
||||
/** @internal */
|
||||
final class IssueHandler
|
||||
{
|
||||
private string $error_level = Config::REPORT_ERROR;
|
||||
|
||||
/**
|
||||
* @var array<ErrorLevelFileFilter>
|
||||
*/
|
||||
private array $custom_levels = [];
|
||||
|
||||
public static function loadFromXMLElement(SimpleXMLElement $e, string $base_dir): IssueHandler
|
||||
{
|
||||
$handler = new self();
|
||||
|
||||
if (isset($e['errorLevel'])) {
|
||||
$handler->error_level = (string) $e['errorLevel'];
|
||||
|
||||
if (!in_array($handler->error_level, Config::$ERROR_LEVELS, true)) {
|
||||
throw new ConfigException('Unexpected error level ' . $handler->error_level);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var SimpleXMLElement $error_level */
|
||||
foreach ($e->errorLevel as $error_level) {
|
||||
$handler->custom_levels[] = ErrorLevelFileFilter::loadFromXMLElement($error_level, $base_dir, true);
|
||||
}
|
||||
|
||||
return $handler;
|
||||
}
|
||||
|
||||
public function setCustomLevels(array $customLevels, string $base_dir): void
|
||||
{
|
||||
/** @var array $customLevel */
|
||||
foreach ($customLevels as $customLevel) {
|
||||
$this->custom_levels[] = ErrorLevelFileFilter::loadFromArray($customLevel, $base_dir, true);
|
||||
}
|
||||
}
|
||||
|
||||
public function setErrorLevel(string $error_level): void
|
||||
{
|
||||
if (!in_array($error_level, Config::$ERROR_LEVELS, true)) {
|
||||
throw new ConfigException('Unexpected error level ' . $error_level);
|
||||
}
|
||||
|
||||
$this->error_level = $error_level;
|
||||
}
|
||||
|
||||
public function getReportingLevelForFile(string $file_path): string
|
||||
{
|
||||
foreach ($this->custom_levels as $custom_level) {
|
||||
if ($custom_level->allows($file_path)) {
|
||||
return $custom_level->getErrorLevel();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->error_level;
|
||||
}
|
||||
|
||||
public function getReportingLevelForClass(string $fq_classlike_name): ?string
|
||||
{
|
||||
foreach ($this->custom_levels as $custom_level) {
|
||||
if ($custom_level->allowsClass($fq_classlike_name)) {
|
||||
return $custom_level->getErrorLevel();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getReportingLevelForMethod(string $method_id): ?string
|
||||
{
|
||||
foreach ($this->custom_levels as $custom_level) {
|
||||
if ($custom_level->allowsMethod(strtolower($method_id))) {
|
||||
return $custom_level->getErrorLevel();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getReportingLevelForFunction(string $function_id): ?string
|
||||
{
|
||||
foreach ($this->custom_levels as $custom_level) {
|
||||
if ($custom_level->allowsMethod(strtolower($function_id))) {
|
||||
return $custom_level->getErrorLevel();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getReportingLevelForArgument(string $function_id): ?string
|
||||
{
|
||||
foreach ($this->custom_levels as $custom_level) {
|
||||
if ($custom_level->allowsMethod(strtolower($function_id))) {
|
||||
return $custom_level->getErrorLevel();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getReportingLevelForProperty(string $property_id): ?string
|
||||
{
|
||||
foreach ($this->custom_levels as $custom_level) {
|
||||
if ($custom_level->allowsProperty($property_id)) {
|
||||
return $custom_level->getErrorLevel();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getReportingLevelForClassConstant(string $constant_id): ?string
|
||||
{
|
||||
foreach ($this->custom_levels as $custom_level) {
|
||||
if ($custom_level->allowsClassConstant($constant_id)) {
|
||||
return $custom_level->getErrorLevel();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getReportingLevelForVariable(string $var_name): ?string
|
||||
{
|
||||
foreach ($this->custom_levels as $custom_level) {
|
||||
if ($custom_level->allowsVariable($var_name)) {
|
||||
return $custom_level->getErrorLevel();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function getAllIssueTypes(): array
|
||||
{
|
||||
return array_filter(
|
||||
array_map(
|
||||
static fn(string $file_name): string => substr($file_name, 0, -4),
|
||||
scandir(dirname(__DIR__) . '/Issue', SCANDIR_SORT_NONE),
|
||||
),
|
||||
static fn(string $issue_name): bool => $issue_name !== ''
|
||||
&& $issue_name !== 'MethodIssue'
|
||||
&& $issue_name !== 'PropertyIssue'
|
||||
&& $issue_name !== 'ClassConstantIssue'
|
||||
&& $issue_name !== 'FunctionIssue'
|
||||
&& $issue_name !== 'ArgumentIssue'
|
||||
&& $issue_name !== 'VariableIssue'
|
||||
&& $issue_name !== 'ClassIssue'
|
||||
&& $issue_name !== 'CodeIssue'
|
||||
&& $issue_name !== 'PsalmInternalError'
|
||||
&& $issue_name !== 'ParseError'
|
||||
&& $issue_name !== 'PluginIssue'
|
||||
&& $issue_name !== 'MixedIssue'
|
||||
&& $issue_name !== 'MixedIssueTrait',
|
||||
);
|
||||
}
|
||||
}
|
||||
93
vendor/vimeo/psalm/src/Psalm/Config/ProjectFileFilter.php
vendored
Normal file
93
vendor/vimeo/psalm/src/Psalm/Config/ProjectFileFilter.php
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Config;
|
||||
|
||||
use Psalm\Exception\ConfigException;
|
||||
use SimpleXMLElement;
|
||||
|
||||
use function stripos;
|
||||
use function strpos;
|
||||
|
||||
/** @internal */
|
||||
final class ProjectFileFilter extends FileFilter
|
||||
{
|
||||
private ?ProjectFileFilter $file_filter = null;
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
public static function loadFromXMLElement(
|
||||
SimpleXMLElement $e,
|
||||
string $base_dir,
|
||||
bool $inclusive
|
||||
): ProjectFileFilter {
|
||||
$filter = parent::loadFromXMLElement($e, $base_dir, $inclusive);
|
||||
|
||||
if (isset($e->ignoreFiles)) {
|
||||
if (!$inclusive) {
|
||||
throw new ConfigException('Cannot nest ignoreFiles inside itself');
|
||||
}
|
||||
|
||||
/** @var SimpleXMLElement $e->ignoreFiles */
|
||||
$filter->file_filter = static::loadFromXMLElement($e->ignoreFiles, $base_dir, false);
|
||||
}
|
||||
|
||||
return $filter;
|
||||
}
|
||||
|
||||
public function allows(string $file_name, bool $case_sensitive = false): bool
|
||||
{
|
||||
if ($this->inclusive && $this->file_filter) {
|
||||
if (!$this->file_filter->allows($file_name, $case_sensitive)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return parent::allows($file_name, $case_sensitive);
|
||||
}
|
||||
|
||||
public function forbids(string $file_name, bool $case_sensitive = false): bool
|
||||
{
|
||||
if ($this->inclusive && $this->file_filter) {
|
||||
if (!$this->file_filter->allows($file_name, $case_sensitive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function reportTypeStats(string $file_name, bool $case_sensitive = false): bool
|
||||
{
|
||||
foreach ($this->ignore_type_stats as $exclude_dir => $_) {
|
||||
if ($case_sensitive) {
|
||||
if (strpos($file_name, $exclude_dir) === 0) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (stripos($file_name, $exclude_dir) === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function useStrictTypes(string $file_name, bool $case_sensitive = false): bool
|
||||
{
|
||||
foreach ($this->declare_strict_types as $exclude_dir => $_) {
|
||||
if ($case_sensitive) {
|
||||
if (strpos($file_name, $exclude_dir) === 0) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (stripos($file_name, $exclude_dir) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
8
vendor/vimeo/psalm/src/Psalm/Config/TaintAnalysisFileFilter.php
vendored
Normal file
8
vendor/vimeo/psalm/src/Psalm/Config/TaintAnalysisFileFilter.php
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Config;
|
||||
|
||||
/** @internal */
|
||||
final class TaintAnalysisFileFilter extends FileFilter
|
||||
{
|
||||
}
|
||||
940
vendor/vimeo/psalm/src/Psalm/Context.php
vendored
Normal file
940
vendor/vimeo/psalm/src/Psalm/Context.php
vendored
Normal file
@@ -0,0 +1,940 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Clause;
|
||||
use Psalm\Internal\ReferenceConstraint;
|
||||
use Psalm\Internal\Scope\CaseScope;
|
||||
use Psalm\Internal\Scope\FinallyScope;
|
||||
use Psalm\Internal\Scope\LoopScope;
|
||||
use Psalm\Internal\Type\AssertionReconciler;
|
||||
use Psalm\Storage\FunctionLikeStorage;
|
||||
use Psalm\Type\Atomic\DependentType;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Union;
|
||||
use RuntimeException;
|
||||
|
||||
use function array_keys;
|
||||
use function array_search;
|
||||
use function array_shift;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_int;
|
||||
use function json_encode;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
use function preg_replace;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
|
||||
use const JSON_THROW_ON_ERROR;
|
||||
|
||||
final class Context
|
||||
{
|
||||
/**
|
||||
* @var array<string, Union>
|
||||
*/
|
||||
public $vars_in_scope = [];
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
public $vars_possibly_in_scope = [];
|
||||
|
||||
/**
|
||||
* Keeps track of how many times a var_in_scope has been referenced. May not be set for all $vars_in_scope.
|
||||
*
|
||||
* @var array<string, int<0, max>>
|
||||
*/
|
||||
public $referenced_counts = [];
|
||||
|
||||
/**
|
||||
* Maps references to referenced variables for the current scope.
|
||||
* With `$b = &$a`, this will contain `['$b' => '$a']`.
|
||||
*
|
||||
* All keys and values in this array are guaranteed to be set in $vars_in_scope.
|
||||
*
|
||||
* To check if a variable was passed or returned by reference, or
|
||||
* references an object property or array item, see Union::$by_ref.
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $references_in_scope = [];
|
||||
|
||||
/**
|
||||
* Set of references to variables in another scope. These references will be marked as used if they are assigned to.
|
||||
*
|
||||
* @var array<string, true>
|
||||
*/
|
||||
public $references_to_external_scope = [];
|
||||
|
||||
/**
|
||||
* A set of globals that are referenced somewhere.
|
||||
*
|
||||
* @var array<string, true>
|
||||
*/
|
||||
public $referenced_globals = [];
|
||||
|
||||
/**
|
||||
* A set of references that might still be in scope from a scope likely to cause confusion. This applies
|
||||
* to references set inside a loop or if statement, since it's easy to forget about PHP's weird scope
|
||||
* rules, and assinging to a reference will change the referenced variable rather than shadowing it.
|
||||
*
|
||||
* @var array<string, CodeLocation>
|
||||
*/
|
||||
public $references_possibly_from_confusing_scope = [];
|
||||
|
||||
/**
|
||||
* Whether or not we're inside the conditional of an if/where etc.
|
||||
*
|
||||
* This changes whether or not the context is cloned
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $inside_conditional = false;
|
||||
|
||||
/**
|
||||
* Whether or not we're inside an isset call
|
||||
*
|
||||
* Inside issets Psalm is more lenient about certain things
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $inside_isset = false;
|
||||
|
||||
/**
|
||||
* Whether or not we're inside an unset call, where
|
||||
* we don't care about possibly undefined variables
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $inside_unset = false;
|
||||
|
||||
/**
|
||||
* Whether or not we're inside an class_exists call, where
|
||||
* we don't care about possibly undefined classes
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $inside_class_exists = false;
|
||||
|
||||
/**
|
||||
* Whether or not we're inside a function/method call
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $inside_call = false;
|
||||
|
||||
/**
|
||||
* Whether or not we're inside any other situation that treats a variable as used
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $inside_general_use = false;
|
||||
|
||||
/**
|
||||
* Whether or not we're inside a return expression
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $inside_return = false;
|
||||
|
||||
/**
|
||||
* Whether or not we're inside a throw
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $inside_throw = false;
|
||||
|
||||
/**
|
||||
* Whether or not we're inside an assignment
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $inside_assignment = false;
|
||||
|
||||
/**
|
||||
* Whether or not we're inside a try block.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $inside_try = false;
|
||||
|
||||
/**
|
||||
* @var null|CodeLocation
|
||||
*/
|
||||
public $include_location;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
* The name of the current class. Null if outside a class.
|
||||
*/
|
||||
public $self;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $parent;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $check_classes = true;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $check_variables = true;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $check_methods = true;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $check_consts = true;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $check_functions = true;
|
||||
|
||||
/**
|
||||
* A list of classes checked with class_exists
|
||||
*
|
||||
* @var array<lowercase-string,true>
|
||||
*/
|
||||
public $phantom_classes = [];
|
||||
|
||||
/**
|
||||
* A list of files checked with file_exists
|
||||
*
|
||||
* @var array<string,bool>
|
||||
*/
|
||||
public $phantom_files = [];
|
||||
|
||||
/**
|
||||
* A list of clauses in Conjunctive Normal Form
|
||||
*
|
||||
* @var list<Clause>
|
||||
*/
|
||||
public $clauses = [];
|
||||
|
||||
/**
|
||||
* A list of hashed clauses that have already been factored in
|
||||
*
|
||||
* @var list<string|int>
|
||||
*/
|
||||
public $reconciled_expression_clauses = [];
|
||||
|
||||
/**
|
||||
* Whether or not to do a deep analysis and collect mutations to this context
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $collect_mutations = false;
|
||||
|
||||
/**
|
||||
* Whether or not to do a deep analysis and collect initializations from private or final methods
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $collect_initializations = false;
|
||||
|
||||
/**
|
||||
* Whether or not to do a deep analysis and collect initializations from public non-final methods
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $collect_nonprivate_initializations = false;
|
||||
|
||||
/**
|
||||
* Stored to prevent re-analysing methods when checking for initialised properties
|
||||
*
|
||||
* @var array<string, bool>|null
|
||||
*/
|
||||
public $initialized_methods;
|
||||
|
||||
/**
|
||||
* @var array<string, Union>
|
||||
*/
|
||||
public $constants = [];
|
||||
|
||||
/**
|
||||
* Whether or not to track exceptions
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $collect_exceptions = false;
|
||||
|
||||
/**
|
||||
* A list of variables that have been referenced in conditionals
|
||||
*
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
public $cond_referenced_var_ids = [];
|
||||
|
||||
/**
|
||||
* A list of variables that have been passed by reference (where we know their type)
|
||||
*
|
||||
* @var array<string, ReferenceConstraint>
|
||||
*/
|
||||
public $byref_constraints = [];
|
||||
|
||||
/**
|
||||
* A list of vars that have been assigned to
|
||||
*
|
||||
* @var array<string, int>
|
||||
*/
|
||||
public $assigned_var_ids = [];
|
||||
|
||||
/**
|
||||
* A list of vars that have been may have been assigned to
|
||||
*
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
public $possibly_assigned_var_ids = [];
|
||||
|
||||
/**
|
||||
* A list of classes or interfaces that may have been thrown
|
||||
*
|
||||
* @var array<string, array<array-key, CodeLocation>>
|
||||
*/
|
||||
public $possibly_thrown_exceptions = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $is_global = false;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
public $protected_var_ids = [];
|
||||
|
||||
/**
|
||||
* If we've branched from the main scope, a byte offset for where that branch happened
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
public $branch_point;
|
||||
|
||||
/**
|
||||
* What does break mean in this context?
|
||||
*
|
||||
* 'loop' means we're breaking out of a loop,
|
||||
* 'switch' means we're breaking out of a switch
|
||||
*
|
||||
* @var list<'loop'|'switch'>
|
||||
*/
|
||||
public $break_types = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $inside_loop = false;
|
||||
|
||||
/**
|
||||
* @var LoopScope|null
|
||||
*/
|
||||
public $loop_scope;
|
||||
|
||||
/**
|
||||
* @var CaseScope|null
|
||||
*/
|
||||
public $case_scope;
|
||||
|
||||
/**
|
||||
* @var FinallyScope|null
|
||||
*/
|
||||
public $finally_scope;
|
||||
|
||||
/**
|
||||
* @var Context|null
|
||||
*/
|
||||
public $if_body_context;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $strict_types = false;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
public $calling_function_id;
|
||||
|
||||
/**
|
||||
* @var lowercase-string|null
|
||||
*/
|
||||
public $calling_method_id;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $inside_negation = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $ignore_variable_property = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $ignore_variable_method = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $pure = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
* Set by @psalm-immutable
|
||||
*/
|
||||
public $mutation_free = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
* Set by @psalm-external-mutation-free
|
||||
*/
|
||||
public $external_mutation_free = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $error_suppressing = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
public $has_returned = false;
|
||||
|
||||
/**
|
||||
* @var array<string, true>
|
||||
*/
|
||||
public $parent_remove_vars = [];
|
||||
|
||||
/** @internal */
|
||||
public function __construct(?string $self = null)
|
||||
{
|
||||
$this->self = $self;
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
$this->case_scope = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the parent context, looking at the changes within a block and then applying those changes, where
|
||||
* necessary, to the parent context
|
||||
*
|
||||
* @param bool $has_leaving_statements whether or not the parent scope is abandoned between
|
||||
* $start_context and $end_context
|
||||
* @param array<string, bool> $updated_vars
|
||||
*/
|
||||
public function update(
|
||||
Context $start_context,
|
||||
Context $end_context,
|
||||
bool $has_leaving_statements,
|
||||
array $vars_to_update,
|
||||
array &$updated_vars
|
||||
): void {
|
||||
foreach ($start_context->vars_in_scope as $var_id => $old_type) {
|
||||
// this is only true if there was some sort of type negation
|
||||
if (in_array($var_id, $vars_to_update, true)) {
|
||||
// if we're leaving, we're effectively deleting the possibility of the if types
|
||||
$new_type = !$has_leaving_statements && $end_context->hasVariable($var_id)
|
||||
? $end_context->vars_in_scope[$var_id]
|
||||
: null;
|
||||
|
||||
$existing_type = $this->vars_in_scope[$var_id] ?? null;
|
||||
|
||||
if (!$existing_type) {
|
||||
if ($new_type) {
|
||||
$this->vars_in_scope[$var_id] = $new_type;
|
||||
$updated_vars[$var_id] = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// if the type changed within the block of statements, process the replacement
|
||||
// also never allow ourselves to remove all types from a union
|
||||
if ((!$new_type || !$old_type->equals($new_type))
|
||||
&& ($new_type || count($existing_type->getAtomicTypes()) > 1)
|
||||
) {
|
||||
$existing_type = $existing_type
|
||||
->getBuilder()
|
||||
->substitute($old_type, $new_type);
|
||||
|
||||
if ($new_type && $new_type->from_docblock) {
|
||||
$existing_type = $existing_type->setFromDocblock();
|
||||
}
|
||||
$existing_type = $existing_type->freeze();
|
||||
|
||||
$updated_vars[$var_id] = true;
|
||||
}
|
||||
|
||||
$this->vars_in_scope[$var_id] = $existing_type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the list of possible references from a confusing scope,
|
||||
* such as a reference created in an if that might later be reused.
|
||||
*/
|
||||
public function updateReferencesPossiblyFromConfusingScope(
|
||||
Context $confusing_scope_context,
|
||||
StatementsAnalyzer $statements_analyzer
|
||||
): void {
|
||||
$references = $confusing_scope_context->references_in_scope
|
||||
+ $confusing_scope_context->references_to_external_scope;
|
||||
foreach ($references as $reference_id => $_) {
|
||||
if (!isset($this->references_in_scope[$reference_id])
|
||||
&& !isset($this->references_to_external_scope[$reference_id])
|
||||
&& $reference_location = $statements_analyzer->getFirstAppearance($reference_id)
|
||||
) {
|
||||
$this->references_possibly_from_confusing_scope[$reference_id] = $reference_location;
|
||||
}
|
||||
}
|
||||
$this->references_possibly_from_confusing_scope +=
|
||||
$confusing_scope_context->references_possibly_from_confusing_scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, Union> $new_vars_in_scope
|
||||
* @return array<string, Union>
|
||||
*/
|
||||
public function getRedefinedVars(array $new_vars_in_scope, bool $include_new_vars = false): array
|
||||
{
|
||||
$redefined_vars = [];
|
||||
|
||||
foreach ($this->vars_in_scope as $var_id => $this_type) {
|
||||
if (!isset($new_vars_in_scope[$var_id])) {
|
||||
if ($include_new_vars) {
|
||||
$redefined_vars[$var_id] = $this_type;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$new_type = $new_vars_in_scope[$var_id];
|
||||
|
||||
if (!$this_type->equals(
|
||||
$new_type,
|
||||
true,
|
||||
!($this_type->propagate_parent_nodes || $new_type->propagate_parent_nodes),
|
||||
)
|
||||
) {
|
||||
$redefined_vars[$var_id] = $this_type;
|
||||
}
|
||||
}
|
||||
|
||||
return $redefined_vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function getNewOrUpdatedVarIds(Context $original_context, Context $new_context): array
|
||||
{
|
||||
$redefined_var_ids = [];
|
||||
|
||||
foreach ($new_context->vars_in_scope as $var_id => $context_type) {
|
||||
if (!isset($original_context->vars_in_scope[$var_id])
|
||||
|| ($original_context->assigned_var_ids[$var_id] ?? 0)
|
||||
!== ($new_context->assigned_var_ids[$var_id] ?? 0)
|
||||
|| !$original_context->vars_in_scope[$var_id]->equals($context_type)
|
||||
) {
|
||||
$redefined_var_ids[] = $var_id;
|
||||
}
|
||||
}
|
||||
|
||||
return $redefined_var_ids;
|
||||
}
|
||||
|
||||
public function remove(string $remove_var_id, bool $removeDescendents = true): void
|
||||
{
|
||||
if (isset($this->vars_in_scope[$remove_var_id])) {
|
||||
$existing_type = $this->vars_in_scope[$remove_var_id];
|
||||
unset($this->vars_in_scope[$remove_var_id]);
|
||||
|
||||
if ($removeDescendents) {
|
||||
$this->removeDescendents($remove_var_id, $existing_type);
|
||||
}
|
||||
}
|
||||
$this->removePossibleReference($remove_var_id);
|
||||
unset($this->vars_possibly_in_scope[$remove_var_id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a variable from the context which might be a reference to another variable, or
|
||||
* referenced by another variable. Leaves the variable as possibly-in-scope, unlike remove().
|
||||
*/
|
||||
public function removePossibleReference(string $remove_var_id): void
|
||||
{
|
||||
if (isset($this->referenced_counts[$remove_var_id]) && $this->referenced_counts[$remove_var_id] > 0) {
|
||||
// If a referenced variable goes out of scope, we need to update the references.
|
||||
// All of the references to this variable are still references to the same value,
|
||||
// so we pick the first one and make the rest of the references point to it.
|
||||
$references = [];
|
||||
foreach ($this->references_in_scope as $reference => $referenced) {
|
||||
if ($referenced === $remove_var_id) {
|
||||
$references[] = $reference;
|
||||
unset($this->references_in_scope[$reference]);
|
||||
}
|
||||
}
|
||||
assert(!empty($references));
|
||||
$first_reference = array_shift($references);
|
||||
if (!empty($references)) {
|
||||
$this->referenced_counts[$first_reference] = count($references);
|
||||
foreach ($references as $reference) {
|
||||
$this->references_in_scope[$reference] = $first_reference;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($this->references_in_scope[$remove_var_id])) {
|
||||
$this->decrementReferenceCount($remove_var_id);
|
||||
}
|
||||
unset(
|
||||
$this->vars_in_scope[$remove_var_id],
|
||||
$this->cond_referenced_var_ids[$remove_var_id],
|
||||
$this->referenced_counts[$remove_var_id],
|
||||
$this->references_in_scope[$remove_var_id],
|
||||
$this->references_to_external_scope[$remove_var_id],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement the reference count of the variable that $ref_id is referring to. This needs to
|
||||
* be done before $ref_id is changed to no longer reference its currently referenced variable,
|
||||
* for example by unsetting, reassigning to another reference, or being shadowed by a global.
|
||||
*/
|
||||
public function decrementReferenceCount(string $ref_id): void
|
||||
{
|
||||
if (!isset($this->referenced_counts[$this->references_in_scope[$ref_id]])) {
|
||||
throw new InvalidArgumentException("$ref_id is not a reference");
|
||||
}
|
||||
$reference_count = $this->referenced_counts[$this->references_in_scope[$ref_id]];
|
||||
if ($reference_count < 1) {
|
||||
throw new RuntimeException("Incorrect referenced count found");
|
||||
}
|
||||
--$reference_count;
|
||||
$this->referenced_counts[$this->references_in_scope[$ref_id]] = $reference_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Clause[] $clauses
|
||||
* @param array<string, bool> $changed_var_ids
|
||||
* @return array{list<Clause>, list<Clause>}
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function removeReconciledClauses(array $clauses, array $changed_var_ids): array
|
||||
{
|
||||
$included_clauses = [];
|
||||
$rejected_clauses = [];
|
||||
|
||||
foreach ($clauses as $c) {
|
||||
if ($c->wedge) {
|
||||
$included_clauses[] = $c;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($c->possibilities as $key => $_) {
|
||||
if (isset($changed_var_ids[$key])) {
|
||||
$rejected_clauses[] = $c;
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$included_clauses[] = $c;
|
||||
}
|
||||
|
||||
return [$included_clauses, $rejected_clauses];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Clause[] $clauses
|
||||
* @return list<Clause>
|
||||
*/
|
||||
public static function filterClauses(
|
||||
string $remove_var_id,
|
||||
array $clauses,
|
||||
?Union $new_type = null,
|
||||
?StatementsAnalyzer $statements_analyzer = null
|
||||
): array {
|
||||
$new_type_string = $new_type ? $new_type->getId() : '';
|
||||
$clauses_to_keep = [];
|
||||
|
||||
foreach ($clauses as $clause) {
|
||||
$clause = $clause->calculateNegation();
|
||||
|
||||
$quoted_remove_var_id = preg_quote($remove_var_id, '/');
|
||||
|
||||
foreach ($clause->possibilities as $var_id => $_) {
|
||||
if (preg_match('/' . $quoted_remove_var_id . '[\]\[\-]/', $var_id)) {
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($clause->possibilities[$remove_var_id])
|
||||
|| (count($clause->possibilities[$remove_var_id]) === 1
|
||||
&& array_keys($clause->possibilities[$remove_var_id])[0] === $new_type_string)
|
||||
) {
|
||||
$clauses_to_keep[] = $clause;
|
||||
} elseif ($statements_analyzer &&
|
||||
$new_type &&
|
||||
!$new_type->hasMixed()
|
||||
) {
|
||||
$type_changed = false;
|
||||
|
||||
// if the clause contains any possibilities that would be altered
|
||||
// by the new type
|
||||
foreach ($clause->possibilities[$remove_var_id] as $assertion) {
|
||||
// if we're negating a type, we generally don't need the clause anymore
|
||||
if ($assertion->isNegation()) {
|
||||
$type_changed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$result_type = AssertionReconciler::reconcile(
|
||||
$assertion,
|
||||
$new_type,
|
||||
null,
|
||||
$statements_analyzer,
|
||||
false,
|
||||
[],
|
||||
null,
|
||||
[],
|
||||
$failed_reconciliation,
|
||||
);
|
||||
|
||||
if ($result_type->getId() !== $new_type_string) {
|
||||
$type_changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$type_changed) {
|
||||
$clauses_to_keep[] = $clause;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $clauses_to_keep;
|
||||
}
|
||||
|
||||
public function removeVarFromConflictingClauses(
|
||||
string $remove_var_id,
|
||||
?Union $new_type = null,
|
||||
?StatementsAnalyzer $statements_analyzer = null
|
||||
): void {
|
||||
$this->clauses = self::filterClauses($remove_var_id, $this->clauses, $new_type, $statements_analyzer);
|
||||
$this->parent_remove_vars[$remove_var_id] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used after assignments to variables to remove any existing
|
||||
* items in $vars_in_scope that are now made redundant by an update to some data
|
||||
*/
|
||||
public function removeDescendents(
|
||||
string $remove_var_id,
|
||||
Union $existing_type,
|
||||
?Union $new_type = null,
|
||||
?StatementsAnalyzer $statements_analyzer = null
|
||||
): void {
|
||||
$this->removeVarFromConflictingClauses(
|
||||
$remove_var_id,
|
||||
$existing_type->hasMixed()
|
||||
|| ($new_type && $existing_type->from_docblock !== $new_type->from_docblock)
|
||||
? null
|
||||
: $new_type,
|
||||
$statements_analyzer,
|
||||
);
|
||||
|
||||
foreach ($this->vars_in_scope as $var_id => &$type) {
|
||||
if (preg_match('/' . preg_quote($remove_var_id, '/') . '[\]\[\-]/', $var_id)) {
|
||||
$this->remove($var_id, false);
|
||||
}
|
||||
|
||||
$builder = null;
|
||||
foreach ($type->getAtomicTypes() as $atomic_type) {
|
||||
if ($atomic_type instanceof DependentType
|
||||
&& $atomic_type->getVarId() === $remove_var_id
|
||||
) {
|
||||
$builder ??= $type->getBuilder();
|
||||
$builder->addType($atomic_type->getReplacement());
|
||||
}
|
||||
}
|
||||
if ($builder) {
|
||||
$type = $builder->freeze();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function removeMutableObjectVars(bool $methods_only = false): void
|
||||
{
|
||||
$vars_to_remove = [];
|
||||
|
||||
foreach ($this->vars_in_scope as $var_id => $type) {
|
||||
if ($type->has_mutations
|
||||
&& (strpos($var_id, '->') !== false || strpos($var_id, '::') !== false)
|
||||
&& (!$methods_only || strpos($var_id, '()'))
|
||||
) {
|
||||
$vars_to_remove[] = $var_id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$vars_to_remove) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($vars_to_remove as $var_id) {
|
||||
$this->remove($var_id, false);
|
||||
}
|
||||
|
||||
$clauses_to_keep = [];
|
||||
|
||||
foreach ($this->clauses as $clause) {
|
||||
$abandon_clause = false;
|
||||
|
||||
foreach (array_keys($clause->possibilities) as $key) {
|
||||
if ((strpos($key, '->') !== false || strpos($key, '::') !== false)
|
||||
&& (!$methods_only || strpos($key, '()'))
|
||||
) {
|
||||
$abandon_clause = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$abandon_clause) {
|
||||
$clauses_to_keep[] = $clause;
|
||||
}
|
||||
}
|
||||
|
||||
$this->clauses = $clauses_to_keep;
|
||||
}
|
||||
|
||||
public function updateChecks(Context $op_context): void
|
||||
{
|
||||
$this->check_classes = $this->check_classes && $op_context->check_classes;
|
||||
$this->check_variables = $this->check_variables && $op_context->check_variables;
|
||||
$this->check_methods = $this->check_methods && $op_context->check_methods;
|
||||
$this->check_functions = $this->check_functions && $op_context->check_functions;
|
||||
$this->check_consts = $this->check_consts && $op_context->check_consts;
|
||||
}
|
||||
|
||||
public function isPhantomClass(string $class_name): bool
|
||||
{
|
||||
return isset($this->phantom_classes[strtolower($class_name)]);
|
||||
}
|
||||
|
||||
public function hasVariable(string $var_name): bool
|
||||
{
|
||||
if (!$var_name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stripped_var = preg_replace('/(->|\[).*$/', '', $var_name, 1);
|
||||
|
||||
if ($stripped_var !== '$this' || $var_name !== $stripped_var) {
|
||||
$this->cond_referenced_var_ids[$var_name] = true;
|
||||
}
|
||||
|
||||
return isset($this->vars_in_scope[$var_name]);
|
||||
}
|
||||
|
||||
public function getScopeSummary(): string
|
||||
{
|
||||
$summary = [];
|
||||
foreach ($this->vars_possibly_in_scope as $k => $_) {
|
||||
$summary[$k] = true;
|
||||
}
|
||||
foreach ($this->vars_in_scope as $k => $v) {
|
||||
$summary[$k] = $v->getId();
|
||||
}
|
||||
|
||||
return json_encode($summary, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
public function defineGlobals(): void
|
||||
{
|
||||
$globals = [
|
||||
'$argv' => new Union([
|
||||
new TArray([Type::getInt(), Type::getString()]),
|
||||
]),
|
||||
'$argc' => Type::getInt(),
|
||||
];
|
||||
|
||||
$config = Config::getInstance();
|
||||
|
||||
foreach ($config->globals as $global_id => $type_string) {
|
||||
$globals[$global_id] = Type::parseString($type_string);
|
||||
}
|
||||
|
||||
foreach ($globals as $global_id => $type) {
|
||||
$this->vars_in_scope[$global_id] = $type;
|
||||
$this->vars_possibly_in_scope[$global_id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function mergeExceptions(Context $other_context): void
|
||||
{
|
||||
foreach ($other_context->possibly_thrown_exceptions as $possibly_thrown_exception => $codelocations) {
|
||||
foreach ($codelocations as $hash => $codelocation) {
|
||||
$this->possibly_thrown_exceptions[$possibly_thrown_exception][$hash] = $codelocation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function isSuppressingExceptions(StatementsAnalyzer $statements_analyzer): bool
|
||||
{
|
||||
if (!$this->collect_exceptions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$issue_type = $this->is_global ? 'UncaughtThrowInGlobalScope' : 'MissingThrowsDocblock';
|
||||
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
|
||||
$suppressed_issue_position = array_search($issue_type, $suppressed_issues, true);
|
||||
if ($suppressed_issue_position !== false) {
|
||||
if (is_int($suppressed_issue_position)) {
|
||||
$file = $statements_analyzer->getFileAnalyzer()->getFilePath();
|
||||
IssueBuffer::addUsedSuppressions([
|
||||
$file => [$suppressed_issue_position => true],
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function mergeFunctionExceptions(
|
||||
FunctionLikeStorage $function_storage,
|
||||
CodeLocation $codelocation
|
||||
): void {
|
||||
$hash = $codelocation->getHash();
|
||||
foreach ($function_storage->throws as $possibly_thrown_exception => $_) {
|
||||
$this->possibly_thrown_exceptions[$possibly_thrown_exception][$hash] = $codelocation;
|
||||
}
|
||||
}
|
||||
|
||||
public function insideUse(): bool
|
||||
{
|
||||
return $this->inside_assignment
|
||||
|| $this->inside_return
|
||||
|| $this->inside_call
|
||||
|| $this->inside_general_use
|
||||
|| $this->inside_conditional
|
||||
|| $this->inside_throw
|
||||
|| $this->inside_isset;
|
||||
}
|
||||
}
|
||||
102
vendor/vimeo/psalm/src/Psalm/DocComment.php
vendored
Normal file
102
vendor/vimeo/psalm/src/Psalm/DocComment.php
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm;
|
||||
|
||||
use PhpParser\Comment\Doc;
|
||||
use Psalm\Exception\DocblockParseException;
|
||||
use Psalm\Internal\Scanner\DocblockParser;
|
||||
use Psalm\Internal\Scanner\ParsedDocblock;
|
||||
|
||||
use function explode;
|
||||
use function in_array;
|
||||
use function preg_match;
|
||||
use function strlen;
|
||||
use function strpos;
|
||||
use function strspn;
|
||||
use function substr;
|
||||
use function trim;
|
||||
|
||||
final class DocComment
|
||||
{
|
||||
public const PSALM_ANNOTATIONS = [
|
||||
'return', 'param', 'template', 'var', 'type',
|
||||
'template-covariant', 'property', 'property-read', 'property-write', 'method',
|
||||
'assert', 'assert-if-true', 'assert-if-false', 'suppress',
|
||||
'ignore-nullable-return', 'override-property-visibility',
|
||||
'override-method-visibility', 'seal-properties', 'seal-methods',
|
||||
'ignore-falsable-return', 'variadic', 'pure',
|
||||
'ignore-variable-method', 'ignore-variable-property', 'internal',
|
||||
'taint-sink', 'taint-source', 'assert-untainted', 'scope-this',
|
||||
'mutation-free', 'external-mutation-free', 'immutable', 'readonly',
|
||||
'allow-private-mutation', 'readonly-allow-private-mutation',
|
||||
'yield', 'trace', 'import-type', 'flow', 'taint-specialize', 'taint-escape',
|
||||
'taint-unescape', 'self-out', 'consistent-constructor', 'stub-override',
|
||||
'require-extends', 'require-implements', 'param-out', 'ignore-var',
|
||||
'consistent-templates', 'if-this-is', 'this-out', 'check-type', 'check-type-exact',
|
||||
'api',
|
||||
];
|
||||
|
||||
/**
|
||||
* Parse a docblock comment into its parts.
|
||||
*/
|
||||
public static function parsePreservingLength(Doc $docblock): ParsedDocblock
|
||||
{
|
||||
$parsed_docblock = DocblockParser::parse(
|
||||
$docblock->getText(),
|
||||
$docblock->getStartFilePos(),
|
||||
);
|
||||
|
||||
foreach ($parsed_docblock->tags as $special_key => $_) {
|
||||
if (strpos($special_key, 'psalm-') === 0) {
|
||||
$special_key = substr($special_key, 6);
|
||||
|
||||
if (!in_array(
|
||||
$special_key,
|
||||
self::PSALM_ANNOTATIONS,
|
||||
true,
|
||||
)) {
|
||||
throw new DocblockParseException('Unrecognised annotation @psalm-' . $special_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $parsed_docblock;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
* @return array<int,string>
|
||||
*/
|
||||
public static function parseSuppressList(string $suppress_entry): array
|
||||
{
|
||||
preg_match(
|
||||
'/
|
||||
(?(DEFINE)
|
||||
# either a single issue or comma separated list of issues
|
||||
(?<issue_list> (?&issue) \s* , \s* (?&issue_list) | (?&issue) )
|
||||
|
||||
# definition of a single issue
|
||||
(?<issue> [A-Za-z0-9_-]+ )
|
||||
)
|
||||
^ (?P<issues> (?&issue_list) ) (?P<description> .* ) $
|
||||
/xm',
|
||||
$suppress_entry,
|
||||
$matches,
|
||||
);
|
||||
|
||||
if (!isset($matches['issues'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$issue_offset = 0;
|
||||
$ret = [];
|
||||
|
||||
foreach (explode(',', $matches['issues']) as $suppressed_issue) {
|
||||
$issue_offset += strspn($suppressed_issue, "\t\n\f\r ");
|
||||
$ret[$issue_offset] = trim($suppressed_issue);
|
||||
$issue_offset += strlen($suppressed_issue) + 1;
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
}
|
||||
307
vendor/vimeo/psalm/src/Psalm/ErrorBaseline.php
vendored
Normal file
307
vendor/vimeo/psalm/src/Psalm/ErrorBaseline.php
vendored
Normal file
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm;
|
||||
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use Psalm\Exception\ConfigException;
|
||||
use Psalm\Internal\Analyzer\IssueData;
|
||||
use Psalm\Internal\Provider\FileProvider;
|
||||
use RuntimeException;
|
||||
|
||||
use function array_filter;
|
||||
use function array_intersect;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_reduce;
|
||||
use function array_values;
|
||||
use function get_loaded_extensions;
|
||||
use function htmlspecialchars;
|
||||
use function implode;
|
||||
use function ksort;
|
||||
use function min;
|
||||
use function phpversion;
|
||||
use function preg_replace_callback;
|
||||
use function sort;
|
||||
use function sprintf;
|
||||
use function str_replace;
|
||||
use function trim;
|
||||
use function usort;
|
||||
|
||||
use const LIBXML_NOBLANKS;
|
||||
use const PHP_VERSION;
|
||||
|
||||
final class ErrorBaseline
|
||||
{
|
||||
/**
|
||||
* @param array<string,array<string,array{o:int, s:array<int, string>}>> $existingIssues
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function countTotalIssues(array $existingIssues): int
|
||||
{
|
||||
$totalIssues = 0;
|
||||
|
||||
foreach ($existingIssues as $existingIssue) {
|
||||
$totalIssues += array_reduce(
|
||||
$existingIssue,
|
||||
/**
|
||||
* @param array{o:int, s:array<int, string>} $existingIssue
|
||||
*/
|
||||
static fn(int $carry, array $existingIssue): int => $carry + $existingIssue['o'],
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
return $totalIssues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, list<IssueData>> $issues
|
||||
*/
|
||||
public static function create(
|
||||
FileProvider $fileProvider,
|
||||
string $baselineFile,
|
||||
array $issues,
|
||||
bool $include_php_versions
|
||||
): void {
|
||||
$groupedIssues = self::countIssueTypesByFile($issues);
|
||||
|
||||
self::writeToFile($fileProvider, $baselineFile, $groupedIssues, $include_php_versions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array<string,array{o:int, s: list<string>}>>
|
||||
* @throws ConfigException
|
||||
*/
|
||||
public static function read(FileProvider $fileProvider, string $baselineFile): array
|
||||
{
|
||||
if (!$fileProvider->fileExists($baselineFile)) {
|
||||
throw new ConfigException("{$baselineFile} does not exist or is not readable");
|
||||
}
|
||||
|
||||
$xmlSource = $fileProvider->getContents($baselineFile);
|
||||
|
||||
if ($xmlSource === '') {
|
||||
throw new ConfigException('Baseline file is empty');
|
||||
}
|
||||
|
||||
$baselineDoc = new DOMDocument();
|
||||
$baselineDoc->loadXML($xmlSource, LIBXML_NOBLANKS);
|
||||
|
||||
$filesElement = $baselineDoc->getElementsByTagName('files');
|
||||
|
||||
if ($filesElement->length === 0) {
|
||||
throw new ConfigException('Baseline file does not contain <files>');
|
||||
}
|
||||
|
||||
$files = [];
|
||||
|
||||
/** @var DOMElement $filesElement */
|
||||
$filesElement = $filesElement[0];
|
||||
|
||||
foreach ($filesElement->getElementsByTagName('file') as $file) {
|
||||
$fileName = $file->getAttribute('src');
|
||||
|
||||
$fileName = str_replace('\\', '/', $fileName);
|
||||
|
||||
$files[$fileName] = [];
|
||||
|
||||
foreach ($file->childNodes as $issue) {
|
||||
if (!$issue instanceof DOMElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$issueType = $issue->tagName;
|
||||
|
||||
$files[$fileName][$issueType] = ['o' => 0, 's' => []];
|
||||
$codeSamples = $issue->getElementsByTagName('code');
|
||||
|
||||
foreach ($codeSamples as $codeSample) {
|
||||
$files[$fileName][$issueType]['o'] += 1;
|
||||
$files[$fileName][$issueType]['s'][] = trim($codeSample->textContent);
|
||||
}
|
||||
|
||||
// TODO: Remove in Psalm 6
|
||||
$occurrencesAttr = $issue->getAttribute('occurrences');
|
||||
if ($occurrencesAttr !== '') {
|
||||
$files[$fileName][$issueType]['o'] = (int) $occurrencesAttr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, list<IssueData>> $issues
|
||||
* @return array<string, array<string, array{o: int, s: list<string>}>>
|
||||
* @throws ConfigException
|
||||
*/
|
||||
public static function update(
|
||||
FileProvider $fileProvider,
|
||||
string $baselineFile,
|
||||
array $issues,
|
||||
bool $include_php_versions
|
||||
): array {
|
||||
$existingIssues = self::read($fileProvider, $baselineFile);
|
||||
$newIssues = self::countIssueTypesByFile($issues);
|
||||
|
||||
foreach ($existingIssues as $file => &$existingIssuesCount) {
|
||||
if (!isset($newIssues[$file])) {
|
||||
unset($existingIssues[$file]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($existingIssuesCount as $issueType => $existingIssueType) {
|
||||
if (!isset($newIssues[$file][$issueType])) {
|
||||
unset($existingIssuesCount[$issueType]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$existingIssuesCount[$issueType]['o'] = min(
|
||||
$existingIssueType['o'],
|
||||
$newIssues[$file][$issueType]['o'],
|
||||
);
|
||||
$existingIssuesCount[$issueType]['s'] = array_intersect(
|
||||
$existingIssueType['s'],
|
||||
$newIssues[$file][$issueType]['s'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$groupedIssues = array_filter($existingIssues);
|
||||
|
||||
self::writeToFile($fileProvider, $baselineFile, $groupedIssues, $include_php_versions);
|
||||
|
||||
return $groupedIssues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, list<IssueData>> $issues
|
||||
* @return array<string,array<string,array{o:int, s:array<int, string>}>>
|
||||
*/
|
||||
private static function countIssueTypesByFile(array $issues): array
|
||||
{
|
||||
if ($issues === []) {
|
||||
return [];
|
||||
}
|
||||
$groupedIssues = array_reduce(
|
||||
array_merge(...array_values($issues)),
|
||||
/**
|
||||
* @param array<string,array<string,array{o:int, s:array<int, string>}>> $carry
|
||||
* @return array<string,array<string,array{o:int, s:array<int, string>}>>
|
||||
*/
|
||||
static function (array $carry, IssueData $issue): array {
|
||||
if ($issue->severity !== Config::REPORT_ERROR) {
|
||||
return $carry;
|
||||
}
|
||||
|
||||
$fileName = $issue->file_name;
|
||||
$fileName = str_replace('\\', '/', $fileName);
|
||||
$issueType = $issue->type;
|
||||
|
||||
if (!isset($carry[$fileName])) {
|
||||
$carry[$fileName] = [];
|
||||
}
|
||||
|
||||
if (!isset($carry[$fileName][$issueType])) {
|
||||
$carry[$fileName][$issueType] = ['o' => 0, 's' => []];
|
||||
}
|
||||
|
||||
++$carry[$fileName][$issueType]['o'];
|
||||
$carry[$fileName][$issueType]['s'][] = $issue->selected_text;
|
||||
|
||||
return $carry;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Sort files first
|
||||
ksort($groupedIssues);
|
||||
|
||||
foreach ($groupedIssues as &$issues) {
|
||||
ksort($issues);
|
||||
}
|
||||
unset($issues);
|
||||
|
||||
return $groupedIssues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,array<string,array{o:int, s:array<int, string>}>> $groupedIssues
|
||||
*/
|
||||
private static function writeToFile(
|
||||
FileProvider $fileProvider,
|
||||
string $baselineFile,
|
||||
array $groupedIssues,
|
||||
bool $include_php_versions
|
||||
): void {
|
||||
$baselineDoc = new DOMDocument('1.0', 'UTF-8');
|
||||
$filesNode = $baselineDoc->createElement('files');
|
||||
$filesNode->setAttribute('psalm-version', PSALM_VERSION);
|
||||
|
||||
if ($include_php_versions) {
|
||||
$extensions = [...get_loaded_extensions(), ...get_loaded_extensions(true)];
|
||||
|
||||
usort($extensions, 'strnatcasecmp');
|
||||
|
||||
$filesNode->setAttribute('php-version', implode(';' . "\n\t", [...[
|
||||
('php:' . PHP_VERSION),
|
||||
], ...array_map(
|
||||
static fn(string $extension): string => $extension . ':' . phpversion($extension),
|
||||
$extensions,
|
||||
)]));
|
||||
}
|
||||
|
||||
foreach ($groupedIssues as $file => $issueTypes) {
|
||||
$fileNode = $baselineDoc->createElement('file');
|
||||
|
||||
$fileNode->setAttribute('src', $file);
|
||||
|
||||
foreach ($issueTypes as $issueType => $existingIssueType) {
|
||||
$issueNode = $baselineDoc->createElement($issueType);
|
||||
|
||||
sort($existingIssueType['s']);
|
||||
|
||||
foreach ($existingIssueType['s'] as $selection) {
|
||||
$codeNode = $baselineDoc->createElement('code');
|
||||
$textContent = trim($selection);
|
||||
if ($textContent !== htmlspecialchars($textContent)) {
|
||||
$codeNode->appendChild($baselineDoc->createCDATASection($textContent));
|
||||
} else {
|
||||
$codeNode->textContent = trim($textContent);
|
||||
}
|
||||
$issueNode->appendChild($codeNode);
|
||||
}
|
||||
$fileNode->appendChild($issueNode);
|
||||
}
|
||||
|
||||
$filesNode->appendChild($fileNode);
|
||||
}
|
||||
|
||||
$baselineDoc->appendChild($filesNode);
|
||||
$baselineDoc->formatOutput = true;
|
||||
|
||||
$xml = preg_replace_callback(
|
||||
'/<files (psalm-version="[^"]+") php-version="(.+)"(\/?>)\n/',
|
||||
/**
|
||||
* @param string[] $matches
|
||||
*/
|
||||
static fn(array $matches): string => sprintf(
|
||||
"<files\n %s\n php-version=\"\n %s\n \"\n%s\n",
|
||||
$matches[1],
|
||||
str_replace(' 	', "\n ", $matches[2]),
|
||||
$matches[3],
|
||||
),
|
||||
$baselineDoc->saveXML(),
|
||||
);
|
||||
|
||||
if ($xml === null) {
|
||||
throw new RuntimeException('Failed to reformat opening attributes!');
|
||||
}
|
||||
|
||||
$fileProvider->setContents($baselineFile, $xml);
|
||||
}
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/CircularReferenceException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/CircularReferenceException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class CircularReferenceException extends Exception
|
||||
{
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/CodeException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/CodeException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class CodeException extends Exception
|
||||
{
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/ComplicatedExpressionException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/ComplicatedExpressionException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class ComplicatedExpressionException extends Exception
|
||||
{
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/ConfigCreationException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/ConfigCreationException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class ConfigCreationException extends Exception
|
||||
{
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/ConfigException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/ConfigException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
class ConfigException extends Exception
|
||||
{
|
||||
}
|
||||
7
vendor/vimeo/psalm/src/Psalm/Exception/ConfigNotFoundException.php
vendored
Normal file
7
vendor/vimeo/psalm/src/Psalm/Exception/ConfigNotFoundException.php
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
final class ConfigNotFoundException extends ConfigException
|
||||
{
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/DocblockParseException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/DocblockParseException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
class DocblockParseException extends Exception
|
||||
{
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/FileIncludeException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/FileIncludeException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class FileIncludeException extends Exception
|
||||
{
|
||||
}
|
||||
7
vendor/vimeo/psalm/src/Psalm/Exception/IncorrectDocblockException.php
vendored
Normal file
7
vendor/vimeo/psalm/src/Psalm/Exception/IncorrectDocblockException.php
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
final class IncorrectDocblockException extends DocblockParseException
|
||||
{
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/InvalidClasslikeOverrideException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/InvalidClasslikeOverrideException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class InvalidClasslikeOverrideException extends Exception
|
||||
{
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/InvalidMethodOverrideException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/InvalidMethodOverrideException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class InvalidMethodOverrideException extends Exception
|
||||
{
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/RefactorException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/RefactorException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class RefactorException extends Exception
|
||||
{
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/ScopeAnalysisException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/ScopeAnalysisException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class ScopeAnalysisException extends Exception
|
||||
{
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/TypeParseTreeException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/TypeParseTreeException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class TypeParseTreeException extends Exception
|
||||
{
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/UnanalyzedFileException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/UnanalyzedFileException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class UnanalyzedFileException extends Exception
|
||||
{
|
||||
}
|
||||
16
vendor/vimeo/psalm/src/Psalm/Exception/UnpopulatedClasslikeException.php
vendored
Normal file
16
vendor/vimeo/psalm/src/Psalm/Exception/UnpopulatedClasslikeException.php
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use LogicException;
|
||||
|
||||
final class UnpopulatedClasslikeException extends LogicException
|
||||
{
|
||||
public function __construct(string $fq_classlike_name)
|
||||
{
|
||||
parent::__construct(
|
||||
'Cannot check inheritance - \'' . $fq_classlike_name . '\' has not been populated yet.'
|
||||
. ' You may need to defer this check to a later phase.',
|
||||
);
|
||||
}
|
||||
}
|
||||
9
vendor/vimeo/psalm/src/Psalm/Exception/UnpreparedAnalysisException.php
vendored
Normal file
9
vendor/vimeo/psalm/src/Psalm/Exception/UnpreparedAnalysisException.php
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class UnpreparedAnalysisException extends Exception
|
||||
{
|
||||
}
|
||||
24
vendor/vimeo/psalm/src/Psalm/Exception/UnresolvableConstantException.php
vendored
Normal file
24
vendor/vimeo/psalm/src/Psalm/Exception/UnresolvableConstantException.php
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class UnresolvableConstantException extends Exception
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $class_name;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $const_name;
|
||||
|
||||
public function __construct(string $class_name, string $const_name)
|
||||
{
|
||||
$this->class_name = $class_name;
|
||||
$this->const_name = $const_name;
|
||||
}
|
||||
}
|
||||
10
vendor/vimeo/psalm/src/Psalm/Exception/UnsupportedIssueToFixException.php
vendored
Normal file
10
vendor/vimeo/psalm/src/Psalm/Exception/UnsupportedIssueToFixException.php
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class UnsupportedIssueToFixException extends Exception
|
||||
{
|
||||
|
||||
}
|
||||
68
vendor/vimeo/psalm/src/Psalm/FileBasedPluginAdapter.php
vendored
Normal file
68
vendor/vimeo/psalm/src/Psalm/FileBasedPluginAdapter.php
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm;
|
||||
|
||||
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
||||
use Psalm\Internal\Scanner\FileScanner;
|
||||
use Psalm\Plugin\PluginEntryPointInterface;
|
||||
use Psalm\Plugin\RegistrationInterface;
|
||||
use SimpleXMLElement;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
use function reset;
|
||||
use function str_replace;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
|
||||
/** @internal */
|
||||
final class FileBasedPluginAdapter implements PluginEntryPointInterface
|
||||
{
|
||||
private string $path;
|
||||
|
||||
private Codebase $codebase;
|
||||
|
||||
private Config $config;
|
||||
|
||||
public function __construct(string $path, Config $config, Codebase $codebase)
|
||||
{
|
||||
if (!$path) {
|
||||
throw new UnexpectedValueException('$path cannot be empty');
|
||||
}
|
||||
|
||||
$this->path = $path;
|
||||
$this->config = $config;
|
||||
$this->codebase = $codebase;
|
||||
}
|
||||
|
||||
public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement $config = null): void
|
||||
{
|
||||
$fq_class_name = $this->getPluginClassForPath($this->path);
|
||||
|
||||
/** @psalm-suppress UnresolvableInclude */
|
||||
require_once($this->path);
|
||||
|
||||
assert(class_exists($fq_class_name));
|
||||
|
||||
$registration->registerHooksFromClass($fq_class_name);
|
||||
}
|
||||
|
||||
private function getPluginClassForPath(string $path): string
|
||||
{
|
||||
$codebase = $this->codebase;
|
||||
|
||||
$path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path);
|
||||
|
||||
$file_storage = $codebase->createFileStorageForPath($path);
|
||||
$file_to_scan = new FileScanner($path, $this->config->shortenFileName($path), true);
|
||||
$file_to_scan->scan(
|
||||
$codebase,
|
||||
$file_storage,
|
||||
);
|
||||
|
||||
$declared_classes = ClassLikeAnalyzer::getClassesForFile($codebase, $path);
|
||||
|
||||
return reset($declared_classes);
|
||||
}
|
||||
}
|
||||
83
vendor/vimeo/psalm/src/Psalm/FileManipulation.php
vendored
Normal file
83
vendor/vimeo/psalm/src/Psalm/FileManipulation.php
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm;
|
||||
|
||||
use function sha1;
|
||||
use function strlen;
|
||||
use function strrpos;
|
||||
use function substr;
|
||||
use function trim;
|
||||
|
||||
final class FileManipulation
|
||||
{
|
||||
/** @var int */
|
||||
public $start;
|
||||
|
||||
/** @var int */
|
||||
public $end;
|
||||
|
||||
/** @var string */
|
||||
public $insertion_text;
|
||||
|
||||
/** @var bool */
|
||||
public $preserve_indentation;
|
||||
|
||||
/** @var bool */
|
||||
public $remove_trailing_newline;
|
||||
|
||||
public function __construct(
|
||||
int $start,
|
||||
int $end,
|
||||
string $insertion_text,
|
||||
bool $preserve_indentation = false,
|
||||
bool $remove_trailing_newline = false
|
||||
) {
|
||||
$this->start = $start;
|
||||
$this->end = $end;
|
||||
$this->insertion_text = $insertion_text;
|
||||
$this->preserve_indentation = $preserve_indentation;
|
||||
$this->remove_trailing_newline = $remove_trailing_newline;
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return $this->start === $this->end
|
||||
? ($this->start . ':' . sha1($this->insertion_text))
|
||||
: ($this->start . ':' . $this->end);
|
||||
}
|
||||
|
||||
public function transform(string $existing_contents): string
|
||||
{
|
||||
if ($this->preserve_indentation) {
|
||||
$newline_pos = strrpos($existing_contents, "\n", $this->start - strlen($existing_contents));
|
||||
|
||||
$newline_pos = $newline_pos !== false ? $newline_pos + 1 : 0;
|
||||
|
||||
$indentation = substr($existing_contents, $newline_pos, $this->start - $newline_pos);
|
||||
|
||||
if (trim($indentation) === '') {
|
||||
$this->insertion_text .= $indentation;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->remove_trailing_newline
|
||||
&& strlen($existing_contents) > $this->end
|
||||
&& $existing_contents[$this->end] === "\n"
|
||||
) {
|
||||
$newline_pos = strrpos($existing_contents, "\n", $this->start - strlen($existing_contents));
|
||||
|
||||
$newline_pos = $newline_pos !== false ? $newline_pos + 1 : 0;
|
||||
|
||||
$indentation = substr($existing_contents, $newline_pos, $this->start - $newline_pos);
|
||||
|
||||
if (trim($indentation) === '') {
|
||||
$this->start -= strlen($indentation);
|
||||
$this->end++;
|
||||
}
|
||||
}
|
||||
|
||||
return substr($existing_contents, 0, $this->start)
|
||||
. $this->insertion_text
|
||||
. substr($existing_contents, $this->end);
|
||||
}
|
||||
}
|
||||
31
vendor/vimeo/psalm/src/Psalm/FileSource.php
vendored
Normal file
31
vendor/vimeo/psalm/src/Psalm/FileSource.php
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm;
|
||||
|
||||
interface FileSource
|
||||
{
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function getFileName(): string;
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function getFilePath(): string;
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function getRootFileName(): string;
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function getRootFilePath(): string;
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function getAliases(): Aliases;
|
||||
}
|
||||
672
vendor/vimeo/psalm/src/Psalm/Internal/Algebra.php
vendored
Normal file
672
vendor/vimeo/psalm/src/Psalm/Internal/Algebra.php
vendored
Normal file
@@ -0,0 +1,672 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal;
|
||||
|
||||
use Psalm\Exception\ComplicatedExpressionException;
|
||||
use Psalm\Storage\Assertion;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_filter;
|
||||
use function array_intersect_key;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_pop;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function mt_rand;
|
||||
use function reset;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class Algebra
|
||||
{
|
||||
/**
|
||||
* @param array<string, non-empty-list<non-empty-list<Assertion>>> $all_types
|
||||
* @return array<string, non-empty-list<non-empty-list<Assertion>>>
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function negateTypes(array $all_types): array
|
||||
{
|
||||
$negated_types = [];
|
||||
|
||||
foreach ($all_types as $key => $anded_types) {
|
||||
if (count($anded_types) > 1) {
|
||||
$new_anded_types = [];
|
||||
|
||||
foreach ($anded_types as $orred_types) {
|
||||
if (count($orred_types) === 1) {
|
||||
$new_anded_types[] = $orred_types[0]->getNegation();
|
||||
} else {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
assert($new_anded_types !== []);
|
||||
|
||||
$negated_types[$key] = [$new_anded_types];
|
||||
continue;
|
||||
}
|
||||
|
||||
$new_orred_types = [];
|
||||
|
||||
foreach ($anded_types[0] as $orred_type) {
|
||||
$new_orred_types[] = [$orred_type->getNegation()];
|
||||
}
|
||||
|
||||
$negated_types[$key] = $new_orred_types;
|
||||
}
|
||||
|
||||
return $negated_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a very simple simplification heuristic
|
||||
* for CNF formulae.
|
||||
*
|
||||
* It simplifies formulae:
|
||||
* ($a) && ($a || $b) => $a
|
||||
* (!$a) && (!$b) && ($a || $b || $c) => $c
|
||||
*
|
||||
* @param list<Clause> $clauses
|
||||
* @return list<Clause>
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function simplifyCNF(array $clauses): array
|
||||
{
|
||||
$clause_count = count($clauses);
|
||||
|
||||
//65536 seems to be a significant threshold, when put at 65537, the code https://psalm.dev/r/216f362ea6 goes
|
||||
//from seconds in analysis to many minutes
|
||||
if ($clause_count > 65_536) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($clause_count > 50) {
|
||||
$all_has_unknown = true;
|
||||
|
||||
foreach ($clauses as $clause) {
|
||||
$clause_has_unknown = false;
|
||||
foreach ($clause->possibilities as $key => $_) {
|
||||
if ($key[0] === '*') {
|
||||
$clause_has_unknown = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$clause_has_unknown) {
|
||||
$all_has_unknown = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($all_has_unknown) {
|
||||
return $clauses;
|
||||
}
|
||||
}
|
||||
|
||||
$cloned_clauses = [];
|
||||
|
||||
// avoid strict duplicates
|
||||
foreach ($clauses as $clause) {
|
||||
$cloned_clauses[$clause->hash] = $clause;
|
||||
}
|
||||
|
||||
// remove impossible types
|
||||
foreach ($cloned_clauses as $clause_a_hash => $clause_a) {
|
||||
if (!$clause_a->reconcilable || $clause_a->wedge) {
|
||||
continue;
|
||||
}
|
||||
$clause_a_keys = array_keys($clause_a->possibilities);
|
||||
|
||||
if (count($clause_a->possibilities) !== 1 || count(array_values($clause_a->possibilities)[0]) !== 1) {
|
||||
foreach ($cloned_clauses as $clause_b) {
|
||||
if ($clause_a === $clause_b || !$clause_b->reconcilable || $clause_b->wedge) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($clause_a_keys === array_keys($clause_b->possibilities)) {
|
||||
$opposing_keys = [];
|
||||
|
||||
foreach ($clause_a->possibilities as $key => $a_possibilities) {
|
||||
$b_possibilities = $clause_b->possibilities[$key];
|
||||
|
||||
if (array_keys($clause_a->possibilities[$key])
|
||||
=== array_keys($clause_b->possibilities[$key])
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($a_possibilities) === 1 && count($b_possibilities) === 1) {
|
||||
if (reset($a_possibilities)->isNegationOf(reset($b_possibilities))) {
|
||||
$opposing_keys[] = $key;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if (count($opposing_keys) === 1) {
|
||||
unset($cloned_clauses[$clause_a_hash]);
|
||||
|
||||
$clause_a = $clause_a->removePossibilities($opposing_keys[0]);
|
||||
|
||||
if (!$clause_a) {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$cloned_clauses[$clause_a->hash] = $clause_a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$clause_var = array_keys($clause_a->possibilities)[0];
|
||||
$only_type = array_pop(array_values($clause_a->possibilities)[0]);
|
||||
$negated_clause_type = $only_type->getNegation();
|
||||
$negated_clause_type_string = (string)$negated_clause_type;
|
||||
|
||||
foreach ($cloned_clauses as $clause_b_hash => $clause_b) {
|
||||
if ($clause_a === $clause_b || !$clause_b->reconcilable || $clause_b->wedge) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($clause_b->possibilities[$clause_var])) {
|
||||
$unmatched = [];
|
||||
$matched = [];
|
||||
|
||||
foreach ($clause_b->possibilities[$clause_var] as $k => $possible_type) {
|
||||
if ((string)$possible_type === $negated_clause_type_string) {
|
||||
$matched[] = $possible_type;
|
||||
} else {
|
||||
$unmatched[$k] = $possible_type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matched) {
|
||||
$clause_var_possibilities = $unmatched;
|
||||
|
||||
unset($cloned_clauses[$clause_b_hash]);
|
||||
|
||||
if (!$clause_var_possibilities) {
|
||||
$updated_clause = $clause_b->removePossibilities($clause_var);
|
||||
|
||||
if ($updated_clause) {
|
||||
$cloned_clauses[$updated_clause->hash] = $updated_clause;
|
||||
}
|
||||
} else {
|
||||
$updated_clause = $clause_b->addPossibilities(
|
||||
$clause_var,
|
||||
$clause_var_possibilities,
|
||||
);
|
||||
|
||||
$cloned_clauses[$updated_clause->hash] = $updated_clause;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$simplified_clauses = [];
|
||||
|
||||
foreach ($cloned_clauses as $clause_a) {
|
||||
$is_redundant = false;
|
||||
|
||||
foreach ($cloned_clauses as $clause_b) {
|
||||
if ($clause_a === $clause_b
|
||||
|| !$clause_b->reconcilable
|
||||
|| $clause_b->wedge
|
||||
|| $clause_a->wedge
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($clause_a->contains($clause_b)) {
|
||||
$is_redundant = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$is_redundant) {
|
||||
$simplified_clauses[$clause_a->hash] = $clause_a;
|
||||
}
|
||||
}
|
||||
|
||||
$clause_count = count($simplified_clauses);
|
||||
|
||||
// simplify (A || X) && (!A || Y) && (X || Y)
|
||||
// to
|
||||
// simplify (A || X) && (!A || Y)
|
||||
// where X and Y are sets of orred terms
|
||||
if ($clause_count > 2 && $clause_count < 256) {
|
||||
$clauses = array_values($simplified_clauses);
|
||||
for ($i = 0; $i < $clause_count; $i++) {
|
||||
$clause_a = $clauses[$i];
|
||||
for ($k = $i + 1; $k < $clause_count; $k++) {
|
||||
$clause_b = $clauses[$k];
|
||||
$common_keys = array_keys(
|
||||
array_intersect_key($clause_a->possibilities, $clause_b->possibilities),
|
||||
);
|
||||
if ($common_keys) {
|
||||
$common_negated_keys = [];
|
||||
foreach ($common_keys as $common_key) {
|
||||
if (count($clause_a->possibilities[$common_key]) === 1
|
||||
&& count($clause_b->possibilities[$common_key]) === 1
|
||||
&& reset($clause_a->possibilities[$common_key])->isNegationOf(
|
||||
reset($clause_b->possibilities[$common_key]),
|
||||
)
|
||||
) {
|
||||
$common_negated_keys[] = $common_key;
|
||||
}
|
||||
}
|
||||
|
||||
if ($common_negated_keys) {
|
||||
$new_possibilities = [];
|
||||
|
||||
foreach ($clause_a->possibilities as $var_id => $possibilities) {
|
||||
if (in_array($var_id, $common_negated_keys, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($new_possibilities[$var_id])) {
|
||||
$new_possibilities[$var_id] = $possibilities;
|
||||
} else {
|
||||
$new_possibilities[$var_id] = array_merge(
|
||||
$new_possibilities[$var_id],
|
||||
$possibilities,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($clause_b->possibilities as $var_id => $possibilities) {
|
||||
if (in_array($var_id, $common_negated_keys, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($new_possibilities[$var_id])) {
|
||||
$new_possibilities[$var_id] = $possibilities;
|
||||
} else {
|
||||
$new_possibilities[$var_id] = array_merge(
|
||||
$new_possibilities[$var_id],
|
||||
$possibilities,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/** @psalm-suppress MixedArgumentTypeCoercion due I think to Psalm bug */
|
||||
$conflict_clause = (new Clause(
|
||||
$new_possibilities,
|
||||
$clause_a->creating_conditional_id,
|
||||
$clause_a->creating_conditional_id,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
[],
|
||||
));
|
||||
|
||||
unset($simplified_clauses[$conflict_clause->hash]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($simplified_clauses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for clauses with only one possible value
|
||||
*
|
||||
* @param list<Clause> $clauses
|
||||
* @param array<string, bool> $cond_referenced_var_ids
|
||||
* @param array<string, array<int, array<int, Assertion>>> $active_truths
|
||||
* @return array<string, list<list<Assertion>>>
|
||||
*/
|
||||
public static function getTruthsFromFormula(
|
||||
array $clauses,
|
||||
?int $creating_conditional_id = null,
|
||||
array &$cond_referenced_var_ids = [],
|
||||
array &$active_truths = []
|
||||
): array {
|
||||
$truths = [];
|
||||
$active_truths = [];
|
||||
|
||||
if ($clauses === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($clauses as $clause) {
|
||||
if (!$clause->reconcilable || count($clause->possibilities) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($clause->possibilities as $var => $possible_types) {
|
||||
if ($var[0] === '*') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if there's only one possible type, return it
|
||||
if (count($possible_types) === 1) {
|
||||
$possible_type = array_pop($possible_types);
|
||||
|
||||
if (isset($truths[$var]) && !isset($clause->redefined_vars[$var])) {
|
||||
$truths[$var][] = [$possible_type];
|
||||
} else {
|
||||
$truths[$var] = [[$possible_type]];
|
||||
}
|
||||
|
||||
if ($creating_conditional_id && $creating_conditional_id === $clause->creating_conditional_id) {
|
||||
if (!isset($active_truths[$var])) {
|
||||
$active_truths[$var] = [];
|
||||
}
|
||||
|
||||
$active_truths[$var][count($truths[$var]) - 1] = [$possible_type];
|
||||
}
|
||||
} else {
|
||||
// if there's only one active clause, return all the non-negation clause members ORed together
|
||||
$things_that_can_be_said = [];
|
||||
|
||||
foreach ($possible_types as $assertion) {
|
||||
$things_that_can_be_said[(string)$assertion] = $assertion;
|
||||
}
|
||||
|
||||
if ($clause->generated && count($possible_types) > 1) {
|
||||
unset($cond_referenced_var_ids[$var]);
|
||||
}
|
||||
|
||||
$truths[$var] = [array_values($things_that_can_be_said)];
|
||||
|
||||
if ($creating_conditional_id && $creating_conditional_id === $clause->creating_conditional_id) {
|
||||
$active_truths[$var] = [array_values($things_that_can_be_said)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $truths;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-list<Clause> $clauses
|
||||
* @return list<Clause>
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function groupImpossibilities(array $clauses): array
|
||||
{
|
||||
$complexity = 1;
|
||||
|
||||
$seed_clauses = [];
|
||||
|
||||
$clause = array_pop($clauses);
|
||||
|
||||
if (!$clause->wedge) {
|
||||
if ($clause->impossibilities === null) {
|
||||
throw new UnexpectedValueException('$clause->impossibilities should not be null');
|
||||
}
|
||||
|
||||
foreach ($clause->impossibilities as $var => $impossible_types) {
|
||||
foreach ($impossible_types as $impossible_type) {
|
||||
$seed_clause = new Clause(
|
||||
[$var => [(string)$impossible_type => $impossible_type]],
|
||||
$clause->creating_conditional_id,
|
||||
$clause->creating_object_id,
|
||||
);
|
||||
|
||||
$seed_clauses[] = $seed_clause;
|
||||
|
||||
++$complexity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$clauses || !$seed_clauses) {
|
||||
return $seed_clauses;
|
||||
}
|
||||
|
||||
$complexity_upper_bound = count($seed_clauses);
|
||||
|
||||
foreach ($clauses as $clause) {
|
||||
$i = 0;
|
||||
foreach ($clause->possibilities as $p) {
|
||||
$i += count($p);
|
||||
}
|
||||
|
||||
$complexity_upper_bound *= $i;
|
||||
|
||||
if ($complexity_upper_bound > 20_000) {
|
||||
throw new ComplicatedExpressionException();
|
||||
}
|
||||
}
|
||||
|
||||
while ($clauses) {
|
||||
$clause = array_pop($clauses);
|
||||
|
||||
$new_clauses = [];
|
||||
|
||||
foreach ($seed_clauses as $grouped_clause) {
|
||||
if ($clause->impossibilities === null) {
|
||||
throw new UnexpectedValueException('$clause->impossibilities should not be null');
|
||||
}
|
||||
|
||||
foreach ($clause->impossibilities as $var => $impossible_types) {
|
||||
foreach ($impossible_types as $impossible_type) {
|
||||
$new_clause_possibilities = $grouped_clause->possibilities;
|
||||
|
||||
if (isset($new_clause_possibilities[$var])) {
|
||||
$impossible_type_string = (string)$impossible_type;
|
||||
$new_clause_possibilities[$var][$impossible_type_string] = $impossible_type;
|
||||
|
||||
foreach ($new_clause_possibilities[$var] as $ak => $av) {
|
||||
foreach ($new_clause_possibilities[$var] as $bk => $bv) {
|
||||
if ($ak == $bk) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($ak !== $impossible_type_string && $bk !== $impossible_type_string) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($av->isNegationOf($bv)) {
|
||||
break 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$new_clause_possibilities[$var] = [(string)$impossible_type => $impossible_type];
|
||||
}
|
||||
|
||||
$new_clause = new Clause(
|
||||
$new_clause_possibilities,
|
||||
$grouped_clause->creating_conditional_id,
|
||||
$clause->creating_object_id,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
[],
|
||||
);
|
||||
|
||||
$new_clauses[] = $new_clause;
|
||||
|
||||
++$complexity;
|
||||
|
||||
if ($complexity > 20_000) {
|
||||
throw new ComplicatedExpressionException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$seed_clauses = $new_clauses;
|
||||
}
|
||||
|
||||
return $seed_clauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Clause> $left_clauses
|
||||
* @param list<Clause> $right_clauses
|
||||
* @return list<Clause>
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function combineOredClauses(
|
||||
array $left_clauses,
|
||||
array $right_clauses,
|
||||
int $conditional_object_id
|
||||
): array {
|
||||
if (count($left_clauses) > 60_000 || count($right_clauses) > 60_000) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$clauses = [];
|
||||
|
||||
$all_wedges = true;
|
||||
$has_wedge = false;
|
||||
|
||||
foreach ($left_clauses as $left_clause) {
|
||||
foreach ($right_clauses as $right_clause) {
|
||||
$all_wedges = $all_wedges && ($left_clause->wedge && $right_clause->wedge);
|
||||
$has_wedge = $has_wedge || ($left_clause->wedge && $right_clause->wedge);
|
||||
}
|
||||
}
|
||||
|
||||
if ($all_wedges) {
|
||||
return [new Clause([], $conditional_object_id, $conditional_object_id, true)];
|
||||
}
|
||||
|
||||
foreach ($left_clauses as $left_clause) {
|
||||
foreach ($right_clauses as $right_clause) {
|
||||
if ($left_clause->wedge && $right_clause->wedge) {
|
||||
// handled below
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var array<string, non-empty-array<string, Assertion>> */
|
||||
$possibilities = [];
|
||||
|
||||
$can_reconcile = true;
|
||||
|
||||
if ($left_clause->wedge ||
|
||||
$right_clause->wedge ||
|
||||
!$left_clause->reconcilable ||
|
||||
!$right_clause->reconcilable
|
||||
) {
|
||||
$can_reconcile = false;
|
||||
}
|
||||
|
||||
foreach ($left_clause->possibilities as $var => $possible_types) {
|
||||
if (isset($right_clause->redefined_vars[$var])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($possibilities[$var])) {
|
||||
$possibilities[$var] = array_merge($possibilities[$var], $possible_types);
|
||||
} else {
|
||||
$possibilities[$var] = $possible_types;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($right_clause->possibilities as $var => $possible_types) {
|
||||
if (isset($possibilities[$var])) {
|
||||
$possibilities[$var] = array_merge($possibilities[$var], $possible_types);
|
||||
} else {
|
||||
$possibilities[$var] = $possible_types;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($possibilities as $var_possibilities) {
|
||||
if (count($var_possibilities) === 2) {
|
||||
$vals = array_values($var_possibilities);
|
||||
/** @psalm-suppress PossiblyUndefinedIntArrayOffset */
|
||||
if ($vals[0]->isNegationOf($vals[1])) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$creating_conditional_id =
|
||||
$right_clause->creating_conditional_id === $left_clause->creating_conditional_id
|
||||
? $right_clause->creating_conditional_id
|
||||
: $conditional_object_id;
|
||||
|
||||
$clauses[] = new Clause(
|
||||
$possibilities,
|
||||
$creating_conditional_id,
|
||||
$creating_conditional_id,
|
||||
false,
|
||||
$can_reconcile,
|
||||
$right_clause->generated
|
||||
|| $left_clause->generated
|
||||
|| count($left_clauses) > 1
|
||||
|| count($right_clauses) > 1,
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($has_wedge) {
|
||||
$clauses[] = new Clause([], $conditional_object_id, $conditional_object_id, true);
|
||||
}
|
||||
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negates a set of clauses
|
||||
* negateClauses([$a || $b]) => !$a && !$b
|
||||
* negateClauses([$a, $b]) => !$a || !$b
|
||||
* negateClauses([$a, $b || $c]) =>
|
||||
* (!$a || !$b) &&
|
||||
* (!$a || !$c)
|
||||
* negateClauses([$a, $b || $c, $d || $e || $f]) =>
|
||||
* (!$a || !$b || !$d) &&
|
||||
* (!$a || !$b || !$e) &&
|
||||
* (!$a || !$b || !$f) &&
|
||||
* (!$a || !$c || !$d) &&
|
||||
* (!$a || !$c || !$e) &&
|
||||
* (!$a || !$c || !$f)
|
||||
*
|
||||
* @param list<Clause> $clauses
|
||||
* @return non-empty-list<Clause>
|
||||
*/
|
||||
public static function negateFormula(array $clauses): array
|
||||
{
|
||||
$clauses = array_filter(
|
||||
$clauses,
|
||||
static fn(Clause $clause): bool => $clause->reconcilable,
|
||||
);
|
||||
|
||||
if (!$clauses) {
|
||||
$cond_id = mt_rand(0, 100_000_000);
|
||||
return [new Clause([], $cond_id, $cond_id, true)];
|
||||
}
|
||||
|
||||
$clauses_with_impossibilities = [];
|
||||
|
||||
foreach ($clauses as $clause) {
|
||||
$clauses_with_impossibilities[] = $clause->calculateNegation();
|
||||
}
|
||||
|
||||
unset($clauses);
|
||||
|
||||
$impossible_clauses = self::groupImpossibilities($clauses_with_impossibilities);
|
||||
|
||||
if (!$impossible_clauses) {
|
||||
$cond_id = mt_rand(0, 100_000_000);
|
||||
return [new Clause([], $cond_id, $cond_id, true)];
|
||||
}
|
||||
|
||||
$negated = self::simplifyCNF($impossible_clauses);
|
||||
|
||||
if (!$negated) {
|
||||
$cond_id = mt_rand(0, 100_000_000);
|
||||
return [new Clause([], $cond_id, $cond_id, true)];
|
||||
}
|
||||
|
||||
return $negated;
|
||||
}
|
||||
}
|
||||
462
vendor/vimeo/psalm/src/Psalm/Internal/Algebra/FormulaGenerator.php
vendored
Normal file
462
vendor/vimeo/psalm/src/Psalm/Internal/Algebra/FormulaGenerator.php
vendored
Normal file
@@ -0,0 +1,462 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Algebra;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\FileSource;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\AssertionFinder;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Clause;
|
||||
use Psalm\Node\Expr\BinaryOp\VirtualBooleanAnd;
|
||||
use Psalm\Node\Expr\BinaryOp\VirtualBooleanOr;
|
||||
use Psalm\Node\Expr\VirtualBooleanNot;
|
||||
use Psalm\Storage\Assertion\Truthy;
|
||||
|
||||
use function count;
|
||||
use function spl_object_id;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class FormulaGenerator
|
||||
{
|
||||
/**
|
||||
* @return list<Clause>
|
||||
*/
|
||||
public static function getFormula(
|
||||
int $conditional_object_id,
|
||||
int $creating_object_id,
|
||||
PhpParser\Node\Expr $conditional,
|
||||
?string $this_class_name,
|
||||
FileSource $source,
|
||||
?Codebase $codebase = null,
|
||||
bool $inside_negation = false,
|
||||
bool $cache = true
|
||||
): array {
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd ||
|
||||
$conditional instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd
|
||||
) {
|
||||
$left_assertions = self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->left),
|
||||
$conditional->left,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
$inside_negation,
|
||||
$cache,
|
||||
);
|
||||
|
||||
$right_assertions = self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->right),
|
||||
$conditional->right,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
$inside_negation,
|
||||
$cache,
|
||||
);
|
||||
|
||||
return [...$left_assertions, ...$right_assertions];
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr ||
|
||||
$conditional instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr
|
||||
) {
|
||||
$left_clauses = self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->left),
|
||||
$conditional->left,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
$inside_negation,
|
||||
$cache,
|
||||
);
|
||||
|
||||
$right_clauses = self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->right),
|
||||
$conditional->right,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
$inside_negation,
|
||||
$cache,
|
||||
);
|
||||
|
||||
return Algebra::combineOredClauses($left_clauses, $right_clauses, $conditional_object_id);
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BooleanNot) {
|
||||
if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
|
||||
$and_expr = new VirtualBooleanAnd(
|
||||
new VirtualBooleanNot(
|
||||
$conditional->expr->left,
|
||||
$conditional->getAttributes(),
|
||||
),
|
||||
new VirtualBooleanNot(
|
||||
$conditional->expr->right,
|
||||
$conditional->getAttributes(),
|
||||
),
|
||||
$conditional->expr->getAttributes(),
|
||||
);
|
||||
|
||||
return self::getFormula(
|
||||
$conditional_object_id,
|
||||
$conditional_object_id,
|
||||
$and_expr,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
$inside_negation,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if ($conditional->expr instanceof PhpParser\Node\Expr\Isset_
|
||||
&& count($conditional->expr->vars) > 1
|
||||
) {
|
||||
$anded_assertions = null;
|
||||
|
||||
if ($cache && $source instanceof StatementsAnalyzer) {
|
||||
$anded_assertions = $source->node_data->getAssertions($conditional->expr);
|
||||
}
|
||||
|
||||
if ($anded_assertions === null) {
|
||||
$anded_assertions = AssertionFinder::scrapeAssertions(
|
||||
$conditional->expr,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
$inside_negation,
|
||||
$cache,
|
||||
);
|
||||
|
||||
if ($cache && $source instanceof StatementsAnalyzer) {
|
||||
$source->node_data->setAssertions($conditional->expr, $anded_assertions);
|
||||
}
|
||||
}
|
||||
|
||||
$clauses = [];
|
||||
|
||||
foreach ($anded_assertions as $assertions) {
|
||||
foreach ($assertions as $var => $anded_types) {
|
||||
$redefined = false;
|
||||
|
||||
if ($var[0] === '=') {
|
||||
/** @var string */
|
||||
$var = substr($var, 1);
|
||||
$redefined = true;
|
||||
}
|
||||
|
||||
foreach ($anded_types as $orred_types) {
|
||||
$mapped_orred_types = [];
|
||||
foreach ($orred_types as $orred_type) {
|
||||
$mapped_orred_types[(string)$orred_type] = $orred_type;
|
||||
}
|
||||
$clauses[] = new Clause(
|
||||
[$var => $mapped_orred_types],
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->expr),
|
||||
false,
|
||||
true,
|
||||
$orred_types[0]->hasEquality(),
|
||||
$redefined ? [$var => true] : [],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Algebra::negateFormula($clauses);
|
||||
}
|
||||
|
||||
if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) {
|
||||
$and_expr = new VirtualBooleanOr(
|
||||
new VirtualBooleanNot(
|
||||
$conditional->expr->left,
|
||||
$conditional->getAttributes(),
|
||||
),
|
||||
new VirtualBooleanNot(
|
||||
$conditional->expr->right,
|
||||
$conditional->getAttributes(),
|
||||
),
|
||||
$conditional->expr->getAttributes(),
|
||||
);
|
||||
|
||||
return self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->expr),
|
||||
$and_expr,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
$inside_negation,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return Algebra::negateFormula(
|
||||
self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->expr),
|
||||
$conditional->expr,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
!$inside_negation,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical
|
||||
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\Equal
|
||||
) {
|
||||
$false_pos = AssertionFinder::hasFalseVariable($conditional);
|
||||
$true_pos = AssertionFinder::hasTrueVariable($conditional);
|
||||
|
||||
if ($false_pos === AssertionFinder::ASSIGNMENT_TO_RIGHT
|
||||
&& ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Equal
|
||||
|| $conditional->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd
|
||||
|| $conditional->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| $conditional->left instanceof PhpParser\Node\Expr\BooleanNot)
|
||||
) {
|
||||
return Algebra::negateFormula(
|
||||
self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->left),
|
||||
$conditional->left,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
!$inside_negation,
|
||||
$cache,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if ($false_pos === AssertionFinder::ASSIGNMENT_TO_LEFT
|
||||
&& ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Equal
|
||||
|| $conditional->right instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd
|
||||
|| $conditional->right instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| $conditional->right instanceof PhpParser\Node\Expr\BooleanNot)
|
||||
) {
|
||||
return Algebra::negateFormula(
|
||||
self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->right),
|
||||
$conditional->right,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
!$inside_negation,
|
||||
$cache,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if ($true_pos === AssertionFinder::ASSIGNMENT_TO_RIGHT
|
||||
&& ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Equal
|
||||
|| $conditional->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd
|
||||
|| $conditional->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| $conditional->left instanceof PhpParser\Node\Expr\BooleanNot)
|
||||
) {
|
||||
return self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->left),
|
||||
$conditional->left,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
$inside_negation,
|
||||
$cache,
|
||||
);
|
||||
}
|
||||
|
||||
if ($true_pos === AssertionFinder::ASSIGNMENT_TO_LEFT
|
||||
&& ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Equal
|
||||
|| $conditional->right instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd
|
||||
|| $conditional->right instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| $conditional->right instanceof PhpParser\Node\Expr\BooleanNot)
|
||||
) {
|
||||
return self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->right),
|
||||
$conditional->right,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
$inside_negation,
|
||||
$cache,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical
|
||||
|| $conditional instanceof PhpParser\Node\Expr\BinaryOp\NotEqual
|
||||
) {
|
||||
$false_pos = AssertionFinder::hasFalseVariable($conditional);
|
||||
$true_pos = AssertionFinder::hasTrueVariable($conditional);
|
||||
|
||||
if ($true_pos === AssertionFinder::ASSIGNMENT_TO_RIGHT
|
||||
&& ($conditional instanceof PhpParser\Node\Expr\BinaryOp\NotEqual
|
||||
|| $conditional->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd
|
||||
|| $conditional->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| $conditional->left instanceof PhpParser\Node\Expr\BooleanNot)
|
||||
) {
|
||||
return Algebra::negateFormula(
|
||||
self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->left),
|
||||
$conditional->left,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
!$inside_negation,
|
||||
$cache,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if ($true_pos === AssertionFinder::ASSIGNMENT_TO_LEFT
|
||||
&& ($conditional instanceof PhpParser\Node\Expr\BinaryOp\NotEqual
|
||||
|| $conditional->right instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd
|
||||
|| $conditional->right instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| $conditional->right instanceof PhpParser\Node\Expr\BooleanNot)
|
||||
) {
|
||||
return Algebra::negateFormula(
|
||||
self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->right),
|
||||
$conditional->right,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
!$inside_negation,
|
||||
$cache,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if ($false_pos === AssertionFinder::ASSIGNMENT_TO_RIGHT
|
||||
&& ($conditional instanceof PhpParser\Node\Expr\BinaryOp\NotEqual
|
||||
|| $conditional->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd
|
||||
|| $conditional->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| $conditional->left instanceof PhpParser\Node\Expr\BooleanNot)
|
||||
) {
|
||||
return self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->left),
|
||||
$conditional->left,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
$inside_negation,
|
||||
$cache,
|
||||
);
|
||||
}
|
||||
|
||||
if ($false_pos === AssertionFinder::ASSIGNMENT_TO_LEFT
|
||||
&& ($conditional instanceof PhpParser\Node\Expr\BinaryOp\NotEqual
|
||||
|| $conditional->right instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd
|
||||
|| $conditional->right instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| $conditional->right instanceof PhpParser\Node\Expr\BooleanNot)
|
||||
) {
|
||||
return self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->right),
|
||||
$conditional->right,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
$inside_negation,
|
||||
$cache,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\Cast\Bool_) {
|
||||
return self::getFormula(
|
||||
$conditional_object_id,
|
||||
spl_object_id($conditional->expr),
|
||||
$conditional->expr,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
$inside_negation,
|
||||
$cache,
|
||||
);
|
||||
}
|
||||
|
||||
$anded_assertions = null;
|
||||
|
||||
if ($cache && $source instanceof StatementsAnalyzer) {
|
||||
$anded_assertions = $source->node_data->getAssertions($conditional);
|
||||
}
|
||||
|
||||
if ($anded_assertions === null) {
|
||||
$anded_assertions = AssertionFinder::scrapeAssertions(
|
||||
$conditional,
|
||||
$this_class_name,
|
||||
$source,
|
||||
$codebase,
|
||||
$inside_negation,
|
||||
$cache,
|
||||
);
|
||||
|
||||
if ($cache && $source instanceof StatementsAnalyzer) {
|
||||
$source->node_data->setAssertions($conditional, $anded_assertions);
|
||||
}
|
||||
}
|
||||
|
||||
$clauses = [];
|
||||
|
||||
foreach ($anded_assertions as $assertions) {
|
||||
foreach ($assertions as $var => $anded_types) {
|
||||
$redefined = false;
|
||||
|
||||
if ($var[0] === '=') {
|
||||
/** @var string */
|
||||
$var = substr($var, 1);
|
||||
$redefined = true;
|
||||
}
|
||||
|
||||
foreach ($anded_types as $orred_types) {
|
||||
$mapped_orred_types = [];
|
||||
foreach ($orred_types as $orred_type) {
|
||||
$mapped_orred_types[(string)$orred_type] = $orred_type;
|
||||
}
|
||||
$clauses[] = new Clause(
|
||||
[$var => $mapped_orred_types],
|
||||
$conditional_object_id,
|
||||
$creating_object_id,
|
||||
false,
|
||||
true,
|
||||
$orred_types[0]->hasEquality(),
|
||||
$redefined ? [$var => true] : [],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($clauses) {
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
/** @psalm-suppress MixedOperand */
|
||||
$conditional_ref = '*' . $conditional->getAttribute('startFilePos')
|
||||
. ':' . $conditional->getAttribute('endFilePos');
|
||||
|
||||
return [
|
||||
new Clause(
|
||||
[$conditional_ref => ['truthy' => new Truthy()]],
|
||||
$conditional_object_id,
|
||||
$creating_object_id,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
140
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/AlgebraAnalyzer.php
vendored
Normal file
140
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/AlgebraAnalyzer.php
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Exception\ComplicatedExpressionException;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Clause;
|
||||
use Psalm\Issue\ParadoxicalCondition;
|
||||
use Psalm\Issue\RedundantCondition;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Storage\Assertion\InArray;
|
||||
use Psalm\Storage\Assertion\NotInArray;
|
||||
|
||||
use function array_intersect_key;
|
||||
use function count;
|
||||
use function implode;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class AlgebraAnalyzer
|
||||
{
|
||||
/**
|
||||
* This looks to see if there are any clauses in one formula that contradict
|
||||
* clauses in another formula, or clauses that duplicate previous clauses
|
||||
*
|
||||
* e.g.
|
||||
* if ($a) { }
|
||||
* elseif ($a) { }
|
||||
*
|
||||
* @param list<Clause> $formula_1
|
||||
* @param list<Clause> $formula_2
|
||||
* @param array<string, int> $new_assigned_var_ids
|
||||
*/
|
||||
public static function checkForParadox(
|
||||
array $formula_1,
|
||||
array $formula_2,
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node $stmt,
|
||||
array $new_assigned_var_ids
|
||||
): void {
|
||||
try {
|
||||
$negated_formula2 = Algebra::negateFormula($formula_2);
|
||||
} catch (ComplicatedExpressionException $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$formula_1_hashes = [];
|
||||
|
||||
foreach ($formula_1 as $formula_1_clause) {
|
||||
$formula_1_hashes[$formula_1_clause->hash] = true;
|
||||
}
|
||||
|
||||
$formula_2_hashes = [];
|
||||
|
||||
foreach ($formula_2 as $formula_2_clause) {
|
||||
$hash = $formula_2_clause->hash;
|
||||
|
||||
if (!$formula_2_clause->generated
|
||||
&& !$formula_2_clause->wedge
|
||||
&& $formula_2_clause->reconcilable
|
||||
&& (isset($formula_1_hashes[$hash]) || isset($formula_2_hashes[$hash]))
|
||||
&& !array_intersect_key($new_assigned_var_ids, $formula_2_clause->possibilities)
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new RedundantCondition(
|
||||
$formula_2_clause . ' has already been asserted',
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
'already asserted ' . $formula_2_clause,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
|
||||
$formula_2_hashes[$hash] = true;
|
||||
}
|
||||
|
||||
// remove impossible types
|
||||
foreach ($negated_formula2 as $negated_clause_2) {
|
||||
if (!$negated_clause_2->reconcilable || $negated_clause_2->wedge) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($formula_1 as $clause_1) {
|
||||
if ($negated_clause_2 === $clause_1 || !$clause_1->reconcilable || $clause_1->wedge) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$negated_clause_2_contains_1_possibilities = true;
|
||||
|
||||
foreach ($clause_1->possibilities as $key => $keyed_possibilities) {
|
||||
if (!isset($negated_clause_2->possibilities[$key])) {
|
||||
$negated_clause_2_contains_1_possibilities = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($negated_clause_2->possibilities[$key] != $keyed_possibilities) {
|
||||
$negated_clause_2_contains_1_possibilities = false;
|
||||
break;
|
||||
}
|
||||
foreach ($keyed_possibilities as $possibility) {
|
||||
if ($possibility instanceof InArray || $possibility instanceof NotInArray) {
|
||||
$negated_clause_2_contains_1_possibilities = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($negated_clause_2_contains_1_possibilities) {
|
||||
$mini_formula_2 = Algebra::negateFormula([$negated_clause_2]);
|
||||
|
||||
if (!$mini_formula_2[0]->wedge) {
|
||||
if (count($mini_formula_2) > 1) {
|
||||
$paradox_message = 'Condition ((' . implode(') && (', $mini_formula_2) . '))'
|
||||
. ' contradicts a previously-established condition (' . $clause_1 . ')';
|
||||
} else {
|
||||
$paradox_message = 'Condition (' . $mini_formula_2[0] . ')'
|
||||
. ' contradicts a previously-established condition (' . $clause_1 . ')';
|
||||
}
|
||||
} else {
|
||||
$paradox_message = 'Condition not(' . $negated_clause_2 . ')'
|
||||
. ' contradicts a previously-established condition (' . $clause_1 . ')';
|
||||
}
|
||||
|
||||
IssueBuffer::maybeAdd(
|
||||
new ParadoxicalCondition(
|
||||
$paradox_message,
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
396
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php
vendored
Normal file
396
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php
vendored
Normal file
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use Generator;
|
||||
use PhpParser\Node\Arg;
|
||||
use PhpParser\Node\Attribute;
|
||||
use PhpParser\Node\AttributeGroup;
|
||||
use PhpParser\Node\Expr\New_;
|
||||
use PhpParser\Node\Name\FullyQualified;
|
||||
use PhpParser\Node\Stmt\Expression;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Codebase\ConstantTypeResolver;
|
||||
use Psalm\Internal\Provider\NodeDataProvider;
|
||||
use Psalm\Internal\Scanner\UnresolvedConstantComponent;
|
||||
use Psalm\Issue\InvalidAttribute;
|
||||
use Psalm\Issue\UndefinedClass;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use Psalm\Storage\HasAttributesInterface;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_shift;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function reset;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class AttributesAnalyzer
|
||||
{
|
||||
private const TARGET_DESCRIPTIONS = [
|
||||
1 => 'class',
|
||||
2 => 'function',
|
||||
4 => 'method',
|
||||
8 => 'property',
|
||||
16 => 'class constant',
|
||||
32 => 'function/method parameter',
|
||||
40 => 'promoted property',
|
||||
];
|
||||
|
||||
// Copied from Attribute class since that class might not exist at runtime
|
||||
public const TARGET_CLASS = 1;
|
||||
public const TARGET_FUNCTION = 2;
|
||||
public const TARGET_METHOD = 4;
|
||||
public const TARGET_PROPERTY = 8;
|
||||
public const TARGET_CLASS_CONSTANT = 16;
|
||||
public const TARGET_PARAMETER = 32;
|
||||
public const TARGET_ALL = 63;
|
||||
public const IS_REPEATABLE = 64;
|
||||
|
||||
/**
|
||||
* @param array<array-key, AttributeGroup> $attribute_groups
|
||||
* @param key-of<self::TARGET_DESCRIPTIONS> $target
|
||||
* @param array<array-key, string> $suppressed_issues
|
||||
*/
|
||||
public static function analyze(
|
||||
SourceAnalyzer $source,
|
||||
Context $context,
|
||||
HasAttributesInterface $storage,
|
||||
array $attribute_groups,
|
||||
int $target,
|
||||
array $suppressed_issues
|
||||
): void {
|
||||
$codebase = $source->getCodebase();
|
||||
$appearing_non_repeatable_attributes = [];
|
||||
foreach (self::iterateAttributeNodes($attribute_groups) as $attribute) {
|
||||
if ($attribute->name instanceof FullyQualified) {
|
||||
$fq_attribute_name = (string) $attribute->name;
|
||||
} else {
|
||||
$fq_attribute_name = ClassLikeAnalyzer::getFQCLNFromNameObject($attribute->name, $source->getAliases());
|
||||
}
|
||||
|
||||
$attribute_name = (string) $attribute->name;
|
||||
$attribute_name_location = new CodeLocation($source, $attribute->name);
|
||||
|
||||
$attribute_class_storage = $codebase->classlikes->classExists($fq_attribute_name)
|
||||
? $codebase->classlike_storage_provider->get($fq_attribute_name)
|
||||
: null;
|
||||
|
||||
$attribute_class_flags = self::getAttributeClassFlags(
|
||||
$source,
|
||||
$attribute_name,
|
||||
$fq_attribute_name,
|
||||
$attribute_name_location,
|
||||
$attribute_class_storage,
|
||||
$suppressed_issues,
|
||||
);
|
||||
|
||||
self::analyzeAttributeConstruction(
|
||||
$source,
|
||||
$context,
|
||||
$fq_attribute_name,
|
||||
$attribute,
|
||||
$suppressed_issues,
|
||||
$storage instanceof ClassLikeStorage ? $storage : null,
|
||||
);
|
||||
|
||||
if (($attribute_class_flags & self::IS_REPEATABLE) === 0) {
|
||||
// Not IS_REPEATABLE
|
||||
if (isset($appearing_non_repeatable_attributes[$fq_attribute_name])) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidAttribute(
|
||||
"Attribute {$attribute_name} is not repeatable",
|
||||
$attribute_name_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
}
|
||||
$appearing_non_repeatable_attributes[$fq_attribute_name] = true;
|
||||
}
|
||||
|
||||
if (($attribute_class_flags & $target) === 0) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidAttribute(
|
||||
"Attribute {$attribute_name} cannot be used on a "
|
||||
. self::TARGET_DESCRIPTIONS[$target],
|
||||
$attribute_name_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, string> $suppressed_issues
|
||||
*/
|
||||
private static function analyzeAttributeConstruction(
|
||||
SourceAnalyzer $source,
|
||||
Context $context,
|
||||
string $fq_attribute_name,
|
||||
Attribute $attribute,
|
||||
array $suppressed_issues,
|
||||
?ClassLikeStorage $classlike_storage = null
|
||||
): void {
|
||||
$attribute_name_location = new CodeLocation($source, $attribute->name);
|
||||
|
||||
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
|
||||
$source,
|
||||
$fq_attribute_name,
|
||||
$attribute_name_location,
|
||||
null,
|
||||
null,
|
||||
$suppressed_issues,
|
||||
new ClassLikeNameOptions(
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
),
|
||||
) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strtolower($fq_attribute_name) === 'attribute' && $classlike_storage) {
|
||||
if ($classlike_storage->is_trait) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidAttribute(
|
||||
'Traits cannot act as attribute classes',
|
||||
$attribute_name_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
} elseif ($classlike_storage->is_interface) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidAttribute(
|
||||
'Interfaces cannot act as attribute classes',
|
||||
$attribute_name_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
} elseif ($classlike_storage->abstract) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidAttribute(
|
||||
'Abstract classes cannot act as attribute classes',
|
||||
$attribute_name_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
} elseif (isset($classlike_storage->methods['__construct'])
|
||||
&& $classlike_storage->methods['__construct']->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidAttribute(
|
||||
'Classes with protected/private constructors cannot act as attribute classes',
|
||||
$attribute_name_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
} elseif ($classlike_storage->is_enum) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidAttribute(
|
||||
'Enums cannot act as attribute classes',
|
||||
$attribute_name_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$statements_analyzer = new StatementsAnalyzer(
|
||||
$source,
|
||||
new NodeDataProvider(),
|
||||
);
|
||||
$statements_analyzer->addSuppressedIssues(array_values($suppressed_issues));
|
||||
|
||||
$had_returned = $context->has_returned;
|
||||
$context->has_returned = false;
|
||||
|
||||
IssueBuffer::startRecording();
|
||||
$statements_analyzer->analyze(
|
||||
[new Expression(new New_($attribute->name, $attribute->args, $attribute->getAttributes()))],
|
||||
// Use a new Context for the Attribute attribute so that it can't access `self`
|
||||
strtolower($fq_attribute_name) === "attribute" ? new Context() : $context,
|
||||
);
|
||||
$context->has_returned = $had_returned;
|
||||
|
||||
$issues = IssueBuffer::clearRecordingLevel();
|
||||
IssueBuffer::stopRecording();
|
||||
foreach ($issues as $issue) {
|
||||
if ($issue instanceof UndefinedClass && $issue->fq_classlike_name === $fq_attribute_name) {
|
||||
// Remove UndefinedClass for the attribute, since we already added UndefinedAttribute
|
||||
continue;
|
||||
}
|
||||
IssueBuffer::bubbleUp($issue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, string> $suppressed_issues
|
||||
*/
|
||||
private static function getAttributeClassFlags(
|
||||
SourceAnalyzer $source,
|
||||
string $attribute_name,
|
||||
string $fq_attribute_name,
|
||||
CodeLocation $attribute_name_location,
|
||||
?ClassLikeStorage $attribute_class_storage,
|
||||
array $suppressed_issues
|
||||
): int {
|
||||
if (strtolower($fq_attribute_name) === "attribute") {
|
||||
// We override this here because we still want to analyze attributes
|
||||
// for PHP 7.4 when the Attribute class doesn't yet exist.
|
||||
return self::TARGET_CLASS;
|
||||
}
|
||||
|
||||
if ($attribute_class_storage === null) {
|
||||
return self::TARGET_ALL; // Defaults to TARGET_ALL
|
||||
}
|
||||
|
||||
foreach ($attribute_class_storage->attributes as $attribute_attribute) {
|
||||
if ($attribute_attribute->fq_class_name === 'Attribute') {
|
||||
if (!$attribute_attribute->args) {
|
||||
return self::TARGET_ALL; // Defaults to TARGET_ALL
|
||||
}
|
||||
|
||||
$first_arg = reset($attribute_attribute->args);
|
||||
|
||||
$first_arg_type = $first_arg->type;
|
||||
|
||||
if ($first_arg_type instanceof UnresolvedConstantComponent) {
|
||||
$first_arg_type = new Union([
|
||||
ConstantTypeResolver::resolve(
|
||||
$source->getCodebase()->classlikes,
|
||||
$first_arg_type,
|
||||
$source instanceof StatementsAnalyzer ? $source : null,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
if (!$first_arg_type->isSingleIntLiteral()) {
|
||||
return self::TARGET_ALL; // Fall back to default if it's invalid
|
||||
}
|
||||
|
||||
return $first_arg_type->getSingleIntLiteral()->value;
|
||||
}
|
||||
}
|
||||
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidAttribute(
|
||||
"The class {$attribute_name} doesn't have the Attribute attribute",
|
||||
$attribute_name_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
|
||||
return self::TARGET_ALL; // Fall back to default if it's invalid
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable<AttributeGroup> $attribute_groups
|
||||
* @return Generator<int, Attribute>
|
||||
*/
|
||||
private static function iterateAttributeNodes(iterable $attribute_groups): Generator
|
||||
{
|
||||
foreach ($attribute_groups as $attribute_group) {
|
||||
foreach ($attribute_group->attrs as $attribute) {
|
||||
yield $attribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze Reflection getAttributes method calls.
|
||||
|
||||
* @param list<Arg> $args
|
||||
*/
|
||||
public static function analyzeGetAttributes(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
string $method_id,
|
||||
array $args
|
||||
): void {
|
||||
if (count($args) !== 1) {
|
||||
// We skip this analysis if $flags is specified on getAttributes, since the only option
|
||||
// is ReflectionAttribute::IS_INSTANCEOF, which causes getAttributes to return children.
|
||||
// When returning children we don't want to limit this since a child could add a target.
|
||||
return;
|
||||
}
|
||||
|
||||
switch ($method_id) {
|
||||
case "ReflectionClass::getattributes":
|
||||
$target = self::TARGET_CLASS;
|
||||
break;
|
||||
case "ReflectionFunction::getattributes":
|
||||
$target = self::TARGET_FUNCTION;
|
||||
break;
|
||||
case "ReflectionMethod::getattributes":
|
||||
$target = self::TARGET_METHOD;
|
||||
break;
|
||||
case "ReflectionProperty::getattributes":
|
||||
$target = self::TARGET_PROPERTY;
|
||||
break;
|
||||
case "ReflectionClassConstant::getattributes":
|
||||
$target = self::TARGET_CLASS_CONSTANT;
|
||||
break;
|
||||
case "ReflectionParameter::getattributes":
|
||||
$target = self::TARGET_PARAMETER;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
$arg = $args[0];
|
||||
if ($arg->name !== null) {
|
||||
for (; !empty($args) && ($arg->name->name ?? null) !== "name"; $arg = array_shift($args));
|
||||
if ($arg->name->name ?? null !== "name") {
|
||||
// No named argument for "name" parameter
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$arg_type = $statements_analyzer->getNodeTypeProvider()->getType($arg->value);
|
||||
if ($arg_type === null || !$arg_type->isSingle() || !$arg_type->hasLiteralString()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$class_string = $arg_type->getSingleAtomic();
|
||||
assert($class_string instanceof TLiteralString);
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
if (!$codebase->classExists($class_string->value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($class_string->value);
|
||||
$arg_location = new CodeLocation($statements_analyzer, $arg);
|
||||
$class_attribute_target = self::getAttributeClassFlags(
|
||||
$statements_analyzer,
|
||||
$class_string->value,
|
||||
$class_string->value,
|
||||
$arg_location,
|
||||
$class_storage,
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
if (($class_attribute_target & $target) === 0) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidAttribute(
|
||||
"Attribute {$class_string->value} cannot be used on a "
|
||||
. self::TARGET_DESCRIPTIONS[$target],
|
||||
$arg_location,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
168
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/CanAlias.php
vendored
Normal file
168
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/CanAlias.php
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Aliases;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\FileManipulation;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
|
||||
use function implode;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
trait CanAlias
|
||||
{
|
||||
/**
|
||||
* @var array<lowercase-string, string>
|
||||
*/
|
||||
private array $aliased_classes = [];
|
||||
|
||||
/**
|
||||
* @var array<lowercase-string, CodeLocation>
|
||||
*/
|
||||
private array $aliased_class_locations = [];
|
||||
|
||||
/**
|
||||
* @var array<lowercase-string, string>
|
||||
*/
|
||||
private array $aliased_classes_flipped = [];
|
||||
|
||||
/**
|
||||
* @var array<lowercase-string, string>
|
||||
*/
|
||||
private array $aliased_classes_flipped_replaceable = [];
|
||||
|
||||
/**
|
||||
* @var array<lowercase-string, non-empty-string>
|
||||
*/
|
||||
private array $aliased_functions = [];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private array $aliased_constants = [];
|
||||
|
||||
public function visitUse(PhpParser\Node\Stmt\Use_ $stmt): void
|
||||
{
|
||||
$codebase = $this->getCodebase();
|
||||
|
||||
foreach ($stmt->uses as $use) {
|
||||
$use_path = implode('\\', $use->name->parts);
|
||||
$use_path_lc = strtolower($use_path);
|
||||
$use_alias = $use->alias->name ?? $use->name->getLast();
|
||||
$use_alias_lc = strtolower($use_alias);
|
||||
|
||||
switch ($use->type !== PhpParser\Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $stmt->type) {
|
||||
case PhpParser\Node\Stmt\Use_::TYPE_FUNCTION:
|
||||
$this->aliased_functions[$use_alias_lc] = $use_path;
|
||||
break;
|
||||
|
||||
case PhpParser\Node\Stmt\Use_::TYPE_CONSTANT:
|
||||
$this->aliased_constants[$use_alias] = $use_path;
|
||||
break;
|
||||
|
||||
case PhpParser\Node\Stmt\Use_::TYPE_NORMAL:
|
||||
$codebase->analyzer->addOffsetReference(
|
||||
$this->getFilePath(),
|
||||
(int) $use->getAttribute('startFilePos'),
|
||||
(int) $use->getAttribute('endFilePos'),
|
||||
$use_path,
|
||||
);
|
||||
if ($codebase->collect_locations) {
|
||||
// register the path
|
||||
$codebase->use_referencing_locations[$use_path_lc][] =
|
||||
new CodeLocation($this, $use);
|
||||
}
|
||||
|
||||
if ($codebase->alter_code) {
|
||||
if (isset($codebase->class_transforms[$use_path_lc])) {
|
||||
$new_fq_class_name = $codebase->class_transforms[$use_path_lc];
|
||||
|
||||
$file_manipulations = [];
|
||||
|
||||
$file_manipulations[] = new FileManipulation(
|
||||
(int) $use->getAttribute('startFilePos'),
|
||||
(int) $use->getAttribute('endFilePos') + 1,
|
||||
$new_fq_class_name . ($use->alias ? ' as ' . $use_alias : ''),
|
||||
);
|
||||
|
||||
FileManipulationBuffer::add($this->getFilePath(), $file_manipulations);
|
||||
}
|
||||
|
||||
$this->aliased_classes_flipped_replaceable[$use_path_lc] = $use_alias;
|
||||
}
|
||||
|
||||
$this->aliased_classes[$use_alias_lc] = $use_path;
|
||||
$this->aliased_class_locations[$use_alias_lc] = new CodeLocation($this, $stmt);
|
||||
$this->aliased_classes_flipped[$use_path_lc] = $use_alias;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function visitGroupUse(PhpParser\Node\Stmt\GroupUse $stmt): void
|
||||
{
|
||||
$use_prefix = implode('\\', $stmt->prefix->parts);
|
||||
|
||||
$codebase = $this->getCodebase();
|
||||
|
||||
foreach ($stmt->uses as $use) {
|
||||
$use_path = $use_prefix . '\\' . implode('\\', $use->name->parts);
|
||||
$use_alias = $use->alias->name ?? $use->name->getLast();
|
||||
|
||||
switch ($use->type !== PhpParser\Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $stmt->type) {
|
||||
case PhpParser\Node\Stmt\Use_::TYPE_FUNCTION:
|
||||
$this->aliased_functions[strtolower($use_alias)] = $use_path;
|
||||
break;
|
||||
|
||||
case PhpParser\Node\Stmt\Use_::TYPE_CONSTANT:
|
||||
$this->aliased_constants[$use_alias] = $use_path;
|
||||
break;
|
||||
|
||||
case PhpParser\Node\Stmt\Use_::TYPE_NORMAL:
|
||||
if ($codebase->collect_locations) {
|
||||
// register the path
|
||||
$codebase->use_referencing_locations[strtolower($use_path)][] =
|
||||
new CodeLocation($this, $use);
|
||||
}
|
||||
|
||||
$this->aliased_classes[strtolower($use_alias)] = $use_path;
|
||||
$this->aliased_classes_flipped[strtolower($use_path)] = $use_alias;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return array<lowercase-string, string>
|
||||
*/
|
||||
public function getAliasedClassesFlipped(): array
|
||||
{
|
||||
return $this->aliased_classes_flipped;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getAliasedClassesFlippedReplaceable(): array
|
||||
{
|
||||
return $this->aliased_classes_flipped_replaceable;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getAliases(): Aliases
|
||||
{
|
||||
return new Aliases(
|
||||
$this->getNamespace(),
|
||||
$this->aliased_classes,
|
||||
$this->aliased_functions,
|
||||
$this->aliased_constants,
|
||||
);
|
||||
}
|
||||
}
|
||||
2496
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php
vendored
Normal file
2496
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassAnalyzer.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
820
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php
vendored
Normal file
820
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php
vendored
Normal file
@@ -0,0 +1,820 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use PhpParser;
|
||||
use Psalm\Aliases;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\Internal\Provider\NodeDataProvider;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
|
||||
use Psalm\Issue\InaccessibleProperty;
|
||||
use Psalm\Issue\InvalidClass;
|
||||
use Psalm\Issue\InvalidTemplateParam;
|
||||
use Psalm\Issue\MissingDependency;
|
||||
use Psalm\Issue\MissingTemplateParam;
|
||||
use Psalm\Issue\ReservedWord;
|
||||
use Psalm\Issue\TooManyTemplateParams;
|
||||
use Psalm\Issue\UndefinedAttributeClass;
|
||||
use Psalm\Issue\UndefinedClass;
|
||||
use Psalm\Issue\UndefinedDocblockClass;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Plugin\EventHandler\Event\AfterClassLikeExistenceCheckEvent;
|
||||
use Psalm\StatementsSource;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Union;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_keys;
|
||||
use function array_pop;
|
||||
use function array_search;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function gettype;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function preg_match;
|
||||
use function preg_replace;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class ClassLikeAnalyzer extends SourceAnalyzer
|
||||
{
|
||||
public const VISIBILITY_PUBLIC = 1;
|
||||
public const VISIBILITY_PROTECTED = 2;
|
||||
public const VISIBILITY_PRIVATE = 3;
|
||||
|
||||
public const SPECIAL_TYPES = [
|
||||
'int' => 'int',
|
||||
'string' => 'string',
|
||||
'float' => 'float',
|
||||
'bool' => 'bool',
|
||||
'false' => 'false',
|
||||
'object' => 'object',
|
||||
'never' => 'never',
|
||||
'callable' => 'callable',
|
||||
'array' => 'array',
|
||||
'iterable' => 'iterable',
|
||||
'null' => 'null',
|
||||
'mixed' => 'mixed',
|
||||
];
|
||||
|
||||
public const GETTYPE_TYPES = [
|
||||
'boolean' => true,
|
||||
'integer' => true,
|
||||
'double' => true,
|
||||
'string' => true,
|
||||
'array' => true,
|
||||
'object' => true,
|
||||
'resource' => true,
|
||||
'resource (closed)' => true,
|
||||
'NULL' => true,
|
||||
'unknown type' => true,
|
||||
];
|
||||
|
||||
protected PhpParser\Node\Stmt\ClassLike $class;
|
||||
|
||||
public FileAnalyzer $file_analyzer;
|
||||
|
||||
protected string $fq_class_name;
|
||||
|
||||
/**
|
||||
* The parent class
|
||||
*/
|
||||
protected ?string $parent_fq_class_name = null;
|
||||
|
||||
/**
|
||||
* @var PhpParser\Node\Stmt[]
|
||||
*/
|
||||
protected array $leftover_stmts = [];
|
||||
|
||||
protected ClassLikeStorage $storage;
|
||||
|
||||
public function __construct(PhpParser\Node\Stmt\ClassLike $class, SourceAnalyzer $source, string $fq_class_name)
|
||||
{
|
||||
$this->class = $class;
|
||||
$this->source = $source;
|
||||
$this->file_analyzer = $source->getFileAnalyzer();
|
||||
$this->fq_class_name = $fq_class_name;
|
||||
$codebase = $source->getCodebase();
|
||||
$this->storage = $codebase->classlike_storage_provider->get($fq_class_name);
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
unset($this->source);
|
||||
unset($this->file_analyzer);
|
||||
}
|
||||
|
||||
public function getMethodMutations(
|
||||
string $method_name,
|
||||
Context $context
|
||||
): void {
|
||||
$project_analyzer = $this->getFileAnalyzer()->project_analyzer;
|
||||
$codebase = $project_analyzer->getCodebase();
|
||||
|
||||
foreach ($this->class->stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod &&
|
||||
strtolower($stmt->name->name) === strtolower($method_name)
|
||||
) {
|
||||
$method_analyzer = new MethodAnalyzer($stmt, $this);
|
||||
|
||||
$method_analyzer->analyze($context, new NodeDataProvider(), null, true);
|
||||
|
||||
$context->clauses = [];
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\TraitUse) {
|
||||
foreach ($stmt->traits as $trait) {
|
||||
$fq_trait_name = self::getFQCLNFromNameObject(
|
||||
$trait,
|
||||
$this->source->getAliases(),
|
||||
);
|
||||
|
||||
$trait_file_analyzer = $project_analyzer->getFileAnalyzerForClassLike($fq_trait_name);
|
||||
$trait_node = $codebase->classlikes->getTraitNode($fq_trait_name);
|
||||
$trait_storage = $codebase->classlike_storage_provider->get($fq_trait_name);
|
||||
$trait_aliases = $trait_storage->aliases;
|
||||
|
||||
if ($trait_aliases === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$trait_analyzer = new TraitAnalyzer(
|
||||
$trait_node,
|
||||
$trait_file_analyzer,
|
||||
$fq_trait_name,
|
||||
$trait_aliases,
|
||||
);
|
||||
|
||||
foreach ($trait_node->stmts as $trait_stmt) {
|
||||
if ($trait_stmt instanceof PhpParser\Node\Stmt\ClassMethod &&
|
||||
strtolower($trait_stmt->name->name) === strtolower($method_name)
|
||||
) {
|
||||
$method_analyzer = new MethodAnalyzer($trait_stmt, $trait_analyzer);
|
||||
|
||||
$actual_method_id = $method_analyzer->getMethodId();
|
||||
|
||||
if ($context->self && $context->self !== $this->fq_class_name) {
|
||||
$analyzed_method_id = $method_analyzer->getMethodId($context->self);
|
||||
$declaring_method_id = $codebase->methods->getDeclaringMethodId($analyzed_method_id);
|
||||
|
||||
if ((string) $actual_method_id !== (string) $declaring_method_id) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$method_analyzer->analyze(
|
||||
$context,
|
||||
new NodeDataProvider(),
|
||||
null,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$trait_file_analyzer->clearSourceBeforeDestruction();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getFunctionLikeAnalyzer(string $method_name): ?MethodAnalyzer
|
||||
{
|
||||
foreach ($this->class->stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod &&
|
||||
strtolower($stmt->name->name) === strtolower($method_name)
|
||||
) {
|
||||
return new MethodAnalyzer($stmt, $this);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $suppressed_issues
|
||||
*/
|
||||
public static function checkFullyQualifiedClassLikeName(
|
||||
StatementsSource $statements_source,
|
||||
string $fq_class_name,
|
||||
CodeLocation $code_location,
|
||||
?string $calling_fq_class_name,
|
||||
?string $calling_method_id,
|
||||
array $suppressed_issues,
|
||||
?ClassLikeNameOptions $options = null
|
||||
): ?bool {
|
||||
if ($options === null) {
|
||||
$options = new ClassLikeNameOptions();
|
||||
}
|
||||
|
||||
$codebase = $statements_source->getCodebase();
|
||||
if ($fq_class_name === '') {
|
||||
if (IssueBuffer::accepts(
|
||||
new UndefinedClass(
|
||||
'Class or interface <empty string> does not exist',
|
||||
$code_location,
|
||||
'empty string',
|
||||
),
|
||||
$suppressed_issues,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$fq_class_name = preg_replace('/^\\\/', '', $fq_class_name, 1);
|
||||
|
||||
if (in_array($fq_class_name, ['callable', 'iterable', 'self', 'static', 'parent'], true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (preg_match(
|
||||
'/(^|\\\)(int|float|bool|string|void|null|false|true|object|mixed)$/i',
|
||||
$fq_class_name,
|
||||
) || strtolower($fq_class_name) === 'resource'
|
||||
) {
|
||||
$class_name_parts = explode('\\', $fq_class_name);
|
||||
$class_name = array_pop($class_name_parts);
|
||||
|
||||
IssueBuffer::maybeAdd(
|
||||
new ReservedWord(
|
||||
$class_name . ' is a reserved word',
|
||||
$code_location,
|
||||
$class_name,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$class_exists = $codebase->classlikes->classExists(
|
||||
$fq_class_name,
|
||||
!$options->inferred ? $code_location : null,
|
||||
$calling_fq_class_name,
|
||||
$calling_method_id,
|
||||
);
|
||||
|
||||
$interface_exists = $codebase->classlikes->interfaceExists(
|
||||
$fq_class_name,
|
||||
!$options->inferred ? $code_location : null,
|
||||
$calling_fq_class_name,
|
||||
$calling_method_id,
|
||||
);
|
||||
|
||||
$enum_exists = $codebase->classlikes->enumExists(
|
||||
$fq_class_name,
|
||||
!$options->inferred ? $code_location : null,
|
||||
$calling_fq_class_name,
|
||||
$calling_method_id,
|
||||
);
|
||||
|
||||
if (!$class_exists
|
||||
&& !($interface_exists && $options->allow_interface)
|
||||
&& !($enum_exists && $options->allow_enum)
|
||||
) {
|
||||
if (!$options->allow_trait || !$codebase->classlikes->traitExists($fq_class_name, $code_location)) {
|
||||
if ($options->from_docblock) {
|
||||
if (IssueBuffer::accepts(
|
||||
new UndefinedDocblockClass(
|
||||
'Docblock-defined class, interface or enum named ' . $fq_class_name . ' does not exist',
|
||||
$code_location,
|
||||
$fq_class_name,
|
||||
),
|
||||
$suppressed_issues,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} elseif ($options->from_attribute) {
|
||||
if (IssueBuffer::accepts(
|
||||
new UndefinedAttributeClass(
|
||||
'Attribute class ' . $fq_class_name . ' does not exist',
|
||||
$code_location,
|
||||
$fq_class_name,
|
||||
),
|
||||
$suppressed_issues,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (IssueBuffer::accepts(
|
||||
new UndefinedClass(
|
||||
'Class, interface or enum named ' . $fq_class_name . ' does not exist',
|
||||
$code_location,
|
||||
$fq_class_name,
|
||||
),
|
||||
$suppressed_issues,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$aliased_name = $codebase->classlikes->getUnAliasedName(
|
||||
$fq_class_name,
|
||||
);
|
||||
|
||||
try {
|
||||
$class_storage = $codebase->classlike_storage_provider->get($aliased_name);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
if (!$options->inferred) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($class_storage->invalid_dependencies as $dependency_class_name => $_) {
|
||||
// if the implemented/extended class is stubbed, it may not yet have
|
||||
// been hydrated
|
||||
if ($codebase->classlike_storage_provider->has($dependency_class_name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
new MissingDependency(
|
||||
$fq_class_name . ' depends on class or interface '
|
||||
. $dependency_class_name . ' that does not exist',
|
||||
$code_location,
|
||||
$fq_class_name,
|
||||
),
|
||||
$suppressed_issues,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$options->inferred) {
|
||||
if (($class_exists && !$codebase->classHasCorrectCasing($fq_class_name))
|
||||
|| ($interface_exists && !$codebase->interfaceHasCorrectCasing($fq_class_name))
|
||||
|| ($enum_exists && !$codebase->classlikes->enumHasCorrectCasing($fq_class_name))
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidClass(
|
||||
'Class, interface or enum ' . $fq_class_name . ' has wrong casing',
|
||||
$code_location,
|
||||
$fq_class_name,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$options->inferred) {
|
||||
$event = new AfterClassLikeExistenceCheckEvent(
|
||||
$fq_class_name,
|
||||
$code_location,
|
||||
$statements_source,
|
||||
$codebase,
|
||||
[],
|
||||
);
|
||||
|
||||
$codebase->config->eventDispatcher->dispatchAfterClassLikeExistenceCheck($event);
|
||||
|
||||
$file_manipulations = $event->getFileReplacements();
|
||||
if ($file_manipulations) {
|
||||
FileManipulationBuffer::add($code_location->file_path, $file_manipulations);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the fully-qualified class name from a Name object
|
||||
*/
|
||||
public static function getFQCLNFromNameObject(
|
||||
PhpParser\Node\Name $class_name,
|
||||
Aliases $aliases
|
||||
): string {
|
||||
/** @var string|null */
|
||||
$resolved_name = $class_name->getAttribute('resolvedName');
|
||||
|
||||
if ($resolved_name) {
|
||||
return $resolved_name;
|
||||
}
|
||||
|
||||
if ($class_name instanceof PhpParser\Node\Name\FullyQualified) {
|
||||
return implode('\\', $class_name->parts);
|
||||
}
|
||||
|
||||
if (in_array($class_name->parts[0], ['self', 'static', 'parent'], true)) {
|
||||
return $class_name->parts[0];
|
||||
}
|
||||
|
||||
return Type::getFQCLNFromString(
|
||||
implode('\\', $class_name->parts),
|
||||
$aliases,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return array<lowercase-string, string>
|
||||
*/
|
||||
public function getAliasedClassesFlipped(): array
|
||||
{
|
||||
if ($this->source instanceof NamespaceAnalyzer || $this->source instanceof FileAnalyzer) {
|
||||
return $this->source->getAliasedClassesFlipped();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getAliasedClassesFlippedReplaceable(): array
|
||||
{
|
||||
if ($this->source instanceof NamespaceAnalyzer || $this->source instanceof FileAnalyzer) {
|
||||
return $this->source->getAliasedClassesFlippedReplaceable();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getFQCLN(): string
|
||||
{
|
||||
return $this->fq_class_name;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getClassName(): ?string
|
||||
{
|
||||
return $this->class->name->name ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return array<string, array<string, Union>>|null
|
||||
*/
|
||||
public function getTemplateTypeMap(): ?array
|
||||
{
|
||||
return $this->storage->template_types;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getParentFQCLN(): ?string
|
||||
{
|
||||
return $this->parent_fq_class_name;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function isStatic(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Psalm type from a particular value
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
public static function getTypeFromValue($value): Union
|
||||
{
|
||||
switch (gettype($value)) {
|
||||
case 'boolean':
|
||||
if ($value) {
|
||||
return Type::getTrue();
|
||||
}
|
||||
|
||||
return Type::getFalse();
|
||||
|
||||
case 'integer':
|
||||
return Type::getInt(false, $value);
|
||||
|
||||
case 'double':
|
||||
return Type::getFloat($value);
|
||||
|
||||
case 'string':
|
||||
return Type::getString($value);
|
||||
|
||||
case 'array':
|
||||
return Type::getArray();
|
||||
|
||||
case 'NULL':
|
||||
return Type::getNull();
|
||||
|
||||
default:
|
||||
return Type::getMixed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $suppressed_issues
|
||||
*/
|
||||
public static function checkPropertyVisibility(
|
||||
string $property_id,
|
||||
Context $context,
|
||||
SourceAnalyzer $source,
|
||||
CodeLocation $code_location,
|
||||
array $suppressed_issues,
|
||||
bool $emit_issues = true
|
||||
): ?bool {
|
||||
[$fq_class_name, $property_name] = explode('::$', $property_id);
|
||||
|
||||
$codebase = $source->getCodebase();
|
||||
|
||||
if ($codebase->properties->property_visibility_provider->has($fq_class_name)) {
|
||||
$property_visible = $codebase->properties->property_visibility_provider->isPropertyVisible(
|
||||
$source,
|
||||
$fq_class_name,
|
||||
$property_name,
|
||||
true,
|
||||
$context,
|
||||
$code_location,
|
||||
);
|
||||
|
||||
if ($property_visible !== null) {
|
||||
return $property_visible;
|
||||
}
|
||||
}
|
||||
|
||||
$declaring_property_class = $codebase->properties->getDeclaringClassForProperty(
|
||||
$property_id,
|
||||
true,
|
||||
);
|
||||
$appearing_property_class = $codebase->properties->getAppearingClassForProperty(
|
||||
$property_id,
|
||||
true,
|
||||
);
|
||||
|
||||
if (!$declaring_property_class || !$appearing_property_class) {
|
||||
throw new UnexpectedValueException(
|
||||
'Appearing/Declaring classes are not defined for ' . $property_id,
|
||||
);
|
||||
}
|
||||
|
||||
// if the calling class is the same, we know the property exists, so it must be visible
|
||||
if ($appearing_property_class === $context->self) {
|
||||
return $emit_issues ? null : true;
|
||||
}
|
||||
|
||||
if ($source->getSource() instanceof TraitAnalyzer
|
||||
&& strtolower($declaring_property_class) === strtolower((string) $source->getFQCLN())
|
||||
) {
|
||||
return $emit_issues ? null : true;
|
||||
}
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($declaring_property_class);
|
||||
|
||||
if (!isset($class_storage->properties[$property_name])) {
|
||||
throw new UnexpectedValueException('$storage should not be null for ' . $property_id);
|
||||
}
|
||||
|
||||
$storage = $class_storage->properties[$property_name];
|
||||
|
||||
switch ($storage->visibility) {
|
||||
case self::VISIBILITY_PUBLIC:
|
||||
return $emit_issues ? null : true;
|
||||
|
||||
case self::VISIBILITY_PRIVATE:
|
||||
if ($emit_issues) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InaccessibleProperty(
|
||||
'Cannot access private property ' . $property_id . ' from context ' . $context->self,
|
||||
$code_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
case self::VISIBILITY_PROTECTED:
|
||||
if (!$context->self) {
|
||||
if ($emit_issues) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InaccessibleProperty(
|
||||
'Cannot access protected property ' . $property_id,
|
||||
$code_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($codebase->classExtends($appearing_property_class, $context->self)) {
|
||||
return $emit_issues ? null : true;
|
||||
}
|
||||
|
||||
if (!$codebase->classExtends($context->self, $appearing_property_class)) {
|
||||
if ($emit_issues) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InaccessibleProperty(
|
||||
'Cannot access protected property ' . $property_id . ' from context ' . $context->self,
|
||||
$code_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $emit_issues ? null : true;
|
||||
}
|
||||
|
||||
protected function checkTemplateParams(
|
||||
Codebase $codebase,
|
||||
ClassLikeStorage $storage,
|
||||
ClassLikeStorage $parent_storage,
|
||||
CodeLocation $code_location,
|
||||
int $given_param_count
|
||||
): void {
|
||||
$expected_param_count = $parent_storage->template_types === null
|
||||
? 0
|
||||
: count($parent_storage->template_types);
|
||||
|
||||
if ($expected_param_count > $given_param_count) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new MissingTemplateParam(
|
||||
$storage->name . ' has missing template params when extending ' . $parent_storage->name
|
||||
. ', expecting ' . $expected_param_count,
|
||||
$code_location,
|
||||
),
|
||||
$storage->suppressed_issues + $this->getSuppressedIssues(),
|
||||
);
|
||||
} elseif ($expected_param_count < $given_param_count) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new TooManyTemplateParams(
|
||||
$storage->name . ' has too many template params when extending ' . $parent_storage->name
|
||||
. ', expecting ' . $expected_param_count,
|
||||
$code_location,
|
||||
),
|
||||
$storage->suppressed_issues + $this->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
|
||||
$storage_param_count = ($storage->template_types ? count($storage->template_types) : 0);
|
||||
|
||||
if ($parent_storage->enforce_template_inheritance
|
||||
&& $expected_param_count !== $storage_param_count
|
||||
) {
|
||||
if ($expected_param_count > $storage_param_count) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new MissingTemplateParam(
|
||||
$storage->name . ' requires the same number of template params as ' . $parent_storage->name
|
||||
. ' but saw ' . $storage_param_count,
|
||||
$code_location,
|
||||
),
|
||||
$storage->suppressed_issues + $this->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
new TooManyTemplateParams(
|
||||
$storage->name . ' requires the same number of template params as ' . $parent_storage->name
|
||||
. ' but saw ' . $storage_param_count,
|
||||
$code_location,
|
||||
),
|
||||
$storage->suppressed_issues + $this->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($parent_storage->template_types && $storage->template_extended_params) {
|
||||
$i = 0;
|
||||
|
||||
$previous_extended = [];
|
||||
|
||||
foreach ($parent_storage->template_types as $template_name => $type_map) {
|
||||
// declares the variables
|
||||
foreach ($type_map as $declaring_class => $template_type) {
|
||||
}
|
||||
|
||||
if (isset($storage->template_extended_params[$parent_storage->name][$template_name])) {
|
||||
$extended_type = $storage->template_extended_params[$parent_storage->name][$template_name];
|
||||
|
||||
if (isset($parent_storage->template_covariants[$i])
|
||||
&& !$parent_storage->template_covariants[$i]
|
||||
) {
|
||||
foreach ($extended_type->getAtomicTypes() as $t) {
|
||||
if ($t instanceof TTemplateParam
|
||||
&& $storage->template_types
|
||||
&& $storage->template_covariants
|
||||
&& ($local_offset
|
||||
= array_search($t->param_name, array_keys($storage->template_types)))
|
||||
!== false
|
||||
&& !empty($storage->template_covariants[$local_offset])
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidTemplateParam(
|
||||
'Cannot extend an invariant template param ' . $template_name
|
||||
. ' into a covariant context',
|
||||
$code_location,
|
||||
),
|
||||
$storage->suppressed_issues + $this->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($parent_storage->enforce_template_inheritance) {
|
||||
foreach ($extended_type->getAtomicTypes() as $t) {
|
||||
if (!$t instanceof TTemplateParam
|
||||
|| !isset($storage->template_types[$t->param_name])
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidTemplateParam(
|
||||
'Cannot extend a strictly-enforced parent template param '
|
||||
. $template_name
|
||||
. ' with a non-template type',
|
||||
$code_location,
|
||||
),
|
||||
$storage->suppressed_issues + $this->getSuppressedIssues(),
|
||||
);
|
||||
} elseif ($storage->template_types[$t->param_name][$storage->name]->getId()
|
||||
!== $template_type->getId()
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidTemplateParam(
|
||||
'Cannot extend a strictly-enforced parent template param '
|
||||
. $template_name
|
||||
. ' with constraint ' . $template_type->getId()
|
||||
. ' with a child template param ' . $t->param_name
|
||||
. ' with different constraint '
|
||||
. $storage->template_types[$t->param_name][$storage->name]->getId(),
|
||||
$code_location,
|
||||
),
|
||||
$storage->suppressed_issues + $this->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$template_type->isMixed()) {
|
||||
$template_result = new TemplateResult(
|
||||
$previous_extended ?: [],
|
||||
[],
|
||||
);
|
||||
|
||||
$template_type_copy = TemplateStandinTypeReplacer::replace(
|
||||
$template_type,
|
||||
$template_result,
|
||||
$codebase,
|
||||
null,
|
||||
$extended_type,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
|
||||
if (!UnionTypeComparator::isContainedBy($codebase, $extended_type, $template_type_copy)) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidTemplateParam(
|
||||
'Extended template param ' . $template_name
|
||||
. ' expects type ' . $template_type_copy->getId()
|
||||
. ', type ' . $extended_type->getId() . ' given',
|
||||
$code_location,
|
||||
),
|
||||
$storage->suppressed_issues + $this->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
$previous_extended[$template_name] = [
|
||||
$declaring_class => $extended_type,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$previous_extended[$template_name] = [
|
||||
$declaring_class => $extended_type,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function getClassesForFile(Codebase $codebase, string $file_path): array
|
||||
{
|
||||
try {
|
||||
return $codebase->file_storage_provider->get($file_path)->classlikes_in_file;
|
||||
} catch (InvalidArgumentException $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public function getFileAnalyzer(): FileAnalyzer
|
||||
{
|
||||
return $this->file_analyzer;
|
||||
}
|
||||
}
|
||||
37
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassLikeNameOptions.php
vendored
Normal file
37
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClassLikeNameOptions.php
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ClassLikeNameOptions
|
||||
{
|
||||
public bool $inferred;
|
||||
|
||||
public bool $allow_trait;
|
||||
|
||||
public bool $allow_interface;
|
||||
|
||||
public bool $allow_enum;
|
||||
|
||||
public bool $from_docblock;
|
||||
|
||||
public bool $from_attribute;
|
||||
|
||||
public function __construct(
|
||||
bool $inferred = false,
|
||||
bool $allow_trait = false,
|
||||
bool $allow_interface = true,
|
||||
bool $allow_enum = true,
|
||||
bool $from_docblock = false,
|
||||
bool $from_attribute = false
|
||||
) {
|
||||
$this->inferred = $inferred;
|
||||
$this->allow_trait = $allow_trait;
|
||||
$this->allow_interface = $allow_interface;
|
||||
$this->allow_enum = $allow_enum;
|
||||
$this->from_docblock = $from_docblock;
|
||||
$this->from_attribute = $from_attribute;
|
||||
}
|
||||
}
|
||||
345
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php
vendored
Normal file
345
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php
vendored
Normal file
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Codebase\VariableUseGraph;
|
||||
use Psalm\Internal\DataFlow\DataFlowNode;
|
||||
use Psalm\Internal\PhpVisitor\ShortClosureVisitor;
|
||||
use Psalm\Issue\DuplicateParam;
|
||||
use Psalm\Issue\PossiblyUndefinedVariable;
|
||||
use Psalm\Issue\UndefinedVariable;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TMixed;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
use function preg_match;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @extends FunctionLikeAnalyzer<PhpParser\Node\Expr\Closure|PhpParser\Node\Expr\ArrowFunction>
|
||||
*/
|
||||
class ClosureAnalyzer extends FunctionLikeAnalyzer
|
||||
{
|
||||
/**
|
||||
* @param PhpParser\Node\Expr\Closure|PhpParser\Node\Expr\ArrowFunction $function
|
||||
*/
|
||||
public function __construct(PhpParser\Node\FunctionLike $function, SourceAnalyzer $source)
|
||||
{
|
||||
$codebase = $source->getCodebase();
|
||||
|
||||
$function_id = strtolower($source->getFilePath())
|
||||
. ':' . $function->getLine()
|
||||
. ':' . (int)$function->getAttribute('startFilePos')
|
||||
. ':-:closure';
|
||||
|
||||
$storage = $codebase->getClosureStorage($source->getFilePath(), $function_id);
|
||||
|
||||
parent::__construct($function, $source, $storage);
|
||||
}
|
||||
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getTemplateTypeMap(): ?array
|
||||
{
|
||||
return $this->source->getTemplateTypeMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-lowercase-string
|
||||
*/
|
||||
public function getClosureId(): string
|
||||
{
|
||||
return strtolower($this->getFilePath())
|
||||
. ':' . $this->function->getLine()
|
||||
. ':' . (int)$this->function->getAttribute('startFilePos')
|
||||
. ':-:closure';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node\Expr\Closure|PhpParser\Node\Expr\ArrowFunction $stmt
|
||||
*/
|
||||
public static function analyzeExpression(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\FunctionLike $stmt,
|
||||
Context $context
|
||||
): bool {
|
||||
$closure_analyzer = new ClosureAnalyzer($stmt, $statements_analyzer);
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\Closure
|
||||
&& self::analyzeClosureUses($statements_analyzer, $stmt, $context) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$use_context = new Context($context->self);
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
if (!$statements_analyzer->isStatic() && !$closure_analyzer->isStatic()) {
|
||||
if ($context->collect_mutations &&
|
||||
$context->self &&
|
||||
$codebase->classExtends(
|
||||
$context->self,
|
||||
(string)$statements_analyzer->getFQCLN(),
|
||||
)
|
||||
) {
|
||||
/** @psalm-suppress PossiblyUndefinedStringArrayOffset */
|
||||
$use_context->vars_in_scope['$this'] = $context->vars_in_scope['$this'];
|
||||
} elseif ($context->self) {
|
||||
$this_atomic = new TNamedObject($context->self, true);
|
||||
|
||||
$use_context->vars_in_scope['$this'] = new Union([$this_atomic]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($context->vars_in_scope as $var => $type) {
|
||||
if (strpos($var, '$this->') === 0) {
|
||||
$use_context->vars_in_scope[$var] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($context->self) {
|
||||
$self_class_storage = $codebase->classlike_storage_provider->get($context->self);
|
||||
|
||||
ClassAnalyzer::addContextProperties(
|
||||
$statements_analyzer,
|
||||
$self_class_storage,
|
||||
$use_context,
|
||||
$context->self,
|
||||
$statements_analyzer->getParentFQCLN(),
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($context->vars_possibly_in_scope as $var => $_) {
|
||||
if (strpos($var, '$this->') === 0) {
|
||||
$use_context->vars_possibly_in_scope[$var] = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\Closure) {
|
||||
foreach ($stmt->uses as $use) {
|
||||
if (!is_string($use->var->name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$use_var_id = '$' . $use->var->name;
|
||||
|
||||
// insert the ref into the current context if passed by ref, as whatever we're passing
|
||||
// the closure to could execute it straight away.
|
||||
if ($use->byRef && !$context->hasVariable($use_var_id)) {
|
||||
$context->vars_in_scope[$use_var_id] = new Union([new TMixed()], ['by_ref' => true]);
|
||||
}
|
||||
|
||||
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph
|
||||
&& $context->hasVariable($use_var_id)
|
||||
) {
|
||||
$parent_nodes = $context->vars_in_scope[$use_var_id]->parent_nodes;
|
||||
|
||||
foreach ($parent_nodes as $parent_node) {
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$parent_node,
|
||||
new DataFlowNode('closure-use', 'closure use', null),
|
||||
'closure-use',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$use_context->vars_in_scope[$use_var_id] =
|
||||
$context->hasVariable($use_var_id) && !$use->byRef
|
||||
? $context->vars_in_scope[$use_var_id]
|
||||
: Type::getMixed();
|
||||
|
||||
if ($use->byRef) {
|
||||
$use_context->vars_in_scope[$use_var_id] =
|
||||
$use_context->vars_in_scope[$use_var_id]->setProperties(['by_ref' => true]);
|
||||
$use_context->references_to_external_scope[$use_var_id] = true;
|
||||
}
|
||||
|
||||
$use_context->vars_possibly_in_scope[$use_var_id] = true;
|
||||
|
||||
foreach ($context->vars_in_scope as $var_id => $type) {
|
||||
if (preg_match('/^\$' . $use->var->name . '[\[\-]/', $var_id)) {
|
||||
$use_context->vars_in_scope[$var_id] = $type;
|
||||
$use_context->vars_possibly_in_scope[$var_id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$traverser = new PhpParser\NodeTraverser;
|
||||
|
||||
$short_closure_visitor = new ShortClosureVisitor();
|
||||
|
||||
$traverser->addVisitor($short_closure_visitor);
|
||||
$traverser->traverse($stmt->getStmts());
|
||||
|
||||
foreach ($short_closure_visitor->getUsedVariables() as $use_var_id => $_) {
|
||||
if ($context->hasVariable($use_var_id)) {
|
||||
$use_context->vars_in_scope[$use_var_id] = $context->vars_in_scope[$use_var_id];
|
||||
|
||||
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) {
|
||||
$parent_nodes = $context->vars_in_scope[$use_var_id]->parent_nodes;
|
||||
|
||||
foreach ($parent_nodes as $parent_node) {
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$parent_node,
|
||||
new DataFlowNode('closure-use', 'closure use', null),
|
||||
'closure-use',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$use_context->vars_possibly_in_scope[$use_var_id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$use_context->calling_method_id = $context->calling_method_id;
|
||||
$use_context->phantom_classes = $context->phantom_classes;
|
||||
|
||||
$closure_analyzer->analyze($use_context, $statements_analyzer->node_data, $context, false);
|
||||
|
||||
if ($closure_analyzer->inferred_impure
|
||||
&& $statements_analyzer->getSource() instanceof FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
|
||||
if ($closure_analyzer->inferred_has_mutation
|
||||
&& $statements_analyzer->getSource() instanceof FunctionLikeAnalyzer
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
}
|
||||
|
||||
if (!$statements_analyzer->node_data->getType($stmt)) {
|
||||
$statements_analyzer->node_data->setType($stmt, Type::getClosure());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|null
|
||||
*/
|
||||
public static function analyzeClosureUses(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\Closure $stmt,
|
||||
Context $context
|
||||
): ?bool {
|
||||
$param_names = [];
|
||||
|
||||
foreach ($stmt->params as $i => $param) {
|
||||
if ($param->var instanceof PhpParser\Node\Expr\Variable && is_string($param->var->name)) {
|
||||
$param_names[$i] = $param->var->name;
|
||||
} else {
|
||||
$param_names[$i] = '';
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($stmt->uses as $use) {
|
||||
if (!is_string($use->var->name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$use_var_id = '$' . $use->var->name;
|
||||
|
||||
if (in_array($use->var->name, $param_names)) {
|
||||
if (IssueBuffer::accepts(
|
||||
new DuplicateParam(
|
||||
'Closure use duplicates param name ' . $use_var_id,
|
||||
new CodeLocation($statements_analyzer->getSource(), $use->var),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$context->hasVariable($use_var_id)) {
|
||||
if ($use_var_id === '$argv' || $use_var_id === '$argc') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($use->byRef) {
|
||||
$context->vars_in_scope[$use_var_id] = Type::getMixed();
|
||||
$context->vars_possibly_in_scope[$use_var_id] = true;
|
||||
|
||||
if (!$statements_analyzer->hasVariable($use_var_id)) {
|
||||
$statements_analyzer->registerVariable(
|
||||
$use_var_id,
|
||||
new CodeLocation($statements_analyzer, $use->var),
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isset($context->vars_possibly_in_scope[$use_var_id])) {
|
||||
if ($context->check_variables) {
|
||||
if (IssueBuffer::accepts(
|
||||
new UndefinedVariable(
|
||||
'Cannot find referenced variable ' . $use_var_id,
|
||||
new CodeLocation($statements_analyzer->getSource(), $use->var),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$first_appearance = $statements_analyzer->getFirstAppearance($use_var_id);
|
||||
|
||||
if ($first_appearance) {
|
||||
if (IssueBuffer::accepts(
|
||||
new PossiblyUndefinedVariable(
|
||||
'Possibly undefined variable ' . $use_var_id . ', first seen on line ' .
|
||||
$first_appearance->getLineNumber(),
|
||||
new CodeLocation($statements_analyzer->getSource(), $use->var),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($context->check_variables) {
|
||||
if (IssueBuffer::accepts(
|
||||
new UndefinedVariable(
|
||||
'Cannot find referenced variable ' . $use_var_id,
|
||||
new CodeLocation($statements_analyzer->getSource(), $use->var),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
} elseif ($use->byRef) {
|
||||
$new_type = new Union([new TMixed()], [
|
||||
'parent_nodes' => $context->vars_in_scope[$use_var_id]->parent_nodes,
|
||||
]);
|
||||
|
||||
$context->remove($use_var_id);
|
||||
|
||||
$context->vars_in_scope[$use_var_id] = $new_type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
516
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/CommentAnalyzer.php
vendored
Normal file
516
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/CommentAnalyzer.php
vendored
Normal file
@@ -0,0 +1,516 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Aliases;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\CodeLocation\DocblockTypeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\DocComment;
|
||||
use Psalm\Exception\DocblockParseException;
|
||||
use Psalm\Exception\IncorrectDocblockException;
|
||||
use Psalm\Exception\TypeParseTreeException;
|
||||
use Psalm\FileSource;
|
||||
use Psalm\Internal\Scanner\DocblockParser;
|
||||
use Psalm\Internal\Scanner\ParsedDocblock;
|
||||
use Psalm\Internal\Scanner\VarDocblockComment;
|
||||
use Psalm\Internal\Type\TypeAlias;
|
||||
use Psalm\Internal\Type\TypeExpander;
|
||||
use Psalm\Internal\Type\TypeParser;
|
||||
use Psalm\Internal\Type\TypeTokenizer;
|
||||
use Psalm\Issue\InvalidDocblock;
|
||||
use Psalm\Issue\MissingDocblockType;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type\Union;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use function is_string;
|
||||
use function preg_match;
|
||||
use function preg_replace;
|
||||
use function preg_split;
|
||||
use function rtrim;
|
||||
use function str_replace;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function substr_count;
|
||||
use function trim;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CommentAnalyzer
|
||||
{
|
||||
public const TYPE_REGEX = '(\??\\\?[\(\)A-Za-z0-9_&\<\.=,\>\[\]\-\{\}:|?\\\\]*|\$[a-zA-Z_0-9_]+)';
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, Union>>|null $template_type_map
|
||||
* @param array<string, TypeAlias> $type_aliases
|
||||
* @throws DocblockParseException if there was a problem parsing the docblock
|
||||
* @return list<VarDocblockComment>
|
||||
*/
|
||||
public static function getTypeFromComment(
|
||||
PhpParser\Comment\Doc $comment,
|
||||
FileSource $source,
|
||||
Aliases $aliases,
|
||||
?array $template_type_map = null,
|
||||
?array $type_aliases = null
|
||||
): array {
|
||||
$parsed_docblock = DocComment::parsePreservingLength($comment);
|
||||
|
||||
return self::arrayToDocblocks(
|
||||
$comment,
|
||||
$parsed_docblock,
|
||||
$source,
|
||||
$aliases,
|
||||
$template_type_map,
|
||||
$type_aliases,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, Union>>|null $template_type_map
|
||||
* @param array<string, TypeAlias> $type_aliases
|
||||
* @return list<VarDocblockComment>
|
||||
* @throws DocblockParseException if there was a problem parsing the docblock
|
||||
*/
|
||||
public static function arrayToDocblocks(
|
||||
PhpParser\Comment\Doc $comment,
|
||||
ParsedDocblock $parsed_docblock,
|
||||
FileSource $source,
|
||||
Aliases $aliases,
|
||||
?array $template_type_map = null,
|
||||
?array $type_aliases = null
|
||||
): array {
|
||||
$var_id = null;
|
||||
|
||||
$var_type_tokens = null;
|
||||
$original_type = null;
|
||||
|
||||
$var_comments = [];
|
||||
|
||||
$comment_text = $comment->getText();
|
||||
|
||||
$var_line_number = $comment->getStartLine();
|
||||
|
||||
if (isset($parsed_docblock->combined_tags['var'])) {
|
||||
foreach ($parsed_docblock->combined_tags['var'] as $offset => $var_line) {
|
||||
$var_line = trim($var_line);
|
||||
|
||||
if (!$var_line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type_start = null;
|
||||
$type_end = null;
|
||||
|
||||
$line_parts = self::splitDocLine($var_line);
|
||||
|
||||
$line_number = $comment->getStartLine() + substr_count(
|
||||
$comment_text,
|
||||
"\n",
|
||||
0,
|
||||
$offset - $comment->getStartFilePos(),
|
||||
);
|
||||
$description = $parsed_docblock->description;
|
||||
|
||||
if ($line_parts[0]) {
|
||||
$type_start = $offset;
|
||||
$type_end = $type_start + strlen($line_parts[0]);
|
||||
|
||||
$line_parts[0] = self::sanitizeDocblockType($line_parts[0]);
|
||||
|
||||
if ($line_parts[0] === ''
|
||||
|| ($line_parts[0][0] === '$'
|
||||
&& !preg_match('/^\$this(\||$)/', $line_parts[0]))
|
||||
) {
|
||||
throw new IncorrectDocblockException('Misplaced variable');
|
||||
}
|
||||
|
||||
try {
|
||||
$var_type_tokens = TypeTokenizer::getFullyQualifiedTokens(
|
||||
$line_parts[0],
|
||||
$aliases,
|
||||
$template_type_map,
|
||||
$type_aliases,
|
||||
);
|
||||
} catch (TypeParseTreeException $e) {
|
||||
throw new DocblockParseException($line_parts[0] . ' is not a valid type');
|
||||
}
|
||||
|
||||
$original_type = $line_parts[0];
|
||||
|
||||
$var_line_number = $line_number;
|
||||
|
||||
if (count($line_parts) > 1) {
|
||||
if ($line_parts[1][0] === '$') {
|
||||
$var_id = $line_parts[1];
|
||||
$description = trim(substr($var_line, strlen($line_parts[0]) + strlen($line_parts[1]) + 2));
|
||||
} else {
|
||||
$description = trim(substr($var_line, strlen($line_parts[0]) + 1));
|
||||
}
|
||||
$description = preg_replace('/\\n \\*\\s+/um', ' ', $description);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$var_type_tokens || !$original_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$defined_type = TypeParser::parseTokens(
|
||||
$var_type_tokens,
|
||||
null,
|
||||
$template_type_map ?: [],
|
||||
$type_aliases ?: [],
|
||||
true,
|
||||
);
|
||||
} catch (TypeParseTreeException $e) {
|
||||
throw new DocblockParseException(
|
||||
$line_parts[0] .
|
||||
' is not a valid type' .
|
||||
' ('.$e->getMessage().' in ' .
|
||||
$source->getFilePath() .
|
||||
':' .
|
||||
$comment->getStartLine() .
|
||||
')',
|
||||
);
|
||||
}
|
||||
|
||||
$var_comment = new VarDocblockComment();
|
||||
$var_comment->type = $defined_type;
|
||||
$var_comment->var_id = $var_id;
|
||||
$var_comment->line_number = $var_line_number;
|
||||
$var_comment->type_start = $type_start;
|
||||
$var_comment->type_end = $type_end;
|
||||
$var_comment->description = $description;
|
||||
|
||||
self::decorateVarDocblockComment($var_comment, $parsed_docblock);
|
||||
|
||||
$var_comments[] = $var_comment;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$var_comments
|
||||
&& (isset($parsed_docblock->tags['deprecated'])
|
||||
|| isset($parsed_docblock->tags['internal'])
|
||||
|| isset($parsed_docblock->tags['readonly'])
|
||||
|| isset($parsed_docblock->tags['psalm-readonly'])
|
||||
|| isset($parsed_docblock->tags['psalm-readonly-allow-private-mutation'])
|
||||
|| isset($parsed_docblock->tags['psalm-allow-private-mutation'])
|
||||
|| isset($parsed_docblock->tags['psalm-taint-escape'])
|
||||
|| isset($parsed_docblock->tags['psalm-internal'])
|
||||
|| isset($parsed_docblock->tags['psalm-suppress'])
|
||||
|| $parsed_docblock->description)
|
||||
) {
|
||||
$var_comment = new VarDocblockComment();
|
||||
|
||||
self::decorateVarDocblockComment($var_comment, $parsed_docblock);
|
||||
|
||||
$var_comments[] = $var_comment;
|
||||
}
|
||||
|
||||
return $var_comments;
|
||||
}
|
||||
|
||||
private static function decorateVarDocblockComment(
|
||||
VarDocblockComment $var_comment,
|
||||
ParsedDocblock $parsed_docblock
|
||||
): void {
|
||||
$var_comment->deprecated = isset($parsed_docblock->tags['deprecated']);
|
||||
$var_comment->internal = isset($parsed_docblock->tags['internal']);
|
||||
$var_comment->readonly = isset($parsed_docblock->tags['readonly'])
|
||||
|| isset($parsed_docblock->tags['psalm-readonly'])
|
||||
|| isset($parsed_docblock->tags['psalm-readonly-allow-private-mutation']);
|
||||
|
||||
$var_comment->allow_private_mutation
|
||||
= isset($parsed_docblock->tags['psalm-allow-private-mutation'])
|
||||
|| isset($parsed_docblock->tags['psalm-readonly-allow-private-mutation']);
|
||||
|
||||
if (!$var_comment->description) {
|
||||
$var_comment->description = $parsed_docblock->description;
|
||||
}
|
||||
|
||||
if (isset($parsed_docblock->tags['psalm-taint-escape'])) {
|
||||
foreach ($parsed_docblock->tags['psalm-taint-escape'] as $param) {
|
||||
$param = trim($param);
|
||||
$var_comment->removed_taints[] = $param;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($var_comment->psalm_internal = DocblockParser::handlePsalmInternal($parsed_docblock)) !== 0) {
|
||||
$var_comment->internal = true;
|
||||
}
|
||||
|
||||
if (isset($parsed_docblock->tags['psalm-suppress'])) {
|
||||
foreach ($parsed_docblock->tags['psalm-suppress'] as $offset => $suppress_entry) {
|
||||
foreach (DocComment::parseSuppressList($suppress_entry) as $issue_offset => $suppressed_issue) {
|
||||
$var_comment->suppressed_issues[$issue_offset + $offset] = $suppressed_issue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function sanitizeDocblockType(string $docblock_type): string
|
||||
{
|
||||
$docblock_type = preg_replace('@^[ \t]*\*@m', '', $docblock_type);
|
||||
$docblock_type = preg_replace('/,\n\s+\}/', '}', $docblock_type);
|
||||
return str_replace("\n", '', $docblock_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DocblockParseException if an invalid string is found
|
||||
* @return non-empty-list<string>
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function splitDocLine(string $return_block): array
|
||||
{
|
||||
$brackets = '';
|
||||
|
||||
$type = '';
|
||||
|
||||
$expects_callable_return = false;
|
||||
|
||||
$return_block = str_replace("\t", ' ', $return_block);
|
||||
|
||||
$quote_char = null;
|
||||
$escaped = false;
|
||||
|
||||
for ($i = 0, $l = strlen($return_block); $i < $l; ++$i) {
|
||||
$char = $return_block[$i];
|
||||
$next_char = $i < $l - 1 ? $return_block[$i + 1] : null;
|
||||
$last_char = $i > 0 ? $return_block[$i - 1] : null;
|
||||
|
||||
if ($quote_char) {
|
||||
if ($char === $quote_char && !$escaped) {
|
||||
$quote_char = null;
|
||||
|
||||
$type .= $char;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '\\' && !$escaped && ($next_char === $quote_char || $next_char === '\\')) {
|
||||
$escaped = true;
|
||||
|
||||
$type .= $char;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$escaped = false;
|
||||
|
||||
$type .= $char;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '"' || $char === '\'') {
|
||||
$quote_char = $char;
|
||||
|
||||
$type .= $char;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === ':' && $last_char === ')') {
|
||||
$expects_callable_return = true;
|
||||
|
||||
$type .= $char;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '[' || $char === '{' || $char === '(' || $char === '<') {
|
||||
$brackets .= $char;
|
||||
} elseif ($char === ']' || $char === '}' || $char === ')' || $char === '>') {
|
||||
$last_bracket = substr($brackets, -1);
|
||||
$brackets = substr($brackets, 0, -1);
|
||||
|
||||
if (($char === ']' && $last_bracket !== '[')
|
||||
|| ($char === '}' && $last_bracket !== '{')
|
||||
|| ($char === ')' && $last_bracket !== '(')
|
||||
|| ($char === '>' && $last_bracket !== '<')
|
||||
) {
|
||||
throw new DocblockParseException('Invalid string ' . $return_block);
|
||||
}
|
||||
} elseif ($char === ' ') {
|
||||
if ($brackets) {
|
||||
$expects_callable_return = false;
|
||||
$type .= ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($next_char === '|' || $next_char === '&') {
|
||||
$nexter_char = $i < $l - 2 ? $return_block[$i + 2] : null;
|
||||
|
||||
if ($nexter_char === ' ') {
|
||||
++$i;
|
||||
$type .= $next_char . ' ';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($last_char === '|' || $last_char === '&') {
|
||||
$type .= ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($next_char === ':') {
|
||||
++$i;
|
||||
$type .= ' :';
|
||||
$expects_callable_return = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($expects_callable_return) {
|
||||
$type .= ' ';
|
||||
$expects_callable_return = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
$remaining = trim(preg_replace('@^[ \t]*\* *@m', ' ', substr($return_block, $i + 1)));
|
||||
|
||||
if ($remaining) {
|
||||
return array_merge([rtrim($type)], preg_split('/[ \s]+/', $remaining) ?: []);
|
||||
}
|
||||
|
||||
return [$type];
|
||||
}
|
||||
|
||||
$expects_callable_return = false;
|
||||
|
||||
$type .= $char;
|
||||
}
|
||||
|
||||
return [$type];
|
||||
}
|
||||
|
||||
/** @return list<VarDocblockComment> */
|
||||
public static function getVarComments(
|
||||
PhpParser\Comment\Doc $doc_comment,
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\Variable $var
|
||||
): array {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
$parsed_docblock = $statements_analyzer->getParsedDocblock();
|
||||
|
||||
if (!$parsed_docblock) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$var_comments = [];
|
||||
|
||||
try {
|
||||
$var_comments = $codebase->config->disable_var_parsing
|
||||
? []
|
||||
: self::arrayToDocblocks(
|
||||
$doc_comment,
|
||||
$parsed_docblock,
|
||||
$statements_analyzer->getSource(),
|
||||
$statements_analyzer->getSource()->getAliases(),
|
||||
$statements_analyzer->getSource()->getTemplateTypeMap(),
|
||||
);
|
||||
} catch (IncorrectDocblockException $e) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new MissingDocblockType(
|
||||
$e->getMessage(),
|
||||
new CodeLocation($statements_analyzer, $var),
|
||||
),
|
||||
);
|
||||
} catch (DocblockParseException $e) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidDocblock(
|
||||
$e->getMessage(),
|
||||
new CodeLocation($statements_analyzer->getSource(), $var),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return $var_comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<VarDocblockComment> $var_comments
|
||||
*/
|
||||
public static function populateVarTypesFromDocblock(
|
||||
array $var_comments,
|
||||
PhpParser\Node\Expr\Variable $var,
|
||||
Context $context,
|
||||
StatementsAnalyzer $statements_analyzer
|
||||
): ?Union {
|
||||
if (!is_string($var->name)) {
|
||||
return null;
|
||||
}
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
$comment_type = null;
|
||||
$var_id = '$' . $var->name;
|
||||
|
||||
foreach ($var_comments as $var_comment) {
|
||||
if (!$var_comment->type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$var_comment_type = TypeExpander::expandUnion(
|
||||
$codebase,
|
||||
$var_comment->type,
|
||||
$context->self,
|
||||
$context->self,
|
||||
$statements_analyzer->getParentFQCLN(),
|
||||
);
|
||||
|
||||
$var_comment_type = $var_comment_type->setFromDocblock();
|
||||
|
||||
/** @psalm-suppress UnusedMethodCall */
|
||||
$var_comment_type->check(
|
||||
$statements_analyzer,
|
||||
new CodeLocation($statements_analyzer->getSource(), $var),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
if ($codebase->alter_code
|
||||
&& $var_comment->type_start
|
||||
&& $var_comment->type_end
|
||||
&& $var_comment->line_number
|
||||
) {
|
||||
$type_location = new DocblockTypeLocation(
|
||||
$statements_analyzer,
|
||||
$var_comment->type_start,
|
||||
$var_comment->type_end,
|
||||
$var_comment->line_number,
|
||||
);
|
||||
|
||||
$codebase->classlikes->handleDocblockTypeInMigration(
|
||||
$codebase,
|
||||
$statements_analyzer,
|
||||
$var_comment_type,
|
||||
$type_location,
|
||||
$context->calling_method_id,
|
||||
);
|
||||
}
|
||||
|
||||
if (!$var_comment->var_id || $var_comment->var_id === $var_id) {
|
||||
$comment_type = $var_comment_type;
|
||||
continue;
|
||||
}
|
||||
|
||||
$context->vars_in_scope[$var_comment->var_id] = $var_comment_type;
|
||||
} catch (UnexpectedValueException $e) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidDocblock(
|
||||
$e->getMessage(),
|
||||
new CodeLocation($statements_analyzer, $var),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $comment_type;
|
||||
}
|
||||
}
|
||||
62
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/DataFlowNodeData.php
vendored
Normal file
62
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/DataFlowNodeData.php
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use Psalm\Storage\ImmutableNonCloneableTrait;
|
||||
|
||||
/**
|
||||
* @psalm-immutable
|
||||
* @internal
|
||||
*/
|
||||
class DataFlowNodeData
|
||||
{
|
||||
use ImmutableNonCloneableTrait;
|
||||
|
||||
public int $line_from;
|
||||
|
||||
public int $line_to;
|
||||
|
||||
public string $label;
|
||||
|
||||
public string $file_name;
|
||||
|
||||
public string $file_path;
|
||||
|
||||
public string $snippet;
|
||||
|
||||
public int $from;
|
||||
|
||||
public int $to;
|
||||
|
||||
public int $snippet_from;
|
||||
|
||||
public int $column_from;
|
||||
|
||||
public int $column_to;
|
||||
|
||||
public function __construct(
|
||||
string $label,
|
||||
int $line_from,
|
||||
int $line_to,
|
||||
string $file_name,
|
||||
string $file_path,
|
||||
string $snippet,
|
||||
int $from,
|
||||
int $to,
|
||||
int $snippet_from,
|
||||
int $column_from,
|
||||
int $column_to
|
||||
) {
|
||||
$this->label = $label;
|
||||
$this->line_from = $line_from;
|
||||
$this->line_to = $line_to;
|
||||
$this->file_name = $file_name;
|
||||
$this->file_path = $file_path;
|
||||
$this->snippet = $snippet;
|
||||
$this->from = $from;
|
||||
$this->to = $to;
|
||||
$this->snippet_from = $snippet_from;
|
||||
$this->column_from = $column_from;
|
||||
$this->column_to = $column_to;
|
||||
}
|
||||
}
|
||||
677
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FileAnalyzer.php
vendored
Normal file
677
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FileAnalyzer.php
vendored
Normal file
@@ -0,0 +1,677 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation\DocblockTypeLocation;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Context;
|
||||
use Psalm\Exception\UnpreparedAnalysisException;
|
||||
use Psalm\Internal\Codebase\Functions;
|
||||
use Psalm\Internal\Codebase\InternalCallMapHandler;
|
||||
use Psalm\Internal\Codebase\Reflection;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Internal\Provider\ClassLikeStorageProvider;
|
||||
use Psalm\Internal\Provider\FileReferenceProvider;
|
||||
use Psalm\Internal\Provider\FileStorageProvider;
|
||||
use Psalm\Internal\Provider\NodeDataProvider;
|
||||
use Psalm\Internal\Type\TypeAlias\LinkableTypeAlias;
|
||||
use Psalm\Internal\Type\TypeTokenizer;
|
||||
use Psalm\Issue\InvalidTypeImport;
|
||||
use Psalm\Issue\UncaughtThrowInGlobalScope;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\NodeTypeProvider;
|
||||
use Psalm\Plugin\EventHandler\Event\AfterFileAnalysisEvent;
|
||||
use Psalm\Plugin\EventHandler\Event\BeforeFileAnalysisEvent;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Union;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_combine;
|
||||
use function array_diff_key;
|
||||
use function array_keys;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @psalm-consistent-constructor
|
||||
*/
|
||||
class FileAnalyzer extends SourceAnalyzer
|
||||
{
|
||||
use CanAlias;
|
||||
|
||||
protected string $file_name;
|
||||
|
||||
protected string $file_path;
|
||||
|
||||
protected ?string $root_file_path = null;
|
||||
|
||||
protected ?string $root_file_name = null;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private array $required_file_paths = [];
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private array $parent_file_paths = [];
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
private array $suppressed_issues = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
private array $namespace_aliased_classes = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<lowercase-string, string>>
|
||||
*/
|
||||
private array $namespace_aliased_classes_flipped = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, string>>
|
||||
*/
|
||||
private array $namespace_aliased_classes_flipped_replaceable = [];
|
||||
|
||||
/**
|
||||
* @var array<lowercase-string, InterfaceAnalyzer>
|
||||
*/
|
||||
public array $interface_analyzers_to_analyze = [];
|
||||
|
||||
/**
|
||||
* @var array<lowercase-string, ClassAnalyzer>
|
||||
*/
|
||||
public array $class_analyzers_to_analyze = [];
|
||||
|
||||
public ?Context $context = null;
|
||||
|
||||
public ProjectAnalyzer $project_analyzer;
|
||||
|
||||
public Codebase $codebase;
|
||||
|
||||
private int $first_statement_offset = -1;
|
||||
|
||||
private ?NodeDataProvider $node_data = null;
|
||||
|
||||
private ?Union $return_type = null;
|
||||
|
||||
public function __construct(ProjectAnalyzer $project_analyzer, string $file_path, string $file_name)
|
||||
{
|
||||
$this->source = $this;
|
||||
$this->file_path = $file_path;
|
||||
$this->file_name = $file_name;
|
||||
$this->project_analyzer = $project_analyzer;
|
||||
$this->codebase = $project_analyzer->getCodebase();
|
||||
}
|
||||
|
||||
public function analyze(
|
||||
?Context $file_context = null,
|
||||
?Context $global_context = null
|
||||
): void {
|
||||
$codebase = $this->project_analyzer->getCodebase();
|
||||
|
||||
$file_storage = $codebase->file_storage_provider->get($this->file_path);
|
||||
|
||||
if (!$file_storage->deep_scan && !$codebase->server_mode) {
|
||||
throw new UnpreparedAnalysisException('File ' . $this->file_path . ' has not been properly scanned');
|
||||
}
|
||||
|
||||
if ($file_storage->has_visitor_issues) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($file_context) {
|
||||
$this->context = $file_context;
|
||||
}
|
||||
|
||||
if (!$this->context) {
|
||||
$this->context = new Context();
|
||||
}
|
||||
|
||||
if ($codebase->config->useStrictTypesForFile($this->file_path)) {
|
||||
$this->context->strict_types = true;
|
||||
}
|
||||
|
||||
$this->context->is_global = true;
|
||||
$this->context->defineGlobals();
|
||||
$this->context->collect_exceptions = $codebase->config->check_for_throws_in_global_scope;
|
||||
|
||||
try {
|
||||
$stmts = $codebase->getStatementsForFile($this->file_path);
|
||||
} catch (PhpParser\Error $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event = new BeforeFileAnalysisEvent($this, $this->context, $file_storage, $codebase);
|
||||
|
||||
$codebase->config->eventDispatcher->dispatchBeforeFileAnalysis($event);
|
||||
|
||||
if ($codebase->alter_code) {
|
||||
foreach ($stmts as $stmt) {
|
||||
if (!$stmt instanceof PhpParser\Node\Stmt\Declare_) {
|
||||
$this->first_statement_offset = (int) $stmt->getAttribute('startFilePos');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$leftover_stmts = $this->populateCheckers($stmts);
|
||||
|
||||
$this->node_data = new NodeDataProvider();
|
||||
$statements_analyzer = new StatementsAnalyzer($this, $this->node_data);
|
||||
|
||||
foreach ($file_storage->docblock_issues as $docblock_issue) {
|
||||
IssueBuffer::maybeAdd($docblock_issue);
|
||||
}
|
||||
|
||||
// if there are any leftover statements, evaluate them,
|
||||
// in turn causing the classes/interfaces be evaluated
|
||||
if ($leftover_stmts) {
|
||||
$statements_analyzer->analyze($leftover_stmts, $this->context, $global_context, true);
|
||||
|
||||
foreach ($leftover_stmts as $leftover_stmt) {
|
||||
if ($leftover_stmt instanceof PhpParser\Node\Stmt\Return_) {
|
||||
if ($leftover_stmt->expr) {
|
||||
$this->return_type =
|
||||
$statements_analyzer->node_data->getType($leftover_stmt->expr) ?? Type::getMixed();
|
||||
} else {
|
||||
$this->return_type = Type::getVoid();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check any leftover interfaces not already evaluated
|
||||
foreach ($this->interface_analyzers_to_analyze as $interface_analyzer) {
|
||||
$interface_analyzer->analyze();
|
||||
}
|
||||
|
||||
// check any leftover classes not already evaluated
|
||||
|
||||
foreach ($this->class_analyzers_to_analyze as $class_analyzer) {
|
||||
$class_analyzer->analyze(null, $this->context);
|
||||
}
|
||||
|
||||
if ($codebase->config->check_for_throws_in_global_scope) {
|
||||
$uncaught_throws = $statements_analyzer->getUncaughtThrows($this->context);
|
||||
foreach ($uncaught_throws as $possibly_thrown_exception => $codelocations) {
|
||||
foreach ($codelocations as $codelocation) {
|
||||
// issues are suppressed in ThrowAnalyzer, CallAnalyzer, etc.
|
||||
IssueBuffer::maybeAdd(
|
||||
new UncaughtThrowInGlobalScope(
|
||||
$possibly_thrown_exception . ' is thrown but not caught in global scope',
|
||||
$codelocation,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validate type imports
|
||||
if ($file_storage->type_aliases) {
|
||||
foreach ($file_storage->type_aliases as $alias) {
|
||||
if ($alias instanceof LinkableTypeAlias) {
|
||||
$location = new DocblockTypeLocation(
|
||||
$this->getSource(),
|
||||
$alias->start_offset,
|
||||
$alias->end_offset,
|
||||
$alias->line_number,
|
||||
);
|
||||
$fq_source_classlike = $alias->declaring_fq_classlike_name;
|
||||
if (ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
|
||||
$this->getSource(),
|
||||
$fq_source_classlike,
|
||||
$location,
|
||||
null,
|
||||
null,
|
||||
$this->suppressed_issues,
|
||||
new ClassLikeNameOptions(
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
),
|
||||
) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$referenced_class_storage = $codebase->classlike_storage_provider->get($fq_source_classlike);
|
||||
if (!isset($referenced_class_storage->type_aliases[$alias->alias_name])) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidTypeImport(
|
||||
'Type alias ' . $alias->alias_name
|
||||
. ' imported from ' . $fq_source_classlike
|
||||
. ' is not defined on the source class',
|
||||
$location,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$event = new AfterFileAnalysisEvent($this, $this->context, $file_storage, $codebase, $stmts);
|
||||
$codebase->config->eventDispatcher->dispatchAfterFileAnalysis($event);
|
||||
|
||||
$this->class_analyzers_to_analyze = [];
|
||||
$this->interface_analyzers_to_analyze = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, PhpParser\Node\Stmt> $stmts
|
||||
* @return list<PhpParser\Node\Stmt>
|
||||
*/
|
||||
public function populateCheckers(array $stmts): array
|
||||
{
|
||||
$leftover_stmts = [];
|
||||
|
||||
foreach ($stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Trait_) {
|
||||
$leftover_stmts[] = $stmt;
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\ClassLike) {
|
||||
$this->populateClassLikeAnalyzers($stmt);
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Namespace_) {
|
||||
$namespace_name = $stmt->name ? implode('\\', $stmt->name->parts) : '';
|
||||
|
||||
$namespace_analyzer = new NamespaceAnalyzer($stmt, $this);
|
||||
$namespace_analyzer->collectAnalyzableInformation();
|
||||
|
||||
$this->namespace_aliased_classes[$namespace_name] = $namespace_analyzer->getAliases()->uses;
|
||||
$this->namespace_aliased_classes_flipped[$namespace_name] =
|
||||
$namespace_analyzer->getAliasedClassesFlipped();
|
||||
$this->namespace_aliased_classes_flipped_replaceable[$namespace_name] =
|
||||
$namespace_analyzer->getAliasedClassesFlippedReplaceable();
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Use_) {
|
||||
$this->visitUse($stmt);
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\GroupUse) {
|
||||
$this->visitGroupUse($stmt);
|
||||
} else {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
|
||||
foreach ($stmt->stmts as $if_stmt) {
|
||||
if ($if_stmt instanceof PhpParser\Node\Stmt\ClassLike) {
|
||||
$this->populateClassLikeAnalyzers($if_stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$leftover_stmts[] = $stmt;
|
||||
}
|
||||
}
|
||||
|
||||
return $leftover_stmts;
|
||||
}
|
||||
|
||||
private function populateClassLikeAnalyzers(PhpParser\Node\Stmt\ClassLike $stmt): void
|
||||
{
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Class_ || $stmt instanceof PhpParser\Node\Stmt\Enum_) {
|
||||
if (!$stmt->name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this can happen when stubbing
|
||||
if (!$this->codebase->classExists($stmt->name->name)
|
||||
&& !$this->codebase->classlikes->enumExists($stmt->name->name)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$class_analyzer = new ClassAnalyzer($stmt, $this, $stmt->name->name);
|
||||
|
||||
$fq_class_name = $class_analyzer->getFQCLN();
|
||||
|
||||
$this->class_analyzers_to_analyze[strtolower($fq_class_name)] = $class_analyzer;
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Interface_) {
|
||||
if (!$stmt->name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this can happen when stubbing
|
||||
if (!$this->codebase->interfaceExists($stmt->name->name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$class_analyzer = new InterfaceAnalyzer($stmt, $this, $stmt->name->name);
|
||||
|
||||
$fq_class_name = $class_analyzer->getFQCLN();
|
||||
|
||||
$this->interface_analyzers_to_analyze[strtolower($fq_class_name)] = $class_analyzer;
|
||||
}
|
||||
}
|
||||
|
||||
public function addNamespacedClassAnalyzer(string $fq_class_name, ClassAnalyzer $class_analyzer): void
|
||||
{
|
||||
$this->class_analyzers_to_analyze[strtolower($fq_class_name)] = $class_analyzer;
|
||||
}
|
||||
|
||||
public function addNamespacedInterfaceAnalyzer(string $fq_class_name, InterfaceAnalyzer $interface_analyzer): void
|
||||
{
|
||||
$this->interface_analyzers_to_analyze[strtolower($fq_class_name)] = $interface_analyzer;
|
||||
}
|
||||
|
||||
public function getMethodMutations(
|
||||
MethodIdentifier $method_id,
|
||||
Context $this_context,
|
||||
bool $from_project_analyzer = false
|
||||
): void {
|
||||
$fq_class_name = $method_id->fq_class_name;
|
||||
$method_name = $method_id->method_name;
|
||||
$fq_class_name_lc = strtolower($fq_class_name);
|
||||
|
||||
if (isset($this->class_analyzers_to_analyze[$fq_class_name_lc])) {
|
||||
$class_analyzer_to_examine = $this->class_analyzers_to_analyze[$fq_class_name_lc];
|
||||
} else {
|
||||
if (!$from_project_analyzer) {
|
||||
$this->project_analyzer->getMethodMutations(
|
||||
$method_id,
|
||||
$this_context,
|
||||
$this->getRootFilePath(),
|
||||
$this->getRootFileName(),
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$call_context = new Context($this_context->self);
|
||||
$call_context->collect_mutations = $this_context->collect_mutations;
|
||||
$call_context->collect_initializations = $this_context->collect_initializations;
|
||||
$call_context->collect_nonprivate_initializations = $this_context->collect_nonprivate_initializations;
|
||||
$call_context->initialized_methods = $this_context->initialized_methods;
|
||||
$call_context->include_location = $this_context->include_location;
|
||||
$call_context->calling_method_id = $this_context->calling_method_id;
|
||||
|
||||
foreach ($this_context->vars_possibly_in_scope as $var => $_) {
|
||||
if (strpos($var, '$this->') === 0) {
|
||||
$call_context->vars_possibly_in_scope[$var] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this_context->vars_in_scope as $var => $type) {
|
||||
if (strpos($var, '$this->') === 0) {
|
||||
$call_context->vars_in_scope[$var] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($this_context->vars_in_scope['$this'])) {
|
||||
throw new UnexpectedValueException('Should exist');
|
||||
}
|
||||
|
||||
$call_context->vars_in_scope['$this'] = $this_context->vars_in_scope['$this'];
|
||||
|
||||
$class_analyzer_to_examine->getMethodMutations($method_name, $call_context);
|
||||
|
||||
foreach ($call_context->vars_possibly_in_scope as $var => $_) {
|
||||
$this_context->vars_possibly_in_scope[$var] = true;
|
||||
}
|
||||
|
||||
foreach ($call_context->vars_in_scope as $var => $type) {
|
||||
$this_context->vars_in_scope[$var] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
public function getFunctionLikeAnalyzer(MethodIdentifier $method_id): ?MethodAnalyzer
|
||||
{
|
||||
$fq_class_name = $method_id->fq_class_name;
|
||||
$method_name = $method_id->method_name;
|
||||
|
||||
$fq_class_name_lc = strtolower($fq_class_name);
|
||||
|
||||
if (!isset($this->class_analyzers_to_analyze[$fq_class_name_lc])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class_analyzer_to_examine = $this->class_analyzers_to_analyze[$fq_class_name_lc];
|
||||
|
||||
return $class_analyzer_to_examine->getFunctionLikeAnalyzer($method_name);
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getNamespace(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return array<lowercase-string, string>
|
||||
*/
|
||||
public function getAliasedClassesFlipped(?string $namespace_name = null): array
|
||||
{
|
||||
if ($namespace_name && isset($this->namespace_aliased_classes_flipped[$namespace_name])) {
|
||||
return $this->namespace_aliased_classes_flipped[$namespace_name];
|
||||
}
|
||||
|
||||
return $this->aliased_classes_flipped;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getAliasedClassesFlippedReplaceable(?string $namespace_name = null): array
|
||||
{
|
||||
if ($namespace_name && isset($this->namespace_aliased_classes_flipped_replaceable[$namespace_name])) {
|
||||
return $this->namespace_aliased_classes_flipped_replaceable[$namespace_name];
|
||||
}
|
||||
|
||||
return $this->aliased_classes_flipped_replaceable;
|
||||
}
|
||||
|
||||
public static function clearCache(): void
|
||||
{
|
||||
TypeTokenizer::clearCache();
|
||||
Reflection::clearCache();
|
||||
Functions::clearCache();
|
||||
IssueBuffer::clearCache();
|
||||
FileManipulationBuffer::clearCache();
|
||||
FunctionLikeAnalyzer::clearCache();
|
||||
ClassLikeStorageProvider::deleteAll();
|
||||
FileStorageProvider::deleteAll();
|
||||
FileReferenceProvider::clearCache();
|
||||
InternalCallMapHandler::clearCache();
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getFileName(): string
|
||||
{
|
||||
return $this->file_name;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getFilePath(): string
|
||||
{
|
||||
return $this->file_path;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getRootFileName(): string
|
||||
{
|
||||
return $this->root_file_name ?: $this->file_name;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getRootFilePath(): string
|
||||
{
|
||||
return $this->root_file_path ?: $this->file_path;
|
||||
}
|
||||
|
||||
public function setRootFilePath(string $file_path, string $file_name): void
|
||||
{
|
||||
$this->root_file_name = $file_name;
|
||||
$this->root_file_path = $file_path;
|
||||
}
|
||||
|
||||
public function addRequiredFilePath(string $file_path): void
|
||||
{
|
||||
$this->required_file_paths[$file_path] = true;
|
||||
}
|
||||
|
||||
public function addParentFilePath(string $file_path): void
|
||||
{
|
||||
$this->parent_file_paths[$file_path] = true;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function hasParentFilePath(string $file_path): bool
|
||||
{
|
||||
return $this->file_path === $file_path || isset($this->parent_file_paths[$file_path]);
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function hasAlreadyRequiredFilePath(string $file_path): bool
|
||||
{
|
||||
return isset($this->required_file_paths[$file_path]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getRequiredFilePaths(): array
|
||||
{
|
||||
return array_keys($this->required_file_paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getParentFilePaths(): array
|
||||
{
|
||||
return array_keys($this->parent_file_paths);
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getRequireNesting(): int
|
||||
{
|
||||
return count($this->parent_file_paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getSuppressedIssues(): array
|
||||
{
|
||||
return $this->suppressed_issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $new_issues
|
||||
*/
|
||||
public function addSuppressedIssues(array $new_issues): void
|
||||
{
|
||||
if (isset($new_issues[0])) {
|
||||
$new_issues = array_combine($new_issues, $new_issues);
|
||||
}
|
||||
|
||||
$this->suppressed_issues = $new_issues + $this->suppressed_issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $new_issues
|
||||
*/
|
||||
public function removeSuppressedIssues(array $new_issues): void
|
||||
{
|
||||
if (isset($new_issues[0])) {
|
||||
$new_issues = array_combine($new_issues, $new_issues);
|
||||
}
|
||||
|
||||
$this->suppressed_issues = array_diff_key($this->suppressed_issues, $new_issues);
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getFQCLN(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getParentFQCLN(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getClassName(): ?string
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return array<string, array<string, Union>>|null
|
||||
*/
|
||||
public function getTemplateTypeMap(): ?array
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function isStatic(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function getFileAnalyzer(): FileAnalyzer
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function getProjectAnalyzer(): ProjectAnalyzer
|
||||
{
|
||||
return $this->project_analyzer;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getCodebase(): Codebase
|
||||
{
|
||||
return $this->codebase;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getFirstStatementOffset(): int
|
||||
{
|
||||
return $this->first_statement_offset;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getNodeTypeProvider(): NodeTypeProvider
|
||||
{
|
||||
if (!$this->node_data) {
|
||||
throw new UnexpectedValueException('There should be a node type provider');
|
||||
}
|
||||
|
||||
return $this->node_data;
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getReturnType(): ?Union
|
||||
{
|
||||
return $this->return_type;
|
||||
}
|
||||
|
||||
public function clearSourceBeforeDestruction(): void
|
||||
{
|
||||
unset($this->source);
|
||||
}
|
||||
}
|
||||
124
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php
vendored
Normal file
124
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionAnalyzer.php
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function is_string;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @extends FunctionLikeAnalyzer<PhpParser\Node\Stmt\Function_>
|
||||
*/
|
||||
class FunctionAnalyzer extends FunctionLikeAnalyzer
|
||||
{
|
||||
public function __construct(PhpParser\Node\Stmt\Function_ $function, SourceAnalyzer $source)
|
||||
{
|
||||
$codebase = $source->getCodebase();
|
||||
|
||||
$file_storage_provider = $codebase->file_storage_provider;
|
||||
|
||||
$file_storage = $file_storage_provider->get($source->getFilePath());
|
||||
|
||||
$namespace = $source->getNamespace();
|
||||
|
||||
$function_id = ($namespace ? strtolower($namespace) . '\\' : '') . strtolower($function->name->name);
|
||||
|
||||
if (!isset($file_storage->functions[$function_id])) {
|
||||
throw new UnexpectedValueException(
|
||||
'Function ' . $function_id . ' should be defined in ' . $source->getFilePath(),
|
||||
);
|
||||
}
|
||||
|
||||
$storage = $file_storage->functions[$function_id];
|
||||
|
||||
parent::__construct($function, $source, $storage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-empty-lowercase-string
|
||||
* @throws UnexpectedValueException if function is closure or arrow function.
|
||||
*/
|
||||
public function getFunctionId(): string
|
||||
{
|
||||
$namespace = $this->source->getNamespace();
|
||||
|
||||
/** @var non-empty-lowercase-string */
|
||||
return ($namespace ? strtolower($namespace) . '\\' : '') . strtolower($this->function->name->name);
|
||||
}
|
||||
|
||||
public static function analyzeStatement(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Stmt\Function_ $stmt,
|
||||
Context $context
|
||||
): void {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif (!$function_stmt instanceof PhpParser\Node\Stmt\Nop) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
if (!$codebase->register_stub_files
|
||||
&& !$codebase->register_autoload_files
|
||||
) {
|
||||
$function_name = strtolower($stmt->name->name);
|
||||
|
||||
if ($ns = $statements_analyzer->getNamespace()) {
|
||||
$fq_function_name = strtolower($ns) . '\\' . $function_name;
|
||||
} else {
|
||||
$fq_function_name = $function_name;
|
||||
}
|
||||
|
||||
$function_context = new Context($context->self);
|
||||
$function_context->strict_types = $context->strict_types;
|
||||
$config = Config::getInstance();
|
||||
$function_context->collect_exceptions = $config->check_for_throws_docblock;
|
||||
|
||||
if ($function_analyzer = $statements_analyzer->getFunctionAnalyzer($fq_function_name)) {
|
||||
$function_analyzer->analyze(
|
||||
$function_context,
|
||||
$statements_analyzer->node_data,
|
||||
$context,
|
||||
);
|
||||
|
||||
if ($config->reportIssueInFile('InvalidReturnType', $statements_analyzer->getFilePath())) {
|
||||
$method_id = $function_analyzer->getId();
|
||||
|
||||
$function_storage = $codebase->functions->getStorage(
|
||||
$statements_analyzer,
|
||||
strtolower($method_id),
|
||||
);
|
||||
|
||||
$return_type = $function_storage->return_type;
|
||||
$return_type_location = $function_storage->return_type_location;
|
||||
|
||||
$function_analyzer->verifyReturnType(
|
||||
$stmt->getStmts(),
|
||||
$statements_analyzer,
|
||||
$return_type,
|
||||
$statements_analyzer->getFQCLN(),
|
||||
$return_type_location,
|
||||
$function_context->has_returned,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1078
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
vendored
Normal file
1078
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
324
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php
vendored
Normal file
324
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php
vendored
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\FunctionLike;
|
||||
|
||||
use PhpParser;
|
||||
use PhpParser\NodeTraverser;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer;
|
||||
use Psalm\Internal\PhpVisitor\YieldTypeCollector;
|
||||
use Psalm\Internal\Provider\NodeDataProvider;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TGenericObject;
|
||||
use Psalm\Type\Atomic\TIterable;
|
||||
use Psalm\Type\Atomic\TKeyedArray;
|
||||
use Psalm\Type\Atomic\TList;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_merge;
|
||||
|
||||
/**
|
||||
* A class for analysing a given method call's effects in relation to $this/self and also looking at return types
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class ReturnTypeCollector
|
||||
{
|
||||
/**
|
||||
* Gets the return types from a list of statements
|
||||
*
|
||||
* @param array<PhpParser\Node> $stmts
|
||||
* @param list<Union> $yield_types
|
||||
* @return list<Union> a list of return types
|
||||
* @psalm-suppress ComplexMethod to be refactored
|
||||
*/
|
||||
public static function getReturnTypes(
|
||||
Codebase $codebase,
|
||||
NodeDataProvider $nodes,
|
||||
array $stmts,
|
||||
array &$yield_types,
|
||||
bool $collapse_types = false
|
||||
): array {
|
||||
$return_types = [];
|
||||
|
||||
foreach ($stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Return_) {
|
||||
if (!$stmt->expr) {
|
||||
$return_types[] = Type::getVoid();
|
||||
} elseif ($stmt_type = $nodes->getType($stmt)) {
|
||||
$return_types[] = $stmt_type;
|
||||
|
||||
$yield_types = array_merge($yield_types, self::getYieldTypeFromExpression($stmt->expr, $nodes));
|
||||
} elseif ($stmt->expr instanceof PhpParser\Node\Scalar\String_) {
|
||||
$return_types[] = Type::getString();
|
||||
} elseif ($stmt->expr instanceof PhpParser\Node\Scalar\LNumber) {
|
||||
$return_types[] = Type::getInt();
|
||||
} elseif ($stmt->expr instanceof PhpParser\Node\Expr\ConstFetch) {
|
||||
if ((string)$stmt->expr->name === 'true') {
|
||||
$return_types[] = Type::getTrue();
|
||||
} elseif ((string)$stmt->expr->name === 'false') {
|
||||
$return_types[] = Type::getFalse();
|
||||
} elseif ((string)$stmt->expr->name === 'null') {
|
||||
$return_types[] = Type::getNull();
|
||||
}
|
||||
} else {
|
||||
$return_types[] = Type::getMixed();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Break_
|
||||
|| $stmt instanceof PhpParser\Node\Stmt\Continue_
|
||||
) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Throw_) {
|
||||
$return_types[] = Type::getNever();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Expression) {
|
||||
if ($stmt->expr instanceof PhpParser\Node\Expr\Exit_) {
|
||||
$return_types[] = Type::getNever();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($stmt->expr instanceof PhpParser\Node\Expr\FuncCall
|
||||
|| $stmt->expr instanceof PhpParser\Node\Expr\MethodCall
|
||||
|| $stmt->expr instanceof PhpParser\Node\Expr\NullsafeMethodCall
|
||||
|| $stmt->expr instanceof PhpParser\Node\Expr\StaticCall) {
|
||||
$stmt_type = $nodes->getType($stmt->expr);
|
||||
if ($stmt_type && ($stmt_type->isNever() || $stmt_type->explicit_never)) {
|
||||
$return_types[] = Type::getNever();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt->expr instanceof PhpParser\Node\Expr\Assign) {
|
||||
$return_types = [
|
||||
...$return_types,
|
||||
...self::getReturnTypes(
|
||||
$codebase,
|
||||
$nodes,
|
||||
[$stmt->expr->expr],
|
||||
$yield_types,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
$yield_types = array_merge($yield_types, self::getYieldTypeFromExpression($stmt->expr, $nodes));
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\If_) {
|
||||
$return_types = [
|
||||
...$return_types,
|
||||
...self::getReturnTypes(
|
||||
$codebase,
|
||||
$nodes,
|
||||
$stmt->stmts,
|
||||
$yield_types,
|
||||
),
|
||||
];
|
||||
|
||||
foreach ($stmt->elseifs as $elseif) {
|
||||
$return_types = [
|
||||
...$return_types,
|
||||
...self::getReturnTypes(
|
||||
$codebase,
|
||||
$nodes,
|
||||
$elseif->stmts,
|
||||
$yield_types,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if ($stmt->else) {
|
||||
$return_types = [
|
||||
...$return_types,
|
||||
...self::getReturnTypes(
|
||||
$codebase,
|
||||
$nodes,
|
||||
$stmt->else->stmts,
|
||||
$yield_types,
|
||||
),
|
||||
];
|
||||
}
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
|
||||
$return_types = [
|
||||
...$return_types,
|
||||
...self::getReturnTypes(
|
||||
$codebase,
|
||||
$nodes,
|
||||
$stmt->stmts,
|
||||
$yield_types,
|
||||
),
|
||||
];
|
||||
|
||||
foreach ($stmt->catches as $catch) {
|
||||
$return_types = [
|
||||
...$return_types,
|
||||
...self::getReturnTypes(
|
||||
$codebase,
|
||||
$nodes,
|
||||
$catch->stmts,
|
||||
$yield_types,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if ($stmt->finally) {
|
||||
$return_types = [
|
||||
...$return_types,
|
||||
...self::getReturnTypes(
|
||||
$codebase,
|
||||
$nodes,
|
||||
$stmt->finally->stmts,
|
||||
$yield_types,
|
||||
),
|
||||
];
|
||||
}
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\For_) {
|
||||
$return_types = [
|
||||
...$return_types,
|
||||
...self::getReturnTypes(
|
||||
$codebase,
|
||||
$nodes,
|
||||
$stmt->stmts,
|
||||
$yield_types,
|
||||
),
|
||||
];
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Foreach_) {
|
||||
$return_types = [
|
||||
...$return_types,
|
||||
...self::getReturnTypes(
|
||||
$codebase,
|
||||
$nodes,
|
||||
$stmt->stmts,
|
||||
$yield_types,
|
||||
),
|
||||
];
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\While_) {
|
||||
$yield_types = array_merge($yield_types, self::getYieldTypeFromExpression($stmt->cond, $nodes));
|
||||
$return_types = [
|
||||
...$return_types,
|
||||
...self::getReturnTypes(
|
||||
$codebase,
|
||||
$nodes,
|
||||
$stmt->stmts,
|
||||
$yield_types,
|
||||
),
|
||||
];
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Do_) {
|
||||
$return_types = [
|
||||
...$return_types,
|
||||
...self::getReturnTypes(
|
||||
$codebase,
|
||||
$nodes,
|
||||
$stmt->stmts,
|
||||
$yield_types,
|
||||
),
|
||||
];
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
|
||||
foreach ($stmt->cases as $case) {
|
||||
$return_types = [
|
||||
...$return_types,
|
||||
...self::getReturnTypes(
|
||||
$codebase,
|
||||
$nodes,
|
||||
$case->stmts,
|
||||
$yield_types,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we're at the top level and we're not ending in a return, make sure to add possible null
|
||||
if ($collapse_types) {
|
||||
// if it's a generator, boil everything down to a single generator return type
|
||||
if ($yield_types) {
|
||||
$yield_types = self::processYieldTypes($codebase, $return_types, $yield_types);
|
||||
}
|
||||
}
|
||||
|
||||
return $return_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Union> $return_types
|
||||
* @param non-empty-list<Union> $yield_types
|
||||
* @return non-empty-list<Union>
|
||||
*/
|
||||
private static function processYieldTypes(
|
||||
Codebase $codebase,
|
||||
array $return_types,
|
||||
array $yield_types
|
||||
): array {
|
||||
$key_type = null;
|
||||
$value_type = null;
|
||||
|
||||
$yield_type = Type::combineUnionTypeArray($yield_types, null);
|
||||
|
||||
foreach ($yield_type->getAtomicTypes() as $type) {
|
||||
if ($type instanceof TList) {
|
||||
$type = $type->getKeyedArray();
|
||||
}
|
||||
|
||||
if ($type instanceof TKeyedArray) {
|
||||
$type = $type->getGenericArrayType();
|
||||
}
|
||||
|
||||
if ($type instanceof TArray) {
|
||||
[$key_type_param, $value_type_param] = $type->type_params;
|
||||
|
||||
$key_type = Type::combineUnionTypes($key_type_param, $key_type);
|
||||
$value_type = Type::combineUnionTypes($value_type_param, $value_type);
|
||||
} elseif ($type instanceof TIterable
|
||||
|| $type instanceof TNamedObject
|
||||
) {
|
||||
ForeachAnalyzer::getKeyValueParamsForTraversableObject(
|
||||
$type,
|
||||
$codebase,
|
||||
$key_type,
|
||||
$value_type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
new Union([
|
||||
new TGenericObject(
|
||||
'Generator',
|
||||
[
|
||||
$key_type ?? Type::getMixed(),
|
||||
$value_type ?? Type::getMixed(),
|
||||
Type::getMixed(),
|
||||
$return_types ? Type::combineUnionTypeArray($return_types, null) : Type::getVoid(),
|
||||
],
|
||||
),
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<Union>
|
||||
*/
|
||||
private static function getYieldTypeFromExpression(
|
||||
PhpParser\Node\Expr $stmt,
|
||||
NodeDataProvider $nodes
|
||||
): array {
|
||||
$collector = new YieldTypeCollector($nodes);
|
||||
$traverser = new NodeTraverser();
|
||||
$traverser->addVisitor(
|
||||
$collector,
|
||||
);
|
||||
$traverser->traverse([$stmt]);
|
||||
|
||||
return $collector->getYieldTypes();
|
||||
}
|
||||
}
|
||||
2079
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
vendored
Normal file
2079
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
198
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php
vendored
Normal file
198
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php
vendored
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\FileManipulation;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\ClassConstAnalyzer;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\Internal\Provider\NodeDataProvider;
|
||||
use Psalm\Issue\ParseError;
|
||||
use Psalm\Issue\UndefinedInterface;
|
||||
use Psalm\IssueBuffer;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class InterfaceAnalyzer extends ClassLikeAnalyzer
|
||||
{
|
||||
public function __construct(
|
||||
PhpParser\Node\Stmt\Interface_ $interface,
|
||||
SourceAnalyzer $source,
|
||||
string $fq_interface_name
|
||||
) {
|
||||
parent::__construct($interface, $source, $fq_interface_name);
|
||||
}
|
||||
|
||||
public function analyze(): void
|
||||
{
|
||||
if (!$this->class instanceof PhpParser\Node\Stmt\Interface_) {
|
||||
throw new LogicException('Something went badly wrong');
|
||||
}
|
||||
|
||||
$project_analyzer = $this->file_analyzer->project_analyzer;
|
||||
$codebase = $project_analyzer->getCodebase();
|
||||
$config = $project_analyzer->getConfig();
|
||||
|
||||
$fq_interface_name = $this->getFQCLN();
|
||||
|
||||
if (!$fq_interface_name) {
|
||||
throw new UnexpectedValueException('bad');
|
||||
}
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($fq_interface_name);
|
||||
|
||||
if ($this->class->extends) {
|
||||
foreach ($this->class->extends as $extended_interface) {
|
||||
$extended_interface_name = self::getFQCLNFromNameObject(
|
||||
$extended_interface,
|
||||
$this->getAliases(),
|
||||
);
|
||||
|
||||
$parent_reference_location = new CodeLocation($this, $extended_interface);
|
||||
|
||||
if (!$codebase->classOrInterfaceExists(
|
||||
$extended_interface_name,
|
||||
$parent_reference_location,
|
||||
)) {
|
||||
// we should not normally get here
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$extended_interface_storage = $codebase->classlike_storage_provider->get($extended_interface_name);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$code_location = new CodeLocation(
|
||||
$this,
|
||||
$extended_interface,
|
||||
);
|
||||
|
||||
if (!$extended_interface_storage->is_interface) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new UndefinedInterface(
|
||||
$extended_interface_name . ' is not an interface',
|
||||
$code_location,
|
||||
$extended_interface_name,
|
||||
),
|
||||
$this->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($codebase->store_node_types && $extended_interface_name) {
|
||||
$bounds = $parent_reference_location->getSelectionBounds();
|
||||
|
||||
$codebase->analyzer->addOffsetReference(
|
||||
$this->getFilePath(),
|
||||
$bounds[0],
|
||||
$bounds[1],
|
||||
$extended_interface_name,
|
||||
);
|
||||
}
|
||||
|
||||
$this->checkTemplateParams(
|
||||
$codebase,
|
||||
$class_storage,
|
||||
$extended_interface_storage,
|
||||
$code_location,
|
||||
$class_storage->template_type_extends_count[$extended_interface_name] ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$fq_interface_name = $this->getFQCLN();
|
||||
|
||||
if (!$fq_interface_name) {
|
||||
throw new UnexpectedValueException('bad');
|
||||
}
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($fq_interface_name);
|
||||
$interface_context = new Context($this->getFQCLN());
|
||||
|
||||
AttributesAnalyzer::analyze(
|
||||
$this,
|
||||
$interface_context,
|
||||
$class_storage,
|
||||
$this->class->attrGroups,
|
||||
AttributesAnalyzer::TARGET_CLASS,
|
||||
$class_storage->suppressed_issues + $this->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
$member_stmts = [];
|
||||
foreach ($this->class->stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) {
|
||||
$method_analyzer = new MethodAnalyzer($stmt, $this);
|
||||
|
||||
$type_provider = new NodeDataProvider();
|
||||
|
||||
$method_analyzer->analyze($interface_context, $type_provider);
|
||||
|
||||
$actual_method_id = $method_analyzer->getMethodId();
|
||||
|
||||
if ($stmt->name->name !== '__construct'
|
||||
&& $stmt->name->name !== '__destruct'
|
||||
&& $config->reportIssueInFile('InvalidReturnType', $this->getFilePath())
|
||||
) {
|
||||
ClassAnalyzer::analyzeClassMethodReturnType(
|
||||
$stmt,
|
||||
$method_analyzer,
|
||||
$this,
|
||||
$type_provider,
|
||||
$codebase,
|
||||
$class_storage,
|
||||
$fq_interface_name,
|
||||
$actual_method_id,
|
||||
$actual_method_id,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Property) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new ParseError(
|
||||
'Interfaces cannot have properties',
|
||||
new CodeLocation($this, $stmt),
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\ClassConst) {
|
||||
$member_stmts[] = $stmt;
|
||||
|
||||
foreach ($stmt->consts as $const) {
|
||||
$const_id = strtolower($this->fq_class_name) . '::' . $const->name;
|
||||
|
||||
foreach ($codebase->class_constants_to_rename as $original_const_id => $new_const_name) {
|
||||
if ($const_id === $original_const_id) {
|
||||
$file_manipulations = [
|
||||
new FileManipulation(
|
||||
(int) $const->name->getAttribute('startFilePos'),
|
||||
(int) $const->name->getAttribute('endFilePos') + 1,
|
||||
$new_const_name,
|
||||
),
|
||||
];
|
||||
|
||||
FileManipulationBuffer::add(
|
||||
$this->getFilePath(),
|
||||
$file_manipulations,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$statements_analyzer = new StatementsAnalyzer($this, new NodeDataProvider());
|
||||
$statements_analyzer->analyze($member_stmts, $interface_context, null, true);
|
||||
|
||||
ClassConstAnalyzer::analyze($this->storage, $this->getCodebase());
|
||||
}
|
||||
}
|
||||
143
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/IssueData.php
vendored
Normal file
143
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/IssueData.php
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use function str_pad;
|
||||
|
||||
use const STR_PAD_LEFT;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class IssueData
|
||||
{
|
||||
public string $severity;
|
||||
|
||||
public int $line_from;
|
||||
|
||||
public int $line_to;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
public string $type;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
public string $message;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
public string $file_name;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
public string $file_path;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
public string $snippet;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
public string $selected_text;
|
||||
|
||||
public int $from;
|
||||
|
||||
public int $to;
|
||||
|
||||
public int $snippet_from;
|
||||
|
||||
public int $snippet_to;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
public int $column_from;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
public int $column_to;
|
||||
|
||||
public int $error_level;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
public int $shortcode;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
public string $link;
|
||||
|
||||
/**
|
||||
* @var ?list<DataFlowNodeData|array{label: string, entry_path_type: string}>
|
||||
*/
|
||||
public ?array $taint_trace = null;
|
||||
|
||||
/**
|
||||
* @var ?list<DataFlowNodeData>
|
||||
*/
|
||||
public ?array $other_references = null;
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
*/
|
||||
public ?string $dupe_key = null;
|
||||
|
||||
/**
|
||||
* @param ?list<DataFlowNodeData|array{label: string, entry_path_type: string}> $taint_trace
|
||||
* @param ?list<DataFlowNodeData> $other_references
|
||||
*/
|
||||
public function __construct(
|
||||
string $severity,
|
||||
int $line_from,
|
||||
int $line_to,
|
||||
string $type,
|
||||
string $message,
|
||||
string $file_name,
|
||||
string $file_path,
|
||||
string $snippet,
|
||||
string $selected_text,
|
||||
int $from,
|
||||
int $to,
|
||||
int $snippet_from,
|
||||
int $snippet_to,
|
||||
int $column_from,
|
||||
int $column_to,
|
||||
int $shortcode = 0,
|
||||
int $error_level = -1,
|
||||
?array $taint_trace = null,
|
||||
array $other_references = null,
|
||||
?string $dupe_key = null
|
||||
) {
|
||||
$this->severity = $severity;
|
||||
$this->line_from = $line_from;
|
||||
$this->line_to = $line_to;
|
||||
$this->type = $type;
|
||||
$this->message = $message;
|
||||
$this->file_name = $file_name;
|
||||
$this->file_path = $file_path;
|
||||
$this->snippet = $snippet;
|
||||
$this->selected_text = $selected_text;
|
||||
$this->from = $from;
|
||||
$this->to = $to;
|
||||
$this->snippet_from = $snippet_from;
|
||||
$this->snippet_to = $snippet_to;
|
||||
$this->column_from = $column_from;
|
||||
$this->column_to = $column_to;
|
||||
$this->shortcode = $shortcode;
|
||||
$this->error_level = $error_level;
|
||||
$this->link = $shortcode ? 'https://psalm.dev/' . str_pad((string) $shortcode, 3, "0", STR_PAD_LEFT) : '';
|
||||
$this->taint_trace = $taint_trace;
|
||||
$this->other_references = $other_references;
|
||||
$this->dupe_key = $dupe_key;
|
||||
}
|
||||
}
|
||||
345
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/MethodAnalyzer.php
vendored
Normal file
345
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/MethodAnalyzer.php
vendored
Normal file
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use LogicException;
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Codebase\InternalCallMapHandler;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Issue\InvalidEnumMethod;
|
||||
use Psalm\Issue\InvalidStaticInvocation;
|
||||
use Psalm\Issue\MethodSignatureMustOmitReturnType;
|
||||
use Psalm\Issue\NonStaticSelfCall;
|
||||
use Psalm\Issue\UndefinedMethod;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\StatementsSource;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use Psalm\Storage\MethodStorage;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function in_array;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @extends FunctionLikeAnalyzer<PhpParser\Node\Stmt\ClassMethod>
|
||||
*/
|
||||
class MethodAnalyzer extends FunctionLikeAnalyzer
|
||||
{
|
||||
// https://github.com/php/php-src/blob/a83923044c48982c80804ae1b45e761c271966d3/Zend/zend_enum.c#L77-L95
|
||||
private const FORBIDDEN_ENUM_METHODS = [
|
||||
'__construct',
|
||||
'__destruct',
|
||||
'__clone',
|
||||
'__get',
|
||||
'__set',
|
||||
'__unset',
|
||||
'__isset',
|
||||
'__tostring',
|
||||
'__debuginfo',
|
||||
'__serialize',
|
||||
'__unserialize',
|
||||
'__sleep',
|
||||
'__wakeup',
|
||||
'__set_state',
|
||||
];
|
||||
|
||||
/** @psalm-external-mutation-free */
|
||||
public function __construct(
|
||||
PhpParser\Node\Stmt\ClassMethod $function,
|
||||
SourceAnalyzer $source,
|
||||
?MethodStorage $storage = null
|
||||
) {
|
||||
$codebase = $source->getCodebase();
|
||||
|
||||
$method_name_lc = strtolower((string) $function->name);
|
||||
|
||||
$source_fqcln = (string) $source->getFQCLN();
|
||||
|
||||
$source_fqcln_lc = strtolower($source_fqcln);
|
||||
|
||||
$method_id = new MethodIdentifier($source_fqcln, $method_name_lc);
|
||||
|
||||
if (!$storage) {
|
||||
try {
|
||||
$storage = $codebase->methods->getStorage($method_id);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
$class_storage = $codebase->classlike_storage_provider->get($source_fqcln_lc);
|
||||
|
||||
if (!$class_storage->parent_classes) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
|
||||
|
||||
if (!$declaring_method_id) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// happens for fake constructors
|
||||
$storage = $codebase->methods->getStorage($declaring_method_id);
|
||||
}
|
||||
}
|
||||
|
||||
parent::__construct($function, $source, $storage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a given method is static or not
|
||||
*
|
||||
* @param array<string> $suppressed_issues
|
||||
*/
|
||||
public static function checkStatic(
|
||||
MethodIdentifier $method_id,
|
||||
bool $self_call,
|
||||
bool $is_context_dynamic,
|
||||
Codebase $codebase,
|
||||
CodeLocation $code_location,
|
||||
array $suppressed_issues,
|
||||
?bool &$is_dynamic_this_method = false
|
||||
): void {
|
||||
$codebase_methods = $codebase->methods;
|
||||
|
||||
if ($method_id->fq_class_name === 'Closure'
|
||||
&& $method_id->method_name === 'fromcallable'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$original_method_id = $method_id;
|
||||
|
||||
$method_id = $codebase_methods->getDeclaringMethodId($method_id);
|
||||
|
||||
if (!$method_id) {
|
||||
if (InternalCallMapHandler::inCallMap((string) $original_method_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new LogicException('Declaring method for ' . $original_method_id . ' should not be null');
|
||||
}
|
||||
|
||||
$storage = $codebase_methods->getStorage($method_id);
|
||||
|
||||
if (!$storage->is_static) {
|
||||
if ($self_call) {
|
||||
if (!$is_context_dynamic) {
|
||||
if (IssueBuffer::accepts(
|
||||
new NonStaticSelfCall(
|
||||
'Method ' . $codebase_methods->getCasedMethodId($method_id) .
|
||||
' is not static, but is called ' .
|
||||
'using self::',
|
||||
$code_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$is_dynamic_this_method = true;
|
||||
}
|
||||
} else {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidStaticInvocation(
|
||||
'Method ' . $codebase_methods->getCasedMethodId($method_id) .
|
||||
' is not static, but is called ' .
|
||||
'statically',
|
||||
$code_location,
|
||||
),
|
||||
$suppressed_issues,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $suppressed_issues
|
||||
* @param lowercase-string|null $calling_method_id
|
||||
*/
|
||||
public static function checkMethodExists(
|
||||
Codebase $codebase,
|
||||
MethodIdentifier $method_id,
|
||||
CodeLocation $code_location,
|
||||
array $suppressed_issues,
|
||||
?string $calling_method_id = null
|
||||
): ?bool {
|
||||
if ($codebase->methods->methodExists(
|
||||
$method_id,
|
||||
$calling_method_id,
|
||||
!$calling_method_id
|
||||
|| $calling_method_id !== strtolower((string) $method_id)
|
||||
? $code_location
|
||||
: null,
|
||||
null,
|
||||
$code_location->file_path,
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
new UndefinedMethod('Method ' . $method_id . ' does not exist', $code_location, (string) $method_id),
|
||||
$suppressed_issues,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function isMethodVisible(
|
||||
MethodIdentifier $method_id,
|
||||
Context $context,
|
||||
StatementsSource $source
|
||||
): bool {
|
||||
$codebase = $source->getCodebase();
|
||||
|
||||
$fq_classlike_name = $method_id->fq_class_name;
|
||||
$method_name = $method_id->method_name;
|
||||
|
||||
if ($codebase->methods->visibility_provider->has($fq_classlike_name)) {
|
||||
$method_visible = $codebase->methods->visibility_provider->isMethodVisible(
|
||||
$source,
|
||||
$fq_classlike_name,
|
||||
$method_name,
|
||||
$context,
|
||||
null,
|
||||
);
|
||||
|
||||
if ($method_visible !== null) {
|
||||
return $method_visible;
|
||||
}
|
||||
}
|
||||
|
||||
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
|
||||
|
||||
if (!$declaring_method_id) {
|
||||
// this can happen for methods in the callmap that were not reflected
|
||||
return true;
|
||||
}
|
||||
|
||||
$appearing_method_id = $codebase->methods->getAppearingMethodId($method_id);
|
||||
|
||||
$appearing_method_class = null;
|
||||
|
||||
if ($appearing_method_id) {
|
||||
$appearing_method_class = $appearing_method_id->fq_class_name;
|
||||
|
||||
// if the calling class is the same, we know the method exists, so it must be visible
|
||||
if ($appearing_method_class === $context->self) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$declaring_method_class = $declaring_method_id->fq_class_name;
|
||||
|
||||
if ($source->getSource() instanceof TraitAnalyzer
|
||||
&& strtolower($declaring_method_class) === strtolower((string) $source->getFQCLN())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$storage = $codebase->methods->getStorage($declaring_method_id);
|
||||
|
||||
switch ($storage->visibility) {
|
||||
case ClassLikeAnalyzer::VISIBILITY_PUBLIC:
|
||||
return true;
|
||||
|
||||
case ClassLikeAnalyzer::VISIBILITY_PRIVATE:
|
||||
return $context->self && $appearing_method_class === $context->self;
|
||||
|
||||
case ClassLikeAnalyzer::VISIBILITY_PROTECTED:
|
||||
if (!$context->self) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($appearing_method_class
|
||||
&& $codebase->classExtends($appearing_method_class, $context->self)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($appearing_method_class
|
||||
&& !$codebase->classExtends($context->self, $appearing_method_class)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that __clone, __construct, and __destruct do not have a return type
|
||||
* hint in their signature.
|
||||
*/
|
||||
public static function checkMethodSignatureMustOmitReturnType(
|
||||
MethodStorage $method_storage,
|
||||
CodeLocation $code_location
|
||||
): void {
|
||||
if ($method_storage->signature_return_type === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($method_storage->cased_name === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$method_name_lc = strtolower($method_storage->cased_name);
|
||||
$methodsOfInterest = ['__clone', '__construct', '__destruct'];
|
||||
|
||||
if (in_array($method_name_lc, $methodsOfInterest, true)) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new MethodSignatureMustOmitReturnType(
|
||||
'Method ' . $method_storage->cased_name . ' must not declare a return type',
|
||||
$code_location,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getMethodId(?string $context_self = null): MethodIdentifier
|
||||
{
|
||||
$function_name = (string)$this->function->name;
|
||||
|
||||
return new MethodIdentifier(
|
||||
$context_self ?: (string) $this->source->getFQCLN(),
|
||||
strtolower($function_name),
|
||||
);
|
||||
}
|
||||
|
||||
public static function checkForbiddenEnumMethod(MethodStorage $method_storage, ClassLikeStorage $enum_storage): void
|
||||
{
|
||||
if ($method_storage->cased_name === null || $method_storage->location === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$method_name_lc = strtolower($method_storage->cased_name);
|
||||
if (in_array($method_name_lc, self::FORBIDDEN_ENUM_METHODS, true)) {
|
||||
IssueBuffer::maybeAdd(new InvalidEnumMethod(
|
||||
'Enums cannot define ' . $method_storage->cased_name,
|
||||
$method_storage->location,
|
||||
$method_storage->defining_fqcln . '::' . $method_storage->cased_name,
|
||||
));
|
||||
}
|
||||
|
||||
if ($method_name_lc === 'cases') {
|
||||
IssueBuffer::maybeAdd(new InvalidEnumMethod(
|
||||
'Enums cannot define ' . $method_storage->cased_name,
|
||||
$method_storage->location,
|
||||
$method_storage->defining_fqcln . '::' . $method_storage->cased_name,
|
||||
));
|
||||
}
|
||||
|
||||
if ($enum_storage->enum_type && ($method_name_lc === 'from' || $method_name_lc === 'tryfrom')) {
|
||||
IssueBuffer::maybeAdd(new InvalidEnumMethod(
|
||||
'Enums cannot define ' . $method_storage->cased_name,
|
||||
$method_storage->location,
|
||||
$method_storage->defining_fqcln . '::' . $method_storage->cased_name,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
1099
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/MethodComparator.php
vendored
Normal file
1099
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/MethodComparator.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
268
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php
vendored
Normal file
268
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use PhpParser;
|
||||
use PhpParser\Node\Stmt\Namespace_;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Provider\NodeDataProvider;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Union;
|
||||
use ReflectionProperty;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function assert;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function preg_replace;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class NamespaceAnalyzer extends SourceAnalyzer
|
||||
{
|
||||
use CanAlias;
|
||||
|
||||
/**
|
||||
* @var FileAnalyzer
|
||||
* @psalm-suppress NonInvariantDocblockPropertyType
|
||||
*/
|
||||
protected SourceAnalyzer $source;
|
||||
|
||||
private Namespace_ $namespace;
|
||||
|
||||
private string $namespace_name;
|
||||
|
||||
/**
|
||||
* A lookup table for public namespace constants
|
||||
*
|
||||
* @var array<string, array<string, Union>>
|
||||
*/
|
||||
protected static array $public_namespace_constants = [];
|
||||
|
||||
public function __construct(Namespace_ $namespace, FileAnalyzer $source)
|
||||
{
|
||||
$this->source = $source;
|
||||
$this->namespace = $namespace;
|
||||
$this->namespace_name = $this->namespace->name ? implode('\\', $this->namespace->name->parts) : '';
|
||||
}
|
||||
|
||||
public function collectAnalyzableInformation(): void
|
||||
{
|
||||
$leftover_stmts = [];
|
||||
|
||||
if (!isset(self::$public_namespace_constants[$this->namespace_name])) {
|
||||
self::$public_namespace_constants[$this->namespace_name] = [];
|
||||
}
|
||||
|
||||
$codebase = $this->getCodebase();
|
||||
|
||||
foreach ($this->namespace->stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\ClassLike) {
|
||||
$this->collectAnalyzableClassLike($stmt);
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Use_) {
|
||||
$this->visitUse($stmt);
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\GroupUse) {
|
||||
$this->visitGroupUse($stmt);
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Const_) {
|
||||
foreach ($stmt->consts as $const) {
|
||||
self::$public_namespace_constants[$this->namespace_name][$const->name->name] = Type::getMixed();
|
||||
}
|
||||
|
||||
$leftover_stmts[] = $stmt;
|
||||
} else {
|
||||
$leftover_stmts[] = $stmt;
|
||||
}
|
||||
}
|
||||
|
||||
if ($leftover_stmts) {
|
||||
$statements_analyzer = new StatementsAnalyzer($this, new NodeDataProvider());
|
||||
$file_context = $this->source->context;
|
||||
|
||||
if ($file_context !== null) {
|
||||
$context = $file_context;
|
||||
} else {
|
||||
$context = new Context();
|
||||
$context->is_global = true;
|
||||
$context->defineGlobals();
|
||||
$context->collect_exceptions = $codebase->config->check_for_throws_in_global_scope;
|
||||
}
|
||||
$statements_analyzer->analyze($leftover_stmts, $context, null, true);
|
||||
}
|
||||
}
|
||||
|
||||
public function collectAnalyzableClassLike(PhpParser\Node\Stmt\ClassLike $stmt): void
|
||||
{
|
||||
if (!$stmt->name) {
|
||||
throw new UnexpectedValueException('Did not expect anonymous class here');
|
||||
}
|
||||
|
||||
$fq_class_name = Type::getFQCLNFromString($stmt->name->name, $this->getAliases());
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Class_ || $stmt instanceof PhpParser\Node\Stmt\Enum_) {
|
||||
$this->source->addNamespacedClassAnalyzer(
|
||||
$fq_class_name,
|
||||
new ClassAnalyzer($stmt, $this, $fq_class_name),
|
||||
);
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Interface_) {
|
||||
$this->source->addNamespacedInterfaceAnalyzer(
|
||||
$fq_class_name,
|
||||
new InterfaceAnalyzer($stmt, $this, $fq_class_name),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getNamespace(): string
|
||||
{
|
||||
return $this->namespace_name;
|
||||
}
|
||||
|
||||
public function setConstType(string $const_name, Union $const_type): void
|
||||
{
|
||||
self::$public_namespace_constants[$this->namespace_name][$const_name] = $const_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Union>
|
||||
*/
|
||||
public static function getConstantsForNamespace(string $namespace_name, int $visibility): array
|
||||
{
|
||||
// @todo this does not allow for loading in namespace constants not already defined in the current sweep
|
||||
if (!isset(self::$public_namespace_constants[$namespace_name])) {
|
||||
self::$public_namespace_constants[$namespace_name] = [];
|
||||
}
|
||||
|
||||
if ($visibility === ReflectionProperty::IS_PUBLIC) {
|
||||
return self::$public_namespace_constants[$namespace_name];
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Given $visibility not supported');
|
||||
}
|
||||
|
||||
public function getFileAnalyzer(): FileAnalyzer
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if $calling_identifier is the same as, or is within with $identifier, in a
|
||||
* case-insensitive comparison. Identifiers can be namespaces, classlikes, functions, or methods.
|
||||
*
|
||||
* @psalm-pure
|
||||
* @throws InvalidArgumentException if $identifier is not a valid identifier
|
||||
*/
|
||||
public static function isWithin(string $calling_identifier, string $identifier): bool
|
||||
{
|
||||
$normalized_calling_ident = self::normalizeIdentifier($calling_identifier);
|
||||
$normalized_ident = self::normalizeIdentifier($identifier);
|
||||
|
||||
if ($normalized_calling_ident === $normalized_ident) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$normalized_calling_ident_parts = self::getIdentifierParts($normalized_calling_ident);
|
||||
$normalized_ident_parts = self::getIdentifierParts($normalized_ident);
|
||||
|
||||
if (count($normalized_calling_ident_parts) < count($normalized_ident_parts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < count($normalized_ident_parts); ++$i) {
|
||||
if ($normalized_ident_parts[$i] !== $normalized_calling_ident_parts[$i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if $calling_identifier is the same as or is within any identifier
|
||||
* in $identifiers in a case-insensitive comparison, or if $identifiers is empty.
|
||||
* Identifiers can be namespaces, classlikes, functions, or methods.
|
||||
*
|
||||
* @psalm-pure
|
||||
* @psalm-assert-if-false !empty $identifiers
|
||||
* @param list<string> $identifiers
|
||||
*/
|
||||
public static function isWithinAny(string $calling_identifier, array $identifiers): bool
|
||||
{
|
||||
if (count($identifiers) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($identifiers as $identifier) {
|
||||
if (self::isWithin($calling_identifier, $identifier)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param non-empty-string $fullyQualifiedClassName e.g. '\Psalm\Internal\Analyzer\NamespaceAnalyzer'
|
||||
* @return non-empty-string , e.g. 'Psalm'
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function getNameSpaceRoot(string $fullyQualifiedClassName): string
|
||||
{
|
||||
$root_namespace = preg_replace('/^([^\\\]+).*/', '$1', $fullyQualifiedClassName, 1);
|
||||
if ($root_namespace === "") {
|
||||
throw new InvalidArgumentException("Invalid classname \"$fullyQualifiedClassName\"");
|
||||
}
|
||||
return $root_namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ($lowercase is true ? lowercase-string : string)
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function normalizeIdentifier(string $identifier, bool $lowercase = true): string
|
||||
{
|
||||
if ($identifier === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
$identifier = $identifier[0] === "\\" ? substr($identifier, 1) : $identifier;
|
||||
return $lowercase ? strtolower($identifier) : $identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits an identifier into parts, eg `Foo\Bar::baz` becomes ["Foo", "\\", "Bar", "::", "baz"].
|
||||
*
|
||||
* @return list<non-empty-string>
|
||||
* @psalm-pure
|
||||
*/
|
||||
public static function getIdentifierParts(string $identifier): array
|
||||
{
|
||||
$parts = [];
|
||||
while (($pos = strpos($identifier, "\\")) !== false) {
|
||||
if ($pos > 0) {
|
||||
$part = substr($identifier, 0, $pos);
|
||||
assert($part !== "");
|
||||
$parts[] = $part;
|
||||
}
|
||||
$parts[] = "\\";
|
||||
$identifier = substr($identifier, $pos + 1);
|
||||
}
|
||||
if (($pos = strpos($identifier, "::")) !== false) {
|
||||
if ($pos > 0) {
|
||||
$part = substr($identifier, 0, $pos);
|
||||
assert($part !== "");
|
||||
$parts[] = $part;
|
||||
}
|
||||
$parts[] = "::";
|
||||
$identifier = substr($identifier, $pos + 2);
|
||||
}
|
||||
if ($identifier !== "") {
|
||||
$parts[] = $identifier;
|
||||
}
|
||||
|
||||
return $parts;
|
||||
}
|
||||
}
|
||||
1470
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
vendored
Normal file
1470
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
446
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php
vendored
Normal file
446
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/ScopeAnalyzer.php
vendored
Normal file
@@ -0,0 +1,446 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Internal\Provider\NodeDataProvider;
|
||||
use Psalm\NodeTypeProvider;
|
||||
|
||||
use function array_diff;
|
||||
use function array_filter;
|
||||
use function array_intersect;
|
||||
use function array_unique;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ScopeAnalyzer
|
||||
{
|
||||
public const ACTION_END = 'END';
|
||||
public const ACTION_BREAK = 'BREAK';
|
||||
public const ACTION_CONTINUE = 'CONTINUE';
|
||||
public const ACTION_LEAVE_SWITCH = 'LEAVE_SWITCH';
|
||||
public const ACTION_LEAVE_LOOP = 'LEAVE_LOOP';
|
||||
public const ACTION_NONE = 'NONE';
|
||||
public const ACTION_RETURN = 'RETURN';
|
||||
|
||||
/**
|
||||
* @param array<PhpParser\Node> $stmts
|
||||
* @param list<'loop'|'switch'> $break_types
|
||||
* @param bool $return_is_exit Exit and Throw statements are treated differently from return if this is false
|
||||
* @return list<self::ACTION_*>
|
||||
* @psalm-suppress ComplexMethod nothing much we can do
|
||||
*/
|
||||
public static function getControlActions(
|
||||
array $stmts,
|
||||
?NodeDataProvider $nodes,
|
||||
array $break_types,
|
||||
bool $return_is_exit = true
|
||||
): array {
|
||||
if (empty($stmts)) {
|
||||
return [self::ACTION_NONE];
|
||||
}
|
||||
|
||||
$control_actions = [];
|
||||
|
||||
foreach ($stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Return_ ||
|
||||
$stmt instanceof PhpParser\Node\Stmt\Throw_ ||
|
||||
($stmt instanceof PhpParser\Node\Stmt\Expression && $stmt->expr instanceof PhpParser\Node\Expr\Exit_)
|
||||
) {
|
||||
if (!$return_is_exit && $stmt instanceof PhpParser\Node\Stmt\Return_) {
|
||||
$stmt_return_type = null;
|
||||
if ($nodes && $stmt->expr) {
|
||||
$stmt_return_type = $nodes->getType($stmt->expr);
|
||||
}
|
||||
|
||||
// don't consider a return if the expression never returns (e.g. a throw inside a short closure)
|
||||
if ($stmt_return_type && $stmt_return_type->isNever()) {
|
||||
return array_values(array_unique([...$control_actions, ...[self::ACTION_END]]));
|
||||
}
|
||||
|
||||
return array_values(array_unique([...$control_actions, ...[self::ACTION_RETURN]]));
|
||||
}
|
||||
|
||||
return array_values(array_unique([...$control_actions, ...[self::ACTION_END]]));
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Expression) {
|
||||
// This allows calls to functions that always exit to act as exit statements themselves
|
||||
if ($nodes
|
||||
&& ($stmt_expr_type = $nodes->getType($stmt->expr))
|
||||
&& $stmt_expr_type->isNever()
|
||||
) {
|
||||
return array_values(array_unique([...$control_actions, ...[self::ACTION_END]]));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
|
||||
$count = !$stmt->num
|
||||
? 1
|
||||
: ($stmt->num instanceof PhpParser\Node\Scalar\LNumber ? $stmt->num->value : null);
|
||||
|
||||
if ($break_types && $count !== null && count($break_types) >= $count) {
|
||||
/** @psalm-suppress InvalidArrayOffset Some int-range improvements are needed */
|
||||
if ($break_types[count($break_types) - $count] === 'switch') {
|
||||
return [...$control_actions, ...[self::ACTION_LEAVE_SWITCH]];
|
||||
}
|
||||
|
||||
return array_values($control_actions);
|
||||
}
|
||||
|
||||
return array_values(array_unique([...$control_actions, ...[self::ACTION_CONTINUE]]));
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Break_) {
|
||||
$count = !$stmt->num
|
||||
? 1
|
||||
: ($stmt->num instanceof PhpParser\Node\Scalar\LNumber ? $stmt->num->value : null);
|
||||
|
||||
if ($break_types && $count !== null && count($break_types) >= $count) {
|
||||
/** @psalm-suppress InvalidArrayOffset Some int-range improvements are needed */
|
||||
if ($break_types[count($break_types) - $count] === 'switch') {
|
||||
return [...$control_actions, ...[self::ACTION_LEAVE_SWITCH]];
|
||||
}
|
||||
|
||||
/** @psalm-suppress InvalidArrayOffset Some int-range improvements are needed */
|
||||
if ($break_types[count($break_types) - $count] === 'loop') {
|
||||
return [...$control_actions, ...[self::ACTION_LEAVE_LOOP]];
|
||||
}
|
||||
|
||||
return array_values($control_actions);
|
||||
}
|
||||
|
||||
return array_values(array_unique([...$control_actions, ...[self::ACTION_BREAK]]));
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
|
||||
$if_statement_actions = self::getControlActions(
|
||||
$stmt->stmts,
|
||||
$nodes,
|
||||
$break_types,
|
||||
$return_is_exit,
|
||||
);
|
||||
|
||||
$all_leave = !array_filter(
|
||||
$if_statement_actions,
|
||||
static fn(string $action): bool => $action === self::ACTION_NONE,
|
||||
);
|
||||
|
||||
$else_statement_actions = $stmt->else
|
||||
? self::getControlActions(
|
||||
$stmt->else->stmts,
|
||||
$nodes,
|
||||
$break_types,
|
||||
$return_is_exit,
|
||||
) : [];
|
||||
|
||||
$all_leave = $all_leave
|
||||
&& $else_statement_actions
|
||||
&& !array_filter(
|
||||
$else_statement_actions,
|
||||
static fn(string $action): bool => $action === self::ACTION_NONE,
|
||||
);
|
||||
|
||||
$all_elseif_actions = [];
|
||||
|
||||
if ($stmt->elseifs) {
|
||||
foreach ($stmt->elseifs as $elseif) {
|
||||
$elseif_control_actions = self::getControlActions(
|
||||
$elseif->stmts,
|
||||
$nodes,
|
||||
$break_types,
|
||||
$return_is_exit,
|
||||
);
|
||||
|
||||
$all_leave = $all_leave
|
||||
&& !array_filter(
|
||||
$elseif_control_actions,
|
||||
static fn(string $action): bool => $action === self::ACTION_NONE,
|
||||
);
|
||||
|
||||
$all_elseif_actions = [...$elseif_control_actions, ...$all_elseif_actions];
|
||||
}
|
||||
}
|
||||
|
||||
if ($all_leave) {
|
||||
return array_values(
|
||||
array_unique([
|
||||
...$control_actions,
|
||||
...$if_statement_actions,
|
||||
...$else_statement_actions,
|
||||
...$all_elseif_actions,
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
$control_actions = array_filter(
|
||||
[...$control_actions, ...$if_statement_actions, ...$else_statement_actions, ...$all_elseif_actions],
|
||||
static fn(string $action): bool => $action !== self::ACTION_NONE,
|
||||
);
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
|
||||
$has_ended = false;
|
||||
$has_non_breaking_default = false;
|
||||
$has_default_terminator = false;
|
||||
|
||||
$all_case_actions = [];
|
||||
|
||||
// iterate backwards in a case statement
|
||||
for ($d = count($stmt->cases) - 1; $d >= 0; --$d) {
|
||||
$case = $stmt->cases[$d];
|
||||
|
||||
$case_actions = self::getControlActions(
|
||||
$case->stmts,
|
||||
$nodes,
|
||||
[...$break_types, ...['switch']],
|
||||
$return_is_exit,
|
||||
);
|
||||
|
||||
if (array_intersect([
|
||||
self::ACTION_LEAVE_SWITCH,
|
||||
self::ACTION_BREAK,
|
||||
self::ACTION_CONTINUE,
|
||||
], $case_actions)
|
||||
) {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if (!$case->cond) {
|
||||
$has_non_breaking_default = true;
|
||||
}
|
||||
|
||||
$case_does_end = !array_diff(
|
||||
$control_actions,
|
||||
[self::ACTION_END, self::ACTION_RETURN],
|
||||
);
|
||||
|
||||
if ($case_does_end) {
|
||||
$has_ended = true;
|
||||
}
|
||||
|
||||
$all_case_actions = [...$all_case_actions, ...$case_actions];
|
||||
|
||||
if (!$case_does_end && !$has_ended) {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
if ($has_non_breaking_default && $case_does_end) {
|
||||
$has_default_terminator = true;
|
||||
}
|
||||
}
|
||||
|
||||
$all_case_actions = array_filter(
|
||||
$all_case_actions,
|
||||
static fn(string $action): bool => $action !== self::ACTION_NONE,
|
||||
);
|
||||
|
||||
if ($has_default_terminator || $stmt->getAttribute('allMatched', false)) {
|
||||
return array_values(array_unique([...$control_actions, ...$all_case_actions]));
|
||||
}
|
||||
|
||||
$control_actions = [...$control_actions, ...$all_case_actions];
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Do_
|
||||
|| $stmt instanceof PhpParser\Node\Stmt\While_
|
||||
|| $stmt instanceof PhpParser\Node\Stmt\Foreach_
|
||||
|| $stmt instanceof PhpParser\Node\Stmt\For_
|
||||
) {
|
||||
$loop_actions = self::getControlActions(
|
||||
$stmt->stmts,
|
||||
$nodes,
|
||||
[...$break_types, ...['loop']],
|
||||
$return_is_exit,
|
||||
);
|
||||
|
||||
$control_actions = array_filter(
|
||||
[...$control_actions, ...$loop_actions],
|
||||
static fn(string $action): bool => $action !== self::ACTION_NONE,
|
||||
);
|
||||
|
||||
if (($stmt instanceof PhpParser\Node\Stmt\While_
|
||||
|| $stmt instanceof PhpParser\Node\Stmt\Do_)
|
||||
&& $nodes
|
||||
&& ($stmt_expr_type = $nodes->getType($stmt->cond))
|
||||
&& $stmt_expr_type->isAlwaysTruthy()
|
||||
&& !in_array(self::ACTION_LEAVE_LOOP, $control_actions, true)
|
||||
) {
|
||||
//infinite while loop that only return don't have an exit path
|
||||
$have_exit_path = (bool)array_diff(
|
||||
$control_actions,
|
||||
[self::ACTION_END, self::ACTION_RETURN],
|
||||
);
|
||||
|
||||
if (!$have_exit_path) {
|
||||
return array_values(array_unique($control_actions));
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\For_
|
||||
&& $nodes
|
||||
&& !in_array(self::ACTION_LEAVE_LOOP, $control_actions, true)
|
||||
) {
|
||||
$is_infinite_loop = true;
|
||||
if ($stmt->cond) {
|
||||
foreach ($stmt->cond as $cond) {
|
||||
$stmt_expr_type = $nodes->getType($cond);
|
||||
if (!$stmt_expr_type || !$stmt_expr_type->isAlwaysTruthy()) {
|
||||
$is_infinite_loop = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($is_infinite_loop) {
|
||||
//infinite while loop that only return don't have an exit path
|
||||
$have_exit_path = (bool)array_diff(
|
||||
$control_actions,
|
||||
[self::ACTION_END, self::ACTION_RETURN],
|
||||
);
|
||||
|
||||
if (!$have_exit_path) {
|
||||
return array_values(array_unique($control_actions));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$control_actions = array_filter(
|
||||
$control_actions,
|
||||
static fn(string $action): bool => $action !== self::ACTION_LEAVE_LOOP,
|
||||
);
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
|
||||
$try_statement_actions = self::getControlActions(
|
||||
$stmt->stmts,
|
||||
$nodes,
|
||||
$break_types,
|
||||
$return_is_exit,
|
||||
);
|
||||
|
||||
$try_leaves = !array_filter(
|
||||
$try_statement_actions,
|
||||
static fn(string $action): bool => $action === self::ACTION_NONE,
|
||||
);
|
||||
|
||||
$all_catch_actions = [];
|
||||
|
||||
if ($stmt->catches) {
|
||||
$all_leave = $try_leaves;
|
||||
|
||||
foreach ($stmt->catches as $catch) {
|
||||
$catch_actions = self::getControlActions(
|
||||
$catch->stmts,
|
||||
$nodes,
|
||||
$break_types,
|
||||
$return_is_exit,
|
||||
);
|
||||
|
||||
$all_leave = $all_leave
|
||||
&& !array_filter(
|
||||
$catch_actions,
|
||||
static fn(string $action): bool => $action === self::ACTION_NONE,
|
||||
);
|
||||
|
||||
if (!$all_leave) {
|
||||
$control_actions = [...$control_actions, ...$catch_actions];
|
||||
} else {
|
||||
$all_catch_actions = [...$all_catch_actions, ...$catch_actions];
|
||||
}
|
||||
}
|
||||
|
||||
if ($all_leave && $try_statement_actions !== [self::ACTION_NONE]) {
|
||||
return array_values(
|
||||
array_unique(
|
||||
[...$control_actions, ...$try_statement_actions, ...$all_catch_actions],
|
||||
),
|
||||
);
|
||||
}
|
||||
} elseif ($try_leaves) {
|
||||
return array_values(array_unique([...$control_actions, ...$try_statement_actions]));
|
||||
}
|
||||
|
||||
if ($stmt->finally && $stmt->finally->stmts) {
|
||||
$finally_statement_actions = self::getControlActions(
|
||||
$stmt->finally->stmts,
|
||||
$nodes,
|
||||
$break_types,
|
||||
$return_is_exit,
|
||||
);
|
||||
|
||||
if (!in_array(self::ACTION_NONE, $finally_statement_actions, true)) {
|
||||
return [...array_filter(
|
||||
$control_actions,
|
||||
static fn(string $action): bool => $action !== self::ACTION_NONE,
|
||||
), ...$finally_statement_actions];
|
||||
}
|
||||
}
|
||||
|
||||
$control_actions = array_filter(
|
||||
[...$control_actions, ...$try_statement_actions],
|
||||
static fn(string $action): bool => $action !== self::ACTION_NONE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$control_actions[] = self::ACTION_NONE;
|
||||
|
||||
return array_values(array_unique($control_actions));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<PhpParser\Node> $stmts
|
||||
*/
|
||||
public static function onlyThrowsOrExits(NodeTypeProvider $type_provider, array $stmts): bool
|
||||
{
|
||||
if (empty($stmts)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ($i = count($stmts) - 1; $i >= 0; --$i) {
|
||||
$stmt = $stmts[$i];
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Throw_
|
||||
|| ($stmt instanceof PhpParser\Node\Stmt\Expression
|
||||
&& $stmt->expr instanceof PhpParser\Node\Expr\Exit_)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Expression) {
|
||||
$stmt_type = $type_provider->getType($stmt->expr);
|
||||
|
||||
if ($stmt_type && $stmt_type->isNever()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<PhpParser\Node> $stmts
|
||||
*/
|
||||
public static function onlyThrows(array $stmts): bool
|
||||
{
|
||||
$stmts_count = count($stmts);
|
||||
if ($stmts_count !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($stmts as $stmt) {
|
||||
if ($stmt instanceof PhpParser\Node\Stmt\Throw_) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
197
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/SourceAnalyzer.php
vendored
Normal file
197
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/SourceAnalyzer.php
vendored
Normal file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer;
|
||||
|
||||
use Psalm\Aliases;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\NodeTypeProvider;
|
||||
use Psalm\StatementsSource;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
abstract class SourceAnalyzer implements StatementsSource
|
||||
{
|
||||
protected SourceAnalyzer $source;
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
unset($this->source);
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getAliases(): Aliases
|
||||
{
|
||||
return $this->source->getAliases();
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return array<lowercase-string, string>
|
||||
*/
|
||||
public function getAliasedClassesFlipped(): array
|
||||
{
|
||||
return $this->source->getAliasedClassesFlipped();
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getAliasedClassesFlippedReplaceable(): array
|
||||
{
|
||||
return $this->source->getAliasedClassesFlippedReplaceable();
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getFQCLN(): ?string
|
||||
{
|
||||
return $this->source->getFQCLN();
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getClassName(): ?string
|
||||
{
|
||||
return $this->source->getClassName();
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getParentFQCLN(): ?string
|
||||
{
|
||||
return $this->source->getParentFQCLN();
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getFileName(): string
|
||||
{
|
||||
return $this->source->getFileName();
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getFilePath(): string
|
||||
{
|
||||
return $this->source->getFilePath();
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getRootFileName(): string
|
||||
{
|
||||
return $this->source->getRootFileName();
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getRootFilePath(): string
|
||||
{
|
||||
return $this->source->getRootFilePath();
|
||||
}
|
||||
|
||||
public function setRootFilePath(string $file_path, string $file_name): void
|
||||
{
|
||||
$this->source->setRootFilePath($file_path, $file_name);
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function hasParentFilePath(string $file_path): bool
|
||||
{
|
||||
return $this->source->hasParentFilePath($file_path);
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function hasAlreadyRequiredFilePath(string $file_path): bool
|
||||
{
|
||||
return $this->source->hasAlreadyRequiredFilePath($file_path);
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getRequireNesting(): int
|
||||
{
|
||||
return $this->source->getRequireNesting();
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function getSource(): StatementsSource
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of suppressed issues
|
||||
*
|
||||
* @psalm-mutation-free
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getSuppressedIssues(): array
|
||||
{
|
||||
return $this->source->getSuppressedIssues();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $new_issues
|
||||
*/
|
||||
public function addSuppressedIssues(array $new_issues): void
|
||||
{
|
||||
$this->source->addSuppressedIssues($new_issues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $new_issues
|
||||
*/
|
||||
public function removeSuppressedIssues(array $new_issues): void
|
||||
{
|
||||
$this->source->removeSuppressedIssues($new_issues);
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getNamespace(): ?string
|
||||
{
|
||||
return $this->source->getNamespace();
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function isStatic(): bool
|
||||
{
|
||||
return $this->source->isStatic();
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function getCodebase(): Codebase
|
||||
{
|
||||
return $this->source->getCodebase();
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function getProjectAnalyzer(): ProjectAnalyzer
|
||||
{
|
||||
return $this->source->getProjectAnalyzer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
*/
|
||||
public function getFileAnalyzer(): FileAnalyzer
|
||||
{
|
||||
return $this->source->getFileAnalyzer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-mutation-free
|
||||
* @return array<string, array<string, Union>>|null
|
||||
*/
|
||||
public function getTemplateTypeMap(): ?array
|
||||
{
|
||||
return $this->source->getTemplateTypeMap();
|
||||
}
|
||||
|
||||
/** @psalm-mutation-free */
|
||||
public function getNodeTypeProvider(): NodeTypeProvider
|
||||
{
|
||||
return $this->source->getNodeTypeProvider();
|
||||
}
|
||||
}
|
||||
205
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php
vendored
Normal file
205
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/DoAnalyzer.php
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Block;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Algebra\FormulaGenerator;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Clause;
|
||||
use Psalm\Internal\Scope\LoopScope;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Reconciler;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_diff;
|
||||
use function array_filter;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function in_array;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class DoAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Stmt\Do_ $stmt,
|
||||
Context $context
|
||||
): ?bool {
|
||||
$do_context = clone $context;
|
||||
$do_context->break_types[] = 'loop';
|
||||
$do_context->inside_loop = true;
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
if ($codebase->alter_code) {
|
||||
$do_context->branch_point = $do_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
|
||||
}
|
||||
|
||||
$loop_scope = new LoopScope($do_context, $context);
|
||||
$loop_scope->protected_var_ids = $context->protected_var_ids;
|
||||
|
||||
self::analyzeDoNaively($statements_analyzer, $stmt, $do_context, $loop_scope);
|
||||
|
||||
$mixed_var_ids = [];
|
||||
|
||||
foreach ($do_context->vars_in_scope as $var_id => $type) {
|
||||
if ($type->hasMixed()) {
|
||||
$mixed_var_ids[] = $var_id;
|
||||
}
|
||||
}
|
||||
|
||||
$cond_id = spl_object_id($stmt->cond);
|
||||
|
||||
$while_clauses = FormulaGenerator::getFormula(
|
||||
$cond_id,
|
||||
$cond_id,
|
||||
$stmt->cond,
|
||||
$context->self,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
$while_clauses = array_values(
|
||||
array_filter(
|
||||
$while_clauses,
|
||||
static function (Clause $c) use ($mixed_var_ids): bool {
|
||||
$keys = array_keys($c->possibilities);
|
||||
|
||||
$mixed_var_ids = array_diff($mixed_var_ids, $keys);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
foreach ($mixed_var_ids as $mixed_var_id) {
|
||||
if (preg_match('/^' . preg_quote($mixed_var_id, '/') . '(\[|-)/', $key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (!$while_clauses) {
|
||||
$while_clauses = [new Clause([], $cond_id, $cond_id, true)];
|
||||
}
|
||||
|
||||
if (LoopAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt->stmts,
|
||||
WhileAnalyzer::getAndExpressions($stmt->cond),
|
||||
[],
|
||||
$loop_scope,
|
||||
$inner_loop_context,
|
||||
true,
|
||||
true,
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// because it's a do {} while, inner loop vars belong to the main context
|
||||
if (!$inner_loop_context) {
|
||||
throw new UnexpectedValueException('There should be an inner loop context');
|
||||
}
|
||||
|
||||
$negated_while_clauses = Algebra::negateFormula($while_clauses);
|
||||
|
||||
$negated_while_types = Algebra::getTruthsFromFormula(
|
||||
Algebra::simplifyCNF(
|
||||
[...$context->clauses, ...$negated_while_clauses],
|
||||
),
|
||||
);
|
||||
|
||||
if ($negated_while_types) {
|
||||
$changed_var_ids = [];
|
||||
|
||||
[$inner_loop_context->vars_in_scope, $inner_loop_context->references_in_scope] =
|
||||
Reconciler::reconcileKeyedTypes(
|
||||
$negated_while_types,
|
||||
[],
|
||||
$inner_loop_context->vars_in_scope,
|
||||
$inner_loop_context->references_in_scope,
|
||||
$changed_var_ids,
|
||||
[],
|
||||
$statements_analyzer,
|
||||
[],
|
||||
true,
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt->cond),
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($inner_loop_context->vars_in_scope as $var_id => $type) {
|
||||
// if there are break statements in the loop it's not certain
|
||||
// that the loop has finished executing, so the assertions at the end
|
||||
// the loop in the while conditional may not hold
|
||||
if (in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true)) {
|
||||
if (isset($loop_scope->possibly_defined_loop_parent_vars[$var_id])) {
|
||||
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$loop_scope->possibly_defined_loop_parent_vars[$var_id],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$context->vars_in_scope[$var_id] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
$do_context->loop_scope = null;
|
||||
|
||||
$context->vars_possibly_in_scope = array_merge(
|
||||
$context->vars_possibly_in_scope,
|
||||
$do_context->vars_possibly_in_scope,
|
||||
);
|
||||
|
||||
if ($context->collect_exceptions) {
|
||||
$context->mergeExceptions($inner_loop_context);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function analyzeDoNaively(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Stmt\Do_ $stmt,
|
||||
Context $context,
|
||||
LoopScope $loop_scope
|
||||
): void {
|
||||
$do_context = clone $context;
|
||||
|
||||
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
|
||||
|
||||
if (!in_array('RedundantCondition', $suppressed_issues, true)) {
|
||||
$statements_analyzer->addSuppressedIssues(['RedundantCondition']);
|
||||
}
|
||||
if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
|
||||
$statements_analyzer->addSuppressedIssues(['RedundantConditionGivenDocblockType']);
|
||||
}
|
||||
if (!in_array('TypeDoesNotContainType', $suppressed_issues, true)) {
|
||||
$statements_analyzer->addSuppressedIssues(['TypeDoesNotContainType']);
|
||||
}
|
||||
|
||||
$do_context->loop_scope = $loop_scope;
|
||||
|
||||
$statements_analyzer->analyze($stmt->stmts, $do_context);
|
||||
|
||||
if (!in_array('RedundantCondition', $suppressed_issues, true)) {
|
||||
$statements_analyzer->removeSuppressedIssues(['RedundantCondition']);
|
||||
}
|
||||
if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
|
||||
$statements_analyzer->removeSuppressedIssues(['RedundantConditionGivenDocblockType']);
|
||||
}
|
||||
if (!in_array('TypeDoesNotContainType', $suppressed_issues, true)) {
|
||||
$statements_analyzer->removeSuppressedIssues(['TypeDoesNotContainType']);
|
||||
}
|
||||
}
|
||||
}
|
||||
185
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php
vendored
Normal file
185
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/ForAnalyzer.php
vendored
Normal file
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Block;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Scope\LoopScope;
|
||||
use Psalm\Type;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ForAnalyzer
|
||||
{
|
||||
/**
|
||||
* @return false|null
|
||||
*/
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Stmt\For_ $stmt,
|
||||
Context $context
|
||||
): ?bool {
|
||||
$pre_assigned_var_ids = $context->assigned_var_ids;
|
||||
$context->assigned_var_ids = [];
|
||||
|
||||
$init_var_types = [];
|
||||
|
||||
foreach ($stmt->init as $init) {
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $init, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($init instanceof PhpParser\Node\Expr\Assign
|
||||
&& $init->var instanceof PhpParser\Node\Expr\Variable
|
||||
&& is_string($init->var->name)
|
||||
&& ($init_var_type = $statements_analyzer->node_data->getType($init->expr))
|
||||
) {
|
||||
$init_var_types[$init->var->name] = $init_var_type;
|
||||
}
|
||||
}
|
||||
|
||||
$assigned_var_ids = $context->assigned_var_ids;
|
||||
|
||||
$context->assigned_var_ids = array_merge(
|
||||
$pre_assigned_var_ids,
|
||||
$assigned_var_ids,
|
||||
);
|
||||
|
||||
$while_true = !$stmt->cond && !$stmt->init && !$stmt->loop;
|
||||
|
||||
$pre_context = null;
|
||||
|
||||
if ($while_true) {
|
||||
$pre_context = clone $context;
|
||||
}
|
||||
|
||||
$for_context = clone $context;
|
||||
|
||||
$for_context->inside_loop = true;
|
||||
$for_context->break_types[] = 'loop';
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
if ($codebase->alter_code) {
|
||||
$for_context->branch_point = $for_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
|
||||
}
|
||||
|
||||
$loop_scope = new LoopScope($for_context, $context);
|
||||
|
||||
$loop_scope->protected_var_ids = array_merge(
|
||||
$assigned_var_ids,
|
||||
$context->protected_var_ids,
|
||||
);
|
||||
|
||||
if (LoopAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt->stmts,
|
||||
$stmt->cond,
|
||||
$stmt->loop,
|
||||
$loop_scope,
|
||||
$inner_loop_context,
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$inner_loop_context) {
|
||||
throw new UnexpectedValueException('There should be an inner loop context');
|
||||
}
|
||||
|
||||
$always_enters_loop = false;
|
||||
|
||||
foreach ($stmt->cond as $cond) {
|
||||
if ($cond_type = $statements_analyzer->node_data->getType($cond)) {
|
||||
$always_enters_loop = $cond_type->isAlwaysTruthy();
|
||||
}
|
||||
|
||||
if (count($stmt->init) === 1
|
||||
&& count($stmt->cond) === 1
|
||||
&& $cond instanceof PhpParser\Node\Expr\BinaryOp
|
||||
&& ($cond_value = $statements_analyzer->node_data->getType($cond->right))
|
||||
&& ($cond_value->isSingleIntLiteral() || $cond_value->isSingleStringLiteral())
|
||||
&& $cond->left instanceof PhpParser\Node\Expr\Variable
|
||||
&& is_string($cond->left->name)
|
||||
&& isset($init_var_types[$cond->left->name])
|
||||
&& $init_var_types[$cond->left->name]->isSingleIntLiteral()
|
||||
) {
|
||||
$init_value = $init_var_types[$cond->left->name]->getSingleLiteral()->value;
|
||||
$cond_value = $cond_value->getSingleLiteral()->value;
|
||||
|
||||
if ($cond instanceof PhpParser\Node\Expr\BinaryOp\Smaller && $init_value < $cond_value) {
|
||||
$always_enters_loop = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($cond instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual && $init_value <= $cond_value) {
|
||||
$always_enters_loop = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($cond instanceof PhpParser\Node\Expr\BinaryOp\Greater && $init_value > $cond_value) {
|
||||
$always_enters_loop = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($cond instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual && $init_value >= $cond_value) {
|
||||
$always_enters_loop = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($while_true) {
|
||||
$always_enters_loop = true;
|
||||
}
|
||||
|
||||
$can_leave_loop = !$while_true
|
||||
|| in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true);
|
||||
|
||||
if ($always_enters_loop && $can_leave_loop) {
|
||||
foreach ($inner_loop_context->vars_in_scope as $var_id => $type) {
|
||||
// if there are break statements in the loop it's not certain
|
||||
// that the loop has finished executing, so the assertions at the end
|
||||
// the loop in the while conditional may not hold
|
||||
if (in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true)
|
||||
|| in_array(ScopeAnalyzer::ACTION_CONTINUE, $loop_scope->final_actions, true)
|
||||
) {
|
||||
if (isset($loop_scope->possibly_defined_loop_parent_vars[$var_id])) {
|
||||
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$loop_scope->possibly_defined_loop_parent_vars[$var_id],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$context->vars_in_scope[$var_id] = $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$for_context->loop_scope = null;
|
||||
|
||||
if ($can_leave_loop) {
|
||||
$context->vars_possibly_in_scope = array_merge(
|
||||
$context->vars_possibly_in_scope,
|
||||
$for_context->vars_possibly_in_scope,
|
||||
);
|
||||
} elseif ($pre_context) {
|
||||
$context->vars_possibly_in_scope = $pre_context->vars_possibly_in_scope;
|
||||
}
|
||||
|
||||
if ($context->collect_exceptions) {
|
||||
$context->mergeExceptions($for_context);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
1149
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php
vendored
Normal file
1149
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
372
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php
vendored
Normal file
372
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/IfConditionalAnalyzer.php
vendored
Normal file
@@ -0,0 +1,372 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Block;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Context;
|
||||
use Psalm\Exception\ScopeAnalysisException;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Clause;
|
||||
use Psalm\Internal\Scope\IfConditionalScope;
|
||||
use Psalm\Internal\Scope\IfScope;
|
||||
use Psalm\Issue\DocblockTypeContradiction;
|
||||
use Psalm\Issue\RedundantCondition;
|
||||
use Psalm\Issue\RedundantConditionGivenDocblockType;
|
||||
use Psalm\Issue\TypeDoesNotContainType;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
use function array_diff_key;
|
||||
use function array_filter;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class IfConditionalAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr $cond,
|
||||
Context $outer_context,
|
||||
Codebase $codebase,
|
||||
IfScope $if_scope,
|
||||
int $branch_point
|
||||
): IfConditionalScope {
|
||||
$entry_clauses = [];
|
||||
|
||||
// used when evaluating elseifs
|
||||
if ($if_scope->negated_clauses) {
|
||||
$entry_clauses = [...$outer_context->clauses, ...$if_scope->negated_clauses];
|
||||
|
||||
$changed_var_ids = [];
|
||||
|
||||
if ($if_scope->negated_types) {
|
||||
[$vars_reconciled, $references_reconciled] = Reconciler::reconcileKeyedTypes(
|
||||
$if_scope->negated_types,
|
||||
[],
|
||||
$outer_context->vars_in_scope,
|
||||
$outer_context->references_in_scope,
|
||||
$changed_var_ids,
|
||||
[],
|
||||
$statements_analyzer,
|
||||
[],
|
||||
$outer_context->inside_loop,
|
||||
new CodeLocation(
|
||||
$statements_analyzer->getSource(),
|
||||
$cond instanceof PhpParser\Node\Expr\BooleanNot
|
||||
? $cond->expr
|
||||
: $cond,
|
||||
$outer_context->include_location,
|
||||
false,
|
||||
),
|
||||
);
|
||||
|
||||
if ($changed_var_ids) {
|
||||
$outer_context = clone $outer_context;
|
||||
$outer_context->vars_in_scope = $vars_reconciled;
|
||||
$outer_context->references_in_scope = $references_reconciled;
|
||||
|
||||
$entry_clauses = array_values(
|
||||
array_filter(
|
||||
$entry_clauses,
|
||||
static fn(Clause $c): bool => count($c->possibilities) > 1
|
||||
|| $c->wedge
|
||||
|| !isset($changed_var_ids[array_keys($c->possibilities)[0]])
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get the first expression in the if, which should be evaluated on its own
|
||||
// this allows us to update the context of $matches in
|
||||
// if (!preg_match('/a/', 'aa', $matches)) {
|
||||
// exit
|
||||
// }
|
||||
// echo $matches[0];
|
||||
$externally_applied_if_cond_expr = self::getDefinitelyEvaluatedExpressionAfterIf($cond);
|
||||
|
||||
$internally_applied_if_cond_expr = self::getDefinitelyEvaluatedExpressionInsideIf($cond);
|
||||
|
||||
$pre_condition_vars_in_scope = $outer_context->vars_in_scope;
|
||||
|
||||
$referenced_var_ids = $outer_context->cond_referenced_var_ids;
|
||||
$outer_context->cond_referenced_var_ids = [];
|
||||
|
||||
$pre_assigned_var_ids = $outer_context->assigned_var_ids;
|
||||
$outer_context->assigned_var_ids = [];
|
||||
|
||||
$if_context = null;
|
||||
|
||||
if ($internally_applied_if_cond_expr !== $externally_applied_if_cond_expr) {
|
||||
$if_context = clone $outer_context;
|
||||
}
|
||||
|
||||
$was_inside_conditional = $outer_context->inside_conditional;
|
||||
|
||||
$outer_context->inside_conditional = true;
|
||||
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$externally_applied_if_cond_expr,
|
||||
$outer_context,
|
||||
) === false) {
|
||||
throw new ScopeAnalysisException();
|
||||
}
|
||||
|
||||
$first_cond_assigned_var_ids = $outer_context->assigned_var_ids;
|
||||
$outer_context->assigned_var_ids = array_merge(
|
||||
$pre_assigned_var_ids,
|
||||
$first_cond_assigned_var_ids,
|
||||
);
|
||||
|
||||
$first_cond_referenced_var_ids = $outer_context->cond_referenced_var_ids;
|
||||
$outer_context->cond_referenced_var_ids = array_merge(
|
||||
$referenced_var_ids,
|
||||
$first_cond_referenced_var_ids,
|
||||
);
|
||||
|
||||
$outer_context->inside_conditional = $was_inside_conditional;
|
||||
|
||||
if (!$if_context) {
|
||||
$if_context = clone $outer_context;
|
||||
}
|
||||
|
||||
$if_conditional_context = clone $if_context;
|
||||
|
||||
// here we set up a context specifically for the statements in the first `if`, which can
|
||||
// be affected by statements in the if condition
|
||||
$if_conditional_context->if_body_context = $if_context;
|
||||
|
||||
if ($codebase->alter_code) {
|
||||
$if_context->branch_point = $branch_point;
|
||||
}
|
||||
|
||||
// we need to clone the current context so our ongoing updates
|
||||
// to $outer_context don't mess with elseif/else blocks
|
||||
$post_if_context = clone $outer_context;
|
||||
|
||||
if ($internally_applied_if_cond_expr !== $cond
|
||||
|| $externally_applied_if_cond_expr !== $cond
|
||||
) {
|
||||
$assigned_var_ids = $first_cond_assigned_var_ids;
|
||||
$if_conditional_context->assigned_var_ids = [];
|
||||
|
||||
$referenced_var_ids = $first_cond_referenced_var_ids;
|
||||
$if_conditional_context->cond_referenced_var_ids = [];
|
||||
|
||||
$was_inside_conditional = $if_conditional_context->inside_conditional;
|
||||
|
||||
$if_conditional_context->inside_conditional = true;
|
||||
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $cond, $if_conditional_context) === false) {
|
||||
throw new ScopeAnalysisException();
|
||||
}
|
||||
|
||||
$if_conditional_context->inside_conditional = $was_inside_conditional;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
$more_cond_referenced_var_ids = $if_conditional_context->cond_referenced_var_ids;
|
||||
$if_conditional_context->cond_referenced_var_ids = array_merge(
|
||||
$more_cond_referenced_var_ids,
|
||||
$referenced_var_ids,
|
||||
);
|
||||
|
||||
$cond_referenced_var_ids = array_merge(
|
||||
$first_cond_referenced_var_ids,
|
||||
$more_cond_referenced_var_ids,
|
||||
);
|
||||
|
||||
/** @var array<string, int> */
|
||||
$more_cond_assigned_var_ids = $if_conditional_context->assigned_var_ids;
|
||||
$if_conditional_context->assigned_var_ids = array_merge(
|
||||
$more_cond_assigned_var_ids,
|
||||
$assigned_var_ids,
|
||||
);
|
||||
|
||||
$assigned_in_conditional_var_ids = array_merge(
|
||||
$first_cond_assigned_var_ids,
|
||||
$more_cond_assigned_var_ids,
|
||||
);
|
||||
} else {
|
||||
$cond_referenced_var_ids = $first_cond_referenced_var_ids;
|
||||
|
||||
$assigned_in_conditional_var_ids = $first_cond_assigned_var_ids;
|
||||
}
|
||||
|
||||
$newish_var_ids = [];
|
||||
|
||||
foreach (array_diff_key(
|
||||
$if_conditional_context->vars_in_scope,
|
||||
$pre_condition_vars_in_scope,
|
||||
$cond_referenced_var_ids,
|
||||
$assigned_in_conditional_var_ids,
|
||||
) as $name => $_value) {
|
||||
$newish_var_ids[$name] = true;
|
||||
}
|
||||
|
||||
self::handleParadoxicalCondition($statements_analyzer, $cond, true);
|
||||
|
||||
// get all the var ids that were referenced in the conditional, but not assigned in it
|
||||
$cond_referenced_var_ids = array_diff_key($cond_referenced_var_ids, $assigned_in_conditional_var_ids);
|
||||
|
||||
$cond_referenced_var_ids = array_merge($newish_var_ids, $cond_referenced_var_ids);
|
||||
|
||||
return new IfConditionalScope(
|
||||
$if_context,
|
||||
$post_if_context,
|
||||
$cond_referenced_var_ids,
|
||||
$assigned_in_conditional_var_ids,
|
||||
$entry_clauses,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns statements that are definitely evaluated before any statements after the end of the
|
||||
* if/elseif/else blocks
|
||||
*/
|
||||
private static function getDefinitelyEvaluatedExpressionAfterIf(PhpParser\Node\Expr $stmt): PhpParser\Node\Expr
|
||||
{
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical
|
||||
) {
|
||||
if ($stmt->left instanceof PhpParser\Node\Expr\ConstFetch
|
||||
&& $stmt->left->name->parts === ['true']
|
||||
) {
|
||||
return self::getDefinitelyEvaluatedExpressionAfterIf($stmt->right);
|
||||
}
|
||||
|
||||
if ($stmt->right instanceof PhpParser\Node\Expr\ConstFetch
|
||||
&& $stmt->right->name->parts === ['true']
|
||||
) {
|
||||
return self::getDefinitelyEvaluatedExpressionAfterIf($stmt->left);
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp) {
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalXor
|
||||
) {
|
||||
return self::getDefinitelyEvaluatedExpressionAfterIf($stmt->left);
|
||||
}
|
||||
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BooleanNot) {
|
||||
$inner_stmt = self::getDefinitelyEvaluatedExpressionInsideIf($stmt->expr);
|
||||
|
||||
if ($inner_stmt !== $stmt->expr) {
|
||||
return $inner_stmt;
|
||||
}
|
||||
}
|
||||
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns statements that are definitely evaluated before any statements inside
|
||||
* the if block
|
||||
*/
|
||||
private static function getDefinitelyEvaluatedExpressionInsideIf(PhpParser\Node\Expr $stmt): PhpParser\Node\Expr
|
||||
{
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical
|
||||
) {
|
||||
if ($stmt->left instanceof PhpParser\Node\Expr\ConstFetch
|
||||
&& $stmt->left->name->parts === ['true']
|
||||
) {
|
||||
return self::getDefinitelyEvaluatedExpressionInsideIf($stmt->right);
|
||||
}
|
||||
|
||||
if ($stmt->right instanceof PhpParser\Node\Expr\ConstFetch
|
||||
&& $stmt->right->name->parts === ['true']
|
||||
) {
|
||||
return self::getDefinitelyEvaluatedExpressionInsideIf($stmt->left);
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp) {
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalXor
|
||||
) {
|
||||
return self::getDefinitelyEvaluatedExpressionInsideIf($stmt->left);
|
||||
}
|
||||
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BooleanNot) {
|
||||
$inner_stmt = self::getDefinitelyEvaluatedExpressionAfterIf($stmt->expr);
|
||||
|
||||
if ($inner_stmt !== $stmt->expr) {
|
||||
return $inner_stmt;
|
||||
}
|
||||
}
|
||||
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
public static function handleParadoxicalCondition(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr $stmt,
|
||||
bool $emit_redundant_with_assignation = false
|
||||
): void {
|
||||
$type = $statements_analyzer->node_data->getType($stmt);
|
||||
|
||||
if ($type !== null) {
|
||||
if ($type->isAlwaysFalsy()) {
|
||||
if ($type->from_docblock) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new DocblockTypeContradiction(
|
||||
'Operand of type ' . $type->getId() . ' is always falsy',
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
$type->getId() . ' falsy',
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
new TypeDoesNotContainType(
|
||||
'Operand of type ' . $type->getId() . ' is always falsy',
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
$type->getId() . ' falsy',
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
} elseif ($type->isAlwaysTruthy() &&
|
||||
(!$stmt instanceof PhpParser\Node\Expr\Assign || $emit_redundant_with_assignation)
|
||||
) {
|
||||
if ($type->from_docblock) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new RedundantConditionGivenDocblockType(
|
||||
'Operand of type ' . $type->getId() . ' is always truthy',
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
$type->getId() . ' falsy',
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
new RedundantCondition(
|
||||
'Operand of type ' . $type->getId() . ' is always truthy',
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
$type->getId() . ' falsy',
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
237
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseAnalyzer.php
vendored
Normal file
237
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseAnalyzer.php
vendored
Normal file
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Block\IfElse;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Scope\IfScope;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Issue\ConflictingReferenceConstraint;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
use function array_diff_key;
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ElseAnalyzer
|
||||
{
|
||||
/**
|
||||
* @return false|null
|
||||
*/
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
?PhpParser\Node\Stmt\Else_ $else,
|
||||
IfScope $if_scope,
|
||||
Context $else_context,
|
||||
Context $outer_context
|
||||
): ?bool {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
if (!$else && !$if_scope->negated_clauses && !$else_context->clauses) {
|
||||
$if_scope->final_actions = array_merge([ScopeAnalyzer::ACTION_NONE], $if_scope->final_actions);
|
||||
$if_scope->assigned_var_ids = [];
|
||||
$if_scope->new_vars = [];
|
||||
$if_scope->redefined_vars = [];
|
||||
$if_scope->reasonable_clauses = [];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$else_context->clauses = Algebra::simplifyCNF(
|
||||
[...$else_context->clauses, ...$if_scope->negated_clauses],
|
||||
);
|
||||
|
||||
$else_types = Algebra::getTruthsFromFormula($else_context->clauses);
|
||||
|
||||
$original_context = clone $else_context;
|
||||
|
||||
if ($else_types) {
|
||||
$changed_var_ids = [];
|
||||
|
||||
[$else_context->vars_in_scope, $else_context->references_in_scope] = Reconciler::reconcileKeyedTypes(
|
||||
$else_types,
|
||||
[],
|
||||
$else_context->vars_in_scope,
|
||||
$else_context->references_in_scope,
|
||||
$changed_var_ids,
|
||||
[],
|
||||
$statements_analyzer,
|
||||
$statements_analyzer->getTemplateTypeMap() ?: [],
|
||||
$else_context->inside_loop,
|
||||
$else
|
||||
? new CodeLocation($statements_analyzer->getSource(), $else, $outer_context->include_location)
|
||||
: null,
|
||||
);
|
||||
|
||||
$else_context->clauses = Context::removeReconciledClauses($else_context->clauses, $changed_var_ids)[0];
|
||||
|
||||
foreach ($changed_var_ids as $changed_var_id => $_) {
|
||||
foreach ($else_context->vars_in_scope as $var_id => $_) {
|
||||
if (preg_match('/' . preg_quote($changed_var_id, '/') . '[\]\[\-]/', $var_id)
|
||||
&& !array_key_exists($var_id, $changed_var_ids)
|
||||
) {
|
||||
$else_context->removePossibleReference($var_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$old_else_context = clone $else_context;
|
||||
|
||||
$pre_stmts_assigned_var_ids = $else_context->assigned_var_ids;
|
||||
$else_context->assigned_var_ids = [];
|
||||
|
||||
$pre_possibly_assigned_var_ids = $else_context->possibly_assigned_var_ids;
|
||||
$else_context->possibly_assigned_var_ids = [];
|
||||
|
||||
if ($else) {
|
||||
if ($statements_analyzer->analyze(
|
||||
$else->stmts,
|
||||
$else_context,
|
||||
) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($else_context->parent_remove_vars as $var_id => $_) {
|
||||
$outer_context->removeVarFromConflictingClauses($var_id);
|
||||
}
|
||||
|
||||
/** @var array<string, int> */
|
||||
$new_assigned_var_ids = $else_context->assigned_var_ids;
|
||||
$else_context->assigned_var_ids += $pre_stmts_assigned_var_ids;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
$new_possibly_assigned_var_ids = $else_context->possibly_assigned_var_ids;
|
||||
$else_context->possibly_assigned_var_ids += $pre_possibly_assigned_var_ids;
|
||||
|
||||
if ($else) {
|
||||
foreach ($else_context->byref_constraints as $var_id => $byref_constraint) {
|
||||
if (isset($outer_context->byref_constraints[$var_id])
|
||||
&& ($outer_constraint_type = $outer_context->byref_constraints[$var_id]->type)
|
||||
&& $byref_constraint->type
|
||||
&& !UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$byref_constraint->type,
|
||||
$outer_constraint_type,
|
||||
)
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new ConflictingReferenceConstraint(
|
||||
'There is more than one pass-by-reference constraint on ' . $var_id,
|
||||
new CodeLocation($statements_analyzer, $else, $outer_context->include_location, true),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
$outer_context->byref_constraints[$var_id] = $byref_constraint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$final_actions = $else
|
||||
? ScopeAnalyzer::getControlActions(
|
||||
$else->stmts,
|
||||
$statements_analyzer->node_data,
|
||||
[],
|
||||
)
|
||||
: [ScopeAnalyzer::ACTION_NONE];
|
||||
// has a return/throw at end
|
||||
$has_ending_statements = $final_actions === [ScopeAnalyzer::ACTION_END];
|
||||
$has_leaving_statements = $has_ending_statements
|
||||
|| (count($final_actions) && !in_array(ScopeAnalyzer::ACTION_NONE, $final_actions, true));
|
||||
|
||||
$has_break_statement = $final_actions === [ScopeAnalyzer::ACTION_BREAK];
|
||||
$has_continue_statement = $final_actions === [ScopeAnalyzer::ACTION_CONTINUE];
|
||||
|
||||
$if_scope->final_actions = array_merge($final_actions, $if_scope->final_actions);
|
||||
|
||||
// if it doesn't end in a return
|
||||
if (!$has_leaving_statements) {
|
||||
IfAnalyzer::updateIfScope(
|
||||
$codebase,
|
||||
$if_scope,
|
||||
$else_context,
|
||||
$original_context,
|
||||
$new_assigned_var_ids,
|
||||
$new_possibly_assigned_var_ids,
|
||||
[],
|
||||
true,
|
||||
);
|
||||
|
||||
$if_scope->reasonable_clauses = [];
|
||||
}
|
||||
|
||||
// update the parent context as necessary
|
||||
if ($if_scope->negatable_if_types) {
|
||||
$outer_context->update(
|
||||
$old_else_context,
|
||||
$else_context,
|
||||
$has_leaving_statements,
|
||||
array_keys($if_scope->negatable_if_types),
|
||||
$if_scope->updated_vars,
|
||||
);
|
||||
}
|
||||
|
||||
if (!$has_ending_statements) {
|
||||
$vars_possibly_in_scope = array_diff_key(
|
||||
$else_context->vars_possibly_in_scope,
|
||||
$outer_context->vars_possibly_in_scope,
|
||||
);
|
||||
|
||||
$possibly_assigned_var_ids = $new_possibly_assigned_var_ids;
|
||||
|
||||
if ($has_leaving_statements) {
|
||||
if ($else_context->loop_scope) {
|
||||
if (!$has_continue_statement && !$has_break_statement) {
|
||||
$if_scope->new_vars_possibly_in_scope = array_merge(
|
||||
$vars_possibly_in_scope,
|
||||
$if_scope->new_vars_possibly_in_scope,
|
||||
);
|
||||
}
|
||||
|
||||
$else_context->loop_scope->vars_possibly_in_scope = array_merge(
|
||||
$vars_possibly_in_scope,
|
||||
$else_context->loop_scope->vars_possibly_in_scope,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$if_scope->new_vars_possibly_in_scope = array_merge(
|
||||
$vars_possibly_in_scope,
|
||||
$if_scope->new_vars_possibly_in_scope,
|
||||
);
|
||||
|
||||
$if_scope->possibly_assigned_var_ids = array_merge(
|
||||
$possibly_assigned_var_ids,
|
||||
$if_scope->possibly_assigned_var_ids,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($outer_context->collect_exceptions) {
|
||||
$outer_context->mergeExceptions($else_context);
|
||||
}
|
||||
|
||||
// Track references set in the else to make sure they aren't reused later
|
||||
$outer_context->updateReferencesPossiblyFromConfusingScope(
|
||||
$else_context,
|
||||
$statements_analyzer,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
430
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseIfAnalyzer.php
vendored
Normal file
430
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/ElseIfAnalyzer.php
vendored
Normal file
@@ -0,0 +1,430 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Block\IfElse;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Context;
|
||||
use Psalm\Exception\ComplicatedExpressionException;
|
||||
use Psalm\Exception\ScopeAnalysisException;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Algebra\FormulaGenerator;
|
||||
use Psalm\Internal\Analyzer\AlgebraAnalyzer;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\IfConditionalAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Clause;
|
||||
use Psalm\Internal\Scope\IfScope;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Issue\ConflictingReferenceConstraint;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
use function array_combine;
|
||||
use function array_diff;
|
||||
use function array_diff_key;
|
||||
use function array_filter;
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_reduce;
|
||||
use function array_unique;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ElseIfAnalyzer
|
||||
{
|
||||
/**
|
||||
* @return false|null
|
||||
*/
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Stmt\ElseIf_ $elseif,
|
||||
IfScope $if_scope,
|
||||
Context $else_context,
|
||||
Context $outer_context,
|
||||
Codebase $codebase,
|
||||
int $branch_point
|
||||
): ?bool {
|
||||
$pre_conditional_context = clone $else_context;
|
||||
|
||||
try {
|
||||
$if_conditional_scope = IfConditionalAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$elseif->cond,
|
||||
$else_context,
|
||||
$codebase,
|
||||
$if_scope,
|
||||
$branch_point,
|
||||
);
|
||||
|
||||
$elseif_context = $if_conditional_scope->if_context;
|
||||
$cond_referenced_var_ids = $if_conditional_scope->cond_referenced_var_ids;
|
||||
$assigned_in_conditional_var_ids = $if_conditional_scope->assigned_in_conditional_var_ids;
|
||||
} catch (ScopeAnalysisException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mixed_var_ids = [];
|
||||
|
||||
foreach ($elseif_context->vars_in_scope as $var_id => $type) {
|
||||
if ($type->hasMixed()) {
|
||||
$mixed_var_ids[] = $var_id;
|
||||
}
|
||||
}
|
||||
|
||||
$elseif_cond_id = spl_object_id($elseif->cond);
|
||||
|
||||
$elseif_clauses = FormulaGenerator::getFormula(
|
||||
$elseif_cond_id,
|
||||
$elseif_cond_id,
|
||||
$elseif->cond,
|
||||
$else_context->self,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
$elseif_clauses_handled = [];
|
||||
|
||||
foreach ($elseif_clauses as $clause) {
|
||||
$keys = array_keys($clause->possibilities);
|
||||
$mixed_var_ids = array_diff($mixed_var_ids, $keys);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
foreach ($mixed_var_ids as $mixed_var_id) {
|
||||
if (preg_match('/^' . preg_quote($mixed_var_id, '/') . '(\[|-)/', $key)) {
|
||||
$elseif_clauses_handled[] = new Clause([], $elseif_cond_id, $elseif_cond_id, true);
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$elseif_clauses_handled[] = $clause;
|
||||
}
|
||||
|
||||
$elseif_clauses = $elseif_clauses_handled;
|
||||
|
||||
$entry_clauses = [];
|
||||
|
||||
foreach ($if_conditional_scope->entry_clauses as $c) {
|
||||
foreach ($c->possibilities as $key => $_value) {
|
||||
foreach ($assigned_in_conditional_var_ids as $conditional_assigned_var_id => $_) {
|
||||
if (preg_match('/^'.preg_quote($conditional_assigned_var_id, '/').'(\[|-|$)/', $key)) {
|
||||
$entry_clauses[] = new Clause([], $elseif_cond_id, $elseif_cond_id, true);
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$entry_clauses[] = $c;
|
||||
}
|
||||
|
||||
// this will see whether any of the clauses in set A conflict with the clauses in set B
|
||||
AlgebraAnalyzer::checkForParadox(
|
||||
$entry_clauses,
|
||||
$elseif_clauses,
|
||||
$statements_analyzer,
|
||||
$elseif->cond,
|
||||
$assigned_in_conditional_var_ids,
|
||||
);
|
||||
|
||||
$elseif_context_clauses = [...$entry_clauses, ...$elseif_clauses];
|
||||
|
||||
if ($elseif_context->reconciled_expression_clauses) {
|
||||
$reconciled_expression_clauses = $elseif_context->reconciled_expression_clauses;
|
||||
|
||||
$elseif_context_clauses = array_values(
|
||||
array_filter(
|
||||
$elseif_context_clauses,
|
||||
static fn(Clause $c): bool => !in_array($c->hash, $reconciled_expression_clauses, true)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$elseif_context->clauses = Algebra::simplifyCNF($elseif_context_clauses);
|
||||
|
||||
$active_elseif_types = [];
|
||||
|
||||
try {
|
||||
if (array_filter(
|
||||
$entry_clauses,
|
||||
static fn(Clause $clause): bool => (bool) $clause->possibilities,
|
||||
)) {
|
||||
$omit_keys = array_reduce(
|
||||
$entry_clauses,
|
||||
/**
|
||||
* @param array<string> $carry
|
||||
* @return array<string>
|
||||
*/
|
||||
static fn(array $carry, Clause $clause): array
|
||||
=> array_merge($carry, array_keys($clause->possibilities)),
|
||||
[],
|
||||
);
|
||||
|
||||
$omit_keys = array_combine($omit_keys, $omit_keys);
|
||||
$omit_keys = array_diff_key($omit_keys, Algebra::getTruthsFromFormula($entry_clauses));
|
||||
|
||||
$cond_referenced_var_ids = array_diff_key(
|
||||
$cond_referenced_var_ids,
|
||||
$omit_keys,
|
||||
);
|
||||
}
|
||||
$reconcilable_elseif_types = Algebra::getTruthsFromFormula(
|
||||
$elseif_context->clauses,
|
||||
spl_object_id($elseif->cond),
|
||||
$cond_referenced_var_ids,
|
||||
$active_elseif_types,
|
||||
);
|
||||
$negated_elseif_types = Algebra::getTruthsFromFormula(
|
||||
Algebra::negateFormula($elseif_clauses),
|
||||
);
|
||||
} catch (ComplicatedExpressionException $e) {
|
||||
$reconcilable_elseif_types = [];
|
||||
$negated_elseif_types = [];
|
||||
}
|
||||
|
||||
$all_negated_vars = array_unique(
|
||||
[...array_keys($negated_elseif_types), ...array_keys($if_scope->negated_types)],
|
||||
);
|
||||
|
||||
foreach ($all_negated_vars as $var_id) {
|
||||
if (isset($negated_elseif_types[$var_id])) {
|
||||
if (isset($if_scope->negated_types[$var_id])) {
|
||||
$if_scope->negated_types[$var_id] = [
|
||||
...$if_scope->negated_types[$var_id],
|
||||
...$negated_elseif_types[$var_id],
|
||||
];
|
||||
} else {
|
||||
$if_scope->negated_types[$var_id] = $negated_elseif_types[$var_id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$newly_reconciled_var_ids = [];
|
||||
|
||||
// if the elseif has an || in the conditional, we cannot easily reason about it
|
||||
if ($reconcilable_elseif_types) {
|
||||
[$elseif_context->vars_in_scope, $elseif_context->references_in_scope] = Reconciler::reconcileKeyedTypes(
|
||||
$reconcilable_elseif_types,
|
||||
$active_elseif_types,
|
||||
$elseif_context->vars_in_scope,
|
||||
$elseif_context->references_in_scope,
|
||||
$newly_reconciled_var_ids,
|
||||
$cond_referenced_var_ids,
|
||||
$statements_analyzer,
|
||||
$statements_analyzer->getTemplateTypeMap() ?: [],
|
||||
$elseif_context->inside_loop,
|
||||
new CodeLocation(
|
||||
$statements_analyzer->getSource(),
|
||||
$elseif->cond instanceof PhpParser\Node\Expr\BooleanNot
|
||||
? $elseif->cond->expr
|
||||
: $elseif->cond,
|
||||
$outer_context->include_location,
|
||||
),
|
||||
);
|
||||
|
||||
if ($newly_reconciled_var_ids) {
|
||||
$elseif_context->clauses = Context::removeReconciledClauses(
|
||||
$elseif_context->clauses,
|
||||
$newly_reconciled_var_ids,
|
||||
)[0];
|
||||
|
||||
foreach ($newly_reconciled_var_ids as $changed_var_id => $_) {
|
||||
foreach ($elseif_context->vars_in_scope as $var_id => $_) {
|
||||
if (preg_match('/' . preg_quote($changed_var_id, '/') . '[\]\[\-]/', $var_id)
|
||||
&& !array_key_exists($var_id, $newly_reconciled_var_ids)
|
||||
&& !array_key_exists($var_id, $cond_referenced_var_ids)
|
||||
) {
|
||||
$elseif_context->removePossibleReference($var_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pre_stmts_assigned_var_ids = $elseif_context->assigned_var_ids;
|
||||
$elseif_context->assigned_var_ids = [];
|
||||
$pre_stmts_possibly_assigned_var_ids = $elseif_context->possibly_assigned_var_ids;
|
||||
$elseif_context->possibly_assigned_var_ids = [];
|
||||
|
||||
if ($statements_analyzer->analyze(
|
||||
$elseif->stmts,
|
||||
$elseif_context,
|
||||
) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($elseif_context->parent_remove_vars as $var_id => $_) {
|
||||
$outer_context->removeVarFromConflictingClauses($var_id);
|
||||
}
|
||||
|
||||
/** @var array<string, int> */
|
||||
$new_stmts_assigned_var_ids = $elseif_context->assigned_var_ids;
|
||||
$elseif_context->assigned_var_ids = $pre_stmts_assigned_var_ids + $new_stmts_assigned_var_ids;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
$new_stmts_possibly_assigned_var_ids = $elseif_context->possibly_assigned_var_ids;
|
||||
$elseif_context->possibly_assigned_var_ids =
|
||||
$pre_stmts_possibly_assigned_var_ids + $new_stmts_possibly_assigned_var_ids;
|
||||
|
||||
foreach ($elseif_context->byref_constraints as $var_id => $byref_constraint) {
|
||||
if (isset($outer_context->byref_constraints[$var_id])
|
||||
&& ($outer_constraint_type = $outer_context->byref_constraints[$var_id]->type)
|
||||
&& $byref_constraint->type
|
||||
&& !UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$byref_constraint->type,
|
||||
$outer_constraint_type,
|
||||
)
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new ConflictingReferenceConstraint(
|
||||
'There is more than one pass-by-reference constraint on ' . $var_id,
|
||||
new CodeLocation($statements_analyzer, $elseif, $outer_context->include_location, true),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
$outer_context->byref_constraints[$var_id] = $byref_constraint;
|
||||
}
|
||||
}
|
||||
|
||||
$final_actions = ScopeAnalyzer::getControlActions(
|
||||
$elseif->stmts,
|
||||
$statements_analyzer->node_data,
|
||||
[],
|
||||
);
|
||||
// has a return/throw at end
|
||||
$has_ending_statements = $final_actions === [ScopeAnalyzer::ACTION_END];
|
||||
$has_leaving_statements = $has_ending_statements
|
||||
|| (count($final_actions) && !in_array(ScopeAnalyzer::ACTION_NONE, $final_actions, true));
|
||||
|
||||
$has_break_statement = $final_actions === [ScopeAnalyzer::ACTION_BREAK];
|
||||
$has_continue_statement = $final_actions === [ScopeAnalyzer::ACTION_CONTINUE];
|
||||
|
||||
$if_scope->final_actions = array_merge($final_actions, $if_scope->final_actions);
|
||||
|
||||
// update the parent context as necessary
|
||||
if (!$has_leaving_statements) {
|
||||
IfAnalyzer::updateIfScope(
|
||||
$codebase,
|
||||
$if_scope,
|
||||
$elseif_context,
|
||||
$outer_context,
|
||||
array_merge($new_stmts_assigned_var_ids, $assigned_in_conditional_var_ids),
|
||||
$new_stmts_possibly_assigned_var_ids,
|
||||
$newly_reconciled_var_ids,
|
||||
);
|
||||
|
||||
$reasonable_clause_count = count($if_scope->reasonable_clauses);
|
||||
|
||||
if ($reasonable_clause_count && $reasonable_clause_count < 20_000 && $elseif_clauses) {
|
||||
$if_scope->reasonable_clauses = Algebra::combineOredClauses(
|
||||
$if_scope->reasonable_clauses,
|
||||
$elseif_clauses,
|
||||
$elseif_cond_id,
|
||||
);
|
||||
} else {
|
||||
$if_scope->reasonable_clauses = [];
|
||||
}
|
||||
} else {
|
||||
$if_scope->reasonable_clauses = [];
|
||||
}
|
||||
|
||||
if ($negated_elseif_types) {
|
||||
if ($has_leaving_statements) {
|
||||
$newly_reconciled_var_ids = [];
|
||||
|
||||
$implied_outer_context = clone $elseif_context;
|
||||
[$implied_outer_context->vars_in_scope, $implied_outer_context->references_in_scope] =
|
||||
Reconciler::reconcileKeyedTypes(
|
||||
$negated_elseif_types,
|
||||
[],
|
||||
$pre_conditional_context->vars_in_scope,
|
||||
$pre_conditional_context->references_in_scope,
|
||||
$newly_reconciled_var_ids,
|
||||
[],
|
||||
$statements_analyzer,
|
||||
$statements_analyzer->getTemplateTypeMap() ?: [],
|
||||
$elseif_context->inside_loop,
|
||||
new CodeLocation($statements_analyzer->getSource(), $elseif, $outer_context->include_location),
|
||||
);
|
||||
|
||||
$updated_vars = [];
|
||||
|
||||
$outer_context->update(
|
||||
$elseif_context,
|
||||
$implied_outer_context,
|
||||
false,
|
||||
array_keys($negated_elseif_types),
|
||||
$updated_vars,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$has_ending_statements) {
|
||||
$vars_possibly_in_scope = array_diff_key(
|
||||
$elseif_context->vars_possibly_in_scope,
|
||||
$outer_context->vars_possibly_in_scope,
|
||||
);
|
||||
|
||||
$possibly_assigned_var_ids = $new_stmts_possibly_assigned_var_ids;
|
||||
|
||||
if ($has_leaving_statements && $elseif_context->loop_scope) {
|
||||
if (!$has_continue_statement && !$has_break_statement) {
|
||||
$if_scope->new_vars_possibly_in_scope = array_merge(
|
||||
$vars_possibly_in_scope,
|
||||
$if_scope->new_vars_possibly_in_scope,
|
||||
);
|
||||
$if_scope->possibly_assigned_var_ids = array_merge(
|
||||
$possibly_assigned_var_ids,
|
||||
$if_scope->possibly_assigned_var_ids,
|
||||
);
|
||||
}
|
||||
|
||||
$elseif_context->loop_scope->vars_possibly_in_scope = array_merge(
|
||||
$vars_possibly_in_scope,
|
||||
$elseif_context->loop_scope->vars_possibly_in_scope,
|
||||
);
|
||||
} elseif (!$has_leaving_statements) {
|
||||
$if_scope->new_vars_possibly_in_scope = array_merge(
|
||||
$vars_possibly_in_scope,
|
||||
$if_scope->new_vars_possibly_in_scope,
|
||||
);
|
||||
$if_scope->possibly_assigned_var_ids = array_merge(
|
||||
$possibly_assigned_var_ids,
|
||||
$if_scope->possibly_assigned_var_ids,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($outer_context->collect_exceptions) {
|
||||
$outer_context->mergeExceptions($elseif_context);
|
||||
}
|
||||
|
||||
try {
|
||||
$if_scope->negated_clauses = Algebra::simplifyCNF(
|
||||
[...$if_scope->negated_clauses, ...Algebra::negateFormula($elseif_clauses)],
|
||||
);
|
||||
} catch (ComplicatedExpressionException $e) {
|
||||
$if_scope->negated_clauses = [];
|
||||
}
|
||||
|
||||
// Track references set in the elseif to make sure they aren't reused later
|
||||
$outer_context->updateReferencesPossiblyFromConfusingScope(
|
||||
$elseif_context,
|
||||
$statements_analyzer,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
516
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php
vendored
Normal file
516
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/IfElse/IfAnalyzer.php
vendored
Normal file
@@ -0,0 +1,516 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Block\IfElse;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Clause;
|
||||
use Psalm\Internal\Scope\IfConditionalScope;
|
||||
use Psalm\Internal\Scope\IfScope;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Issue\ConflictingReferenceConstraint;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Node\Expr\BinaryOp\VirtualBooleanOr;
|
||||
use Psalm\Node\Expr\VirtualBooleanNot;
|
||||
use Psalm\Node\Expr\VirtualFuncCall;
|
||||
use Psalm\Node\Name\VirtualFullyQualified;
|
||||
use Psalm\Node\VirtualArg;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Reconciler;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_combine;
|
||||
use function array_diff_key;
|
||||
use function array_filter;
|
||||
use function array_intersect;
|
||||
use function array_intersect_key;
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_reduce;
|
||||
use function array_unique;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
use function spl_object_id;
|
||||
use function strpos;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class IfAnalyzer
|
||||
{
|
||||
/**
|
||||
* @param array<string, Union> $pre_assignment_else_redefined_vars
|
||||
* @return false|null
|
||||
*/
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Stmt\If_ $stmt,
|
||||
IfScope $if_scope,
|
||||
IfConditionalScope $if_conditional_scope,
|
||||
Context $if_context,
|
||||
Context $outer_context,
|
||||
array $pre_assignment_else_redefined_vars
|
||||
): ?bool {
|
||||
$cond_referenced_var_ids = $if_conditional_scope->cond_referenced_var_ids;
|
||||
|
||||
$active_if_types = [];
|
||||
|
||||
$reconcilable_if_types = Algebra::getTruthsFromFormula(
|
||||
$if_context->clauses,
|
||||
spl_object_id($stmt->cond),
|
||||
$cond_referenced_var_ids,
|
||||
$active_if_types,
|
||||
);
|
||||
|
||||
if (array_filter(
|
||||
$outer_context->clauses,
|
||||
static fn(Clause $clause): bool => (bool) $clause->possibilities,
|
||||
)) {
|
||||
$omit_keys = array_reduce(
|
||||
$outer_context->clauses,
|
||||
/**
|
||||
* @param array<string> $carry
|
||||
* @return array<string>
|
||||
*/
|
||||
static fn(array $carry, Clause $clause): array
|
||||
=> array_merge($carry, array_keys($clause->possibilities)),
|
||||
[],
|
||||
);
|
||||
|
||||
$omit_keys = array_combine($omit_keys, $omit_keys);
|
||||
$omit_keys = array_diff_key($omit_keys, Algebra::getTruthsFromFormula($outer_context->clauses));
|
||||
|
||||
$cond_referenced_var_ids = array_diff_key(
|
||||
$cond_referenced_var_ids,
|
||||
$omit_keys,
|
||||
);
|
||||
}
|
||||
|
||||
// if the if has an || in the conditional, we cannot easily reason about it
|
||||
if ($reconcilable_if_types) {
|
||||
$changed_var_ids = [];
|
||||
|
||||
[$if_context->vars_in_scope, $if_context->references_in_scope] = Reconciler::reconcileKeyedTypes(
|
||||
$reconcilable_if_types,
|
||||
$active_if_types,
|
||||
$if_context->vars_in_scope,
|
||||
$if_context->references_in_scope,
|
||||
$changed_var_ids,
|
||||
$cond_referenced_var_ids,
|
||||
$statements_analyzer,
|
||||
$statements_analyzer->getTemplateTypeMap() ?: [],
|
||||
$if_context->inside_loop,
|
||||
$outer_context->check_variables
|
||||
? new CodeLocation(
|
||||
$statements_analyzer->getSource(),
|
||||
$stmt->cond instanceof PhpParser\Node\Expr\BooleanNot
|
||||
? $stmt->cond->expr
|
||||
: $stmt->cond,
|
||||
$outer_context->include_location,
|
||||
) : null,
|
||||
);
|
||||
|
||||
foreach ($reconcilable_if_types as $var_id => $_) {
|
||||
$if_context->vars_possibly_in_scope[$var_id] = true;
|
||||
}
|
||||
|
||||
if ($changed_var_ids) {
|
||||
$if_context->clauses = Context::removeReconciledClauses($if_context->clauses, $changed_var_ids)[0];
|
||||
|
||||
foreach ($changed_var_ids as $changed_var_id => $_) {
|
||||
foreach ($if_context->vars_in_scope as $var_id => $_) {
|
||||
if (preg_match('/' . preg_quote($changed_var_id, '/') . '[\]\[\-]/', $var_id)
|
||||
&& !array_key_exists($var_id, $changed_var_ids)
|
||||
&& !array_key_exists($var_id, $cond_referenced_var_ids)
|
||||
) {
|
||||
$if_context->removePossibleReference($var_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$if_scope->if_cond_changed_var_ids = $changed_var_ids;
|
||||
}
|
||||
|
||||
$if_context->reconciled_expression_clauses = [];
|
||||
|
||||
$outer_context->vars_possibly_in_scope = array_merge(
|
||||
$if_context->vars_possibly_in_scope,
|
||||
$outer_context->vars_possibly_in_scope,
|
||||
);
|
||||
|
||||
$old_if_context = clone $if_context;
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$assigned_var_ids = $if_context->assigned_var_ids;
|
||||
$possibly_assigned_var_ids = $if_context->possibly_assigned_var_ids;
|
||||
$if_context->assigned_var_ids = [];
|
||||
$if_context->possibly_assigned_var_ids = [];
|
||||
|
||||
if ($statements_analyzer->analyze(
|
||||
$stmt->stmts,
|
||||
$if_context,
|
||||
) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($if_context->parent_remove_vars as $var_id => $_) {
|
||||
$outer_context->removeVarFromConflictingClauses($var_id);
|
||||
}
|
||||
|
||||
$if_scope->if_actions = $final_actions = ScopeAnalyzer::getControlActions(
|
||||
$stmt->stmts,
|
||||
$statements_analyzer->node_data,
|
||||
[],
|
||||
);
|
||||
|
||||
$has_ending_statements = $final_actions === [ScopeAnalyzer::ACTION_END];
|
||||
|
||||
$has_leaving_statements = $has_ending_statements
|
||||
|| (count($final_actions) && !in_array(ScopeAnalyzer::ACTION_NONE, $final_actions, true));
|
||||
|
||||
$has_break_statement = $final_actions === [ScopeAnalyzer::ACTION_BREAK];
|
||||
$has_continue_statement = $final_actions === [ScopeAnalyzer::ACTION_CONTINUE];
|
||||
|
||||
$if_scope->if_actions = $final_actions;
|
||||
$if_scope->final_actions = $final_actions;
|
||||
|
||||
/** @var array<string, int> */
|
||||
$new_assigned_var_ids = $if_context->assigned_var_ids;
|
||||
/** @var array<string, bool> */
|
||||
$new_possibly_assigned_var_ids = $if_context->possibly_assigned_var_ids;
|
||||
|
||||
$if_context->assigned_var_ids = array_merge($assigned_var_ids, $new_assigned_var_ids);
|
||||
$if_context->possibly_assigned_var_ids = array_merge(
|
||||
$possibly_assigned_var_ids,
|
||||
$new_possibly_assigned_var_ids,
|
||||
);
|
||||
|
||||
foreach ($if_context->byref_constraints as $var_id => $byref_constraint) {
|
||||
if (isset($outer_context->byref_constraints[$var_id])
|
||||
&& $byref_constraint->type
|
||||
&& ($outer_constraint_type = $outer_context->byref_constraints[$var_id]->type)
|
||||
&& !UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$byref_constraint->type,
|
||||
$outer_constraint_type,
|
||||
)
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new ConflictingReferenceConstraint(
|
||||
'There is more than one pass-by-reference constraint on ' . $var_id
|
||||
. ' between ' . $byref_constraint->type->getId()
|
||||
. ' and ' . $outer_constraint_type->getId(),
|
||||
new CodeLocation($statements_analyzer, $stmt, $outer_context->include_location, true),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
$outer_context->byref_constraints[$var_id] = $byref_constraint;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$has_leaving_statements) {
|
||||
self::updateIfScope(
|
||||
$codebase,
|
||||
$if_scope,
|
||||
$if_context,
|
||||
$outer_context,
|
||||
$new_assigned_var_ids,
|
||||
$new_possibly_assigned_var_ids,
|
||||
$if_scope->if_cond_changed_var_ids,
|
||||
);
|
||||
|
||||
if ($if_scope->reasonable_clauses) {
|
||||
// remove all reasonable clauses that would be negated by the if stmts
|
||||
foreach ($new_assigned_var_ids as $var_id => $_) {
|
||||
$if_scope->reasonable_clauses = Context::filterClauses(
|
||||
$var_id,
|
||||
$if_scope->reasonable_clauses,
|
||||
$if_context->vars_in_scope[$var_id] ?? null,
|
||||
$statements_analyzer,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!$has_break_statement) {
|
||||
$if_scope->reasonable_clauses = [];
|
||||
|
||||
// If we're assigning inside
|
||||
if ($if_conditional_scope->assigned_in_conditional_var_ids
|
||||
&& $if_scope->post_leaving_if_context
|
||||
) {
|
||||
self::addConditionallyAssignedVarsToContext(
|
||||
$statements_analyzer,
|
||||
$stmt->cond,
|
||||
$if_scope->post_leaving_if_context,
|
||||
$outer_context,
|
||||
$if_conditional_scope->assigned_in_conditional_var_ids,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// update the parent context as necessary, but only if we can safely reason about type negation.
|
||||
// We only update vars that changed both at the start of the if block and then again by an assignment
|
||||
// in the if statement.
|
||||
if ($if_scope->negated_types) {
|
||||
$vars_to_update = array_intersect(
|
||||
array_keys($pre_assignment_else_redefined_vars),
|
||||
array_keys($if_scope->negated_types),
|
||||
);
|
||||
|
||||
$extra_vars_to_update = [];
|
||||
|
||||
// if there's an object-like array in there, we also need to update the root array variable
|
||||
foreach ($vars_to_update as $var_id) {
|
||||
$bracked_pos = strpos($var_id, '[');
|
||||
if ($bracked_pos !== false) {
|
||||
$extra_vars_to_update[] = substr($var_id, 0, $bracked_pos);
|
||||
}
|
||||
}
|
||||
|
||||
if ($extra_vars_to_update) {
|
||||
$vars_to_update = array_unique(array_merge($extra_vars_to_update, $vars_to_update));
|
||||
}
|
||||
|
||||
$outer_context->update(
|
||||
$old_if_context,
|
||||
$if_context,
|
||||
$has_leaving_statements,
|
||||
$vars_to_update,
|
||||
$if_scope->updated_vars,
|
||||
);
|
||||
}
|
||||
|
||||
if (!$has_ending_statements) {
|
||||
$vars_possibly_in_scope = array_diff_key(
|
||||
$if_context->vars_possibly_in_scope,
|
||||
$outer_context->vars_possibly_in_scope,
|
||||
);
|
||||
|
||||
if ($if_context->loop_scope) {
|
||||
if (!$has_continue_statement && !$has_break_statement) {
|
||||
$if_scope->new_vars_possibly_in_scope = $vars_possibly_in_scope;
|
||||
}
|
||||
|
||||
$if_context->loop_scope->vars_possibly_in_scope = array_merge(
|
||||
$vars_possibly_in_scope,
|
||||
$if_context->loop_scope->vars_possibly_in_scope,
|
||||
);
|
||||
} elseif (!$has_leaving_statements) {
|
||||
$if_scope->new_vars_possibly_in_scope = $vars_possibly_in_scope;
|
||||
}
|
||||
}
|
||||
|
||||
if ($outer_context->collect_exceptions) {
|
||||
$outer_context->mergeExceptions($if_context);
|
||||
}
|
||||
|
||||
// Track references set in the if to make sure they aren't reused later
|
||||
$outer_context->updateReferencesPossiblyFromConfusingScope(
|
||||
$if_context,
|
||||
$statements_analyzer,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $assigned_in_conditional_var_ids
|
||||
*/
|
||||
public static function addConditionallyAssignedVarsToContext(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr $cond,
|
||||
Context $post_leaving_if_context,
|
||||
Context $post_if_context,
|
||||
array $assigned_in_conditional_var_ids
|
||||
): void {
|
||||
// this filters out coercions to expected types in ArgumentAnalyzer
|
||||
$assigned_in_conditional_var_ids = array_filter($assigned_in_conditional_var_ids);
|
||||
|
||||
if (!$assigned_in_conditional_var_ids) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exprs = self::getDefinitelyEvaluatedOredExpressions($cond);
|
||||
|
||||
// if there was no assignment in the first expression it's safe to proceed
|
||||
$old_node_data = $statements_analyzer->node_data;
|
||||
$statements_analyzer->node_data = clone $old_node_data;
|
||||
|
||||
IssueBuffer::startRecording();
|
||||
|
||||
foreach ($exprs as $expr) {
|
||||
if ($expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) {
|
||||
$fake_not = new VirtualBooleanOr(
|
||||
self::negateExpr($expr->left),
|
||||
self::negateExpr($expr->right),
|
||||
$expr->getAttributes(),
|
||||
);
|
||||
} else {
|
||||
$fake_not = self::negateExpr($expr);
|
||||
}
|
||||
|
||||
$fake_negated_expr = new VirtualFuncCall(
|
||||
new VirtualFullyQualified('assert'),
|
||||
[new VirtualArg(
|
||||
$fake_not,
|
||||
false,
|
||||
false,
|
||||
$expr->getAttributes(),
|
||||
)],
|
||||
$expr->getAttributes(),
|
||||
);
|
||||
|
||||
$post_leaving_if_context->inside_negation = !$post_leaving_if_context->inside_negation;
|
||||
|
||||
ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$fake_negated_expr,
|
||||
$post_leaving_if_context,
|
||||
);
|
||||
|
||||
$post_leaving_if_context->inside_negation = !$post_leaving_if_context->inside_negation;
|
||||
}
|
||||
|
||||
IssueBuffer::clearRecordingLevel();
|
||||
IssueBuffer::stopRecording();
|
||||
|
||||
$statements_analyzer->node_data = $old_node_data;
|
||||
|
||||
foreach ($assigned_in_conditional_var_ids as $var_id => $_) {
|
||||
if (isset($post_leaving_if_context->vars_in_scope[$var_id])) {
|
||||
$post_if_context->vars_in_scope[$var_id] = $post_leaving_if_context->vars_in_scope[$var_id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all expressions inside an ored expression
|
||||
*
|
||||
* @return non-empty-list<PhpParser\Node\Expr>
|
||||
*/
|
||||
private static function getDefinitelyEvaluatedOredExpressions(PhpParser\Node\Expr $stmt): array
|
||||
{
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalXor
|
||||
) {
|
||||
return [
|
||||
...self::getDefinitelyEvaluatedOredExpressions($stmt->left),
|
||||
...self::getDefinitelyEvaluatedOredExpressions($stmt->right),
|
||||
];
|
||||
}
|
||||
|
||||
return [$stmt];
|
||||
}
|
||||
|
||||
private static function negateExpr(PhpParser\Node\Expr $expr): PhpParser\Node\Expr
|
||||
{
|
||||
if ($expr instanceof PhpParser\Node\Expr\BooleanNot) {
|
||||
return $expr->expr;
|
||||
}
|
||||
|
||||
return new VirtualBooleanNot($expr, $expr->getAttributes());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $assigned_var_ids
|
||||
* @param array<string, bool> $possibly_assigned_var_ids
|
||||
* @param array<string, bool> $newly_reconciled_var_ids
|
||||
*/
|
||||
public static function updateIfScope(
|
||||
Codebase $codebase,
|
||||
IfScope $if_scope,
|
||||
Context $if_context,
|
||||
Context $outer_context,
|
||||
array $assigned_var_ids,
|
||||
array $possibly_assigned_var_ids,
|
||||
array $newly_reconciled_var_ids,
|
||||
bool $update_new_vars = true
|
||||
): void {
|
||||
$redefined_vars = $if_context->getRedefinedVars($outer_context->vars_in_scope);
|
||||
|
||||
if ($if_scope->new_vars === null) {
|
||||
if ($update_new_vars) {
|
||||
$if_scope->new_vars = array_diff_key($if_context->vars_in_scope, $outer_context->vars_in_scope);
|
||||
}
|
||||
} else {
|
||||
foreach ($if_scope->new_vars as $new_var => $type) {
|
||||
if (!$if_context->hasVariable($new_var)) {
|
||||
unset($if_scope->new_vars[$new_var]);
|
||||
} else {
|
||||
$if_scope->new_vars[$new_var] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$if_context->vars_in_scope[$new_var],
|
||||
$codebase,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$possibly_redefined_vars = $redefined_vars;
|
||||
|
||||
foreach ($possibly_redefined_vars as $var_id => $_) {
|
||||
if (!isset($possibly_assigned_var_ids[$var_id])
|
||||
&& isset($newly_reconciled_var_ids[$var_id])
|
||||
) {
|
||||
unset($possibly_redefined_vars[$var_id]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($if_scope->assigned_var_ids === null) {
|
||||
$if_scope->assigned_var_ids = $assigned_var_ids;
|
||||
} else {
|
||||
$if_scope->assigned_var_ids = array_intersect_key($assigned_var_ids, $if_scope->assigned_var_ids);
|
||||
}
|
||||
|
||||
$if_scope->possibly_assigned_var_ids += $possibly_assigned_var_ids;
|
||||
|
||||
if ($if_scope->redefined_vars === null) {
|
||||
$if_scope->redefined_vars = $redefined_vars;
|
||||
$if_scope->possibly_redefined_vars = $possibly_redefined_vars;
|
||||
} else {
|
||||
foreach ($if_scope->redefined_vars as $redefined_var => $type) {
|
||||
if (!isset($redefined_vars[$redefined_var])) {
|
||||
unset($if_scope->redefined_vars[$redefined_var]);
|
||||
} else {
|
||||
$if_scope->redefined_vars[$redefined_var] = Type::combineUnionTypes(
|
||||
$redefined_vars[$redefined_var],
|
||||
$type,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
if (isset($outer_context->vars_in_scope[$redefined_var])
|
||||
&& $if_scope->redefined_vars[$redefined_var]->equals(
|
||||
$outer_context->vars_in_scope[$redefined_var],
|
||||
)
|
||||
) {
|
||||
unset($if_scope->redefined_vars[$redefined_var]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($possibly_redefined_vars as $var => $type) {
|
||||
$if_scope->possibly_redefined_vars[$var] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$if_scope->possibly_redefined_vars[$var] ?? null,
|
||||
$codebase,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
450
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php
vendored
Normal file
450
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/IfElseAnalyzer.php
vendored
Normal file
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Block;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Exception\ComplicatedExpressionException;
|
||||
use Psalm\Exception\ScopeAnalysisException;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Algebra\FormulaGenerator;
|
||||
use Psalm\Internal\Analyzer\AlgebraAnalyzer;
|
||||
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\IfElse\ElseAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\IfElse\ElseIfAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\IfElse\IfAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Analyzer\TraitAnalyzer;
|
||||
use Psalm\Internal\Clause;
|
||||
use Psalm\Internal\Scope\IfScope;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Node\Expr\VirtualBooleanNot;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
use function array_diff;
|
||||
use function array_filter;
|
||||
use function array_intersect_key;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_unique;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function preg_match;
|
||||
use function preg_quote;
|
||||
use function spl_object_id;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class IfElseAnalyzer
|
||||
{
|
||||
/**
|
||||
* System of type substitution and deletion
|
||||
*
|
||||
* for example
|
||||
*
|
||||
* x: A|null
|
||||
*
|
||||
* if (x)
|
||||
* (x: A)
|
||||
* x = B -- effects: remove A from the type of x, add B
|
||||
* else
|
||||
* (x: null)
|
||||
* x = C -- effects: remove null from the type of x, add C
|
||||
*
|
||||
*
|
||||
* x: A|null
|
||||
*
|
||||
* if (!x)
|
||||
* (x: null)
|
||||
* throw new Exception -- effects: remove null from the type of x
|
||||
*
|
||||
* @return null|false
|
||||
*/
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Stmt\If_ $stmt,
|
||||
Context $context
|
||||
): ?bool {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$if_scope = new IfScope();
|
||||
|
||||
// We need to clone the original context for later use if we're exiting in this if conditional
|
||||
if ($stmt->cond instanceof PhpParser\Node\Expr\BinaryOp
|
||||
|| ($stmt->cond instanceof PhpParser\Node\Expr\BooleanNot
|
||||
&& $stmt->cond->expr instanceof PhpParser\Node\Expr\BinaryOp)
|
||||
) {
|
||||
$final_actions = ScopeAnalyzer::getControlActions(
|
||||
$stmt->stmts,
|
||||
null,
|
||||
[],
|
||||
);
|
||||
|
||||
$has_leaving_statements = $final_actions === [ScopeAnalyzer::ACTION_END]
|
||||
|| (count($final_actions) && !in_array(ScopeAnalyzer::ACTION_NONE, $final_actions, true));
|
||||
|
||||
if ($has_leaving_statements) {
|
||||
$if_scope->post_leaving_if_context = clone $context;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$if_conditional_scope = IfConditionalAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt->cond,
|
||||
$context,
|
||||
$codebase,
|
||||
$if_scope,
|
||||
$context->branch_point ?: (int) $stmt->getAttribute('startFilePos'),
|
||||
);
|
||||
|
||||
// this is the context for stuff that happens within the `if` block
|
||||
$if_context = $if_conditional_scope->if_context;
|
||||
|
||||
// this is the context for stuff that happens after the `if` block
|
||||
$post_if_context = $if_conditional_scope->post_if_context;
|
||||
$assigned_in_conditional_var_ids = $if_conditional_scope->assigned_in_conditional_var_ids;
|
||||
} catch (ScopeAnalysisException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mixed_var_ids = [];
|
||||
|
||||
foreach ($if_context->vars_in_scope as $var_id => $type) {
|
||||
if ($type->isMixed() && isset($context->vars_in_scope[$var_id])) {
|
||||
$mixed_var_ids[] = $var_id;
|
||||
}
|
||||
}
|
||||
|
||||
$cond_object_id = spl_object_id($stmt->cond);
|
||||
|
||||
$if_clauses = FormulaGenerator::getFormula(
|
||||
$cond_object_id,
|
||||
$cond_object_id,
|
||||
$stmt->cond,
|
||||
$context->self,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
if (count($if_clauses) > 200) {
|
||||
$if_clauses = [];
|
||||
}
|
||||
|
||||
$if_clauses_handled = [];
|
||||
foreach ($if_clauses as $clause) {
|
||||
$keys = array_keys($clause->possibilities);
|
||||
|
||||
$mixed_var_ids = array_diff($mixed_var_ids, $keys);
|
||||
|
||||
foreach ($keys as $key) {
|
||||
foreach ($mixed_var_ids as $mixed_var_id) {
|
||||
if (preg_match('/^' . preg_quote($mixed_var_id, '/') . '(\[|-)/', $key)) {
|
||||
$clause = new Clause([], $cond_object_id, $cond_object_id, true);
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$if_clauses_handled[] = $clause;
|
||||
}
|
||||
|
||||
$if_clauses = $if_clauses_handled;
|
||||
|
||||
$entry_clauses = $context->clauses;
|
||||
|
||||
// this will see whether any of the clauses in set A conflict with the clauses in set B
|
||||
AlgebraAnalyzer::checkForParadox(
|
||||
$context->clauses,
|
||||
$if_clauses,
|
||||
$statements_analyzer,
|
||||
$stmt->cond,
|
||||
$assigned_in_conditional_var_ids,
|
||||
);
|
||||
|
||||
$if_clauses = Algebra::simplifyCNF($if_clauses);
|
||||
|
||||
$if_context_clauses = [...$entry_clauses, ...$if_clauses];
|
||||
|
||||
$if_context->clauses = $entry_clauses
|
||||
? Algebra::simplifyCNF($if_context_clauses)
|
||||
: $if_context_clauses;
|
||||
|
||||
if ($if_context->reconciled_expression_clauses) {
|
||||
$reconciled_expression_clauses = $if_context->reconciled_expression_clauses;
|
||||
|
||||
$if_context->clauses = array_values(
|
||||
array_filter(
|
||||
$if_context->clauses,
|
||||
static fn(Clause $c): bool => !in_array($c->hash, $reconciled_expression_clauses)
|
||||
),
|
||||
);
|
||||
|
||||
if (count($if_context->clauses) === 1
|
||||
&& $if_context->clauses[0]->wedge
|
||||
&& !$if_context->clauses[0]->possibilities
|
||||
) {
|
||||
$if_context->clauses = [];
|
||||
$if_context->reconciled_expression_clauses = [];
|
||||
}
|
||||
}
|
||||
|
||||
// define this before we alter local clauses after reconciliation
|
||||
$if_scope->reasonable_clauses = $if_context->clauses;
|
||||
|
||||
try {
|
||||
$if_scope->negated_clauses = Algebra::negateFormula($if_clauses);
|
||||
} catch (ComplicatedExpressionException $e) {
|
||||
try {
|
||||
$if_scope->negated_clauses = FormulaGenerator::getFormula(
|
||||
$cond_object_id,
|
||||
$cond_object_id,
|
||||
new VirtualBooleanNot($stmt->cond),
|
||||
$context->self,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
false,
|
||||
);
|
||||
} catch (ComplicatedExpressionException $e) {
|
||||
$if_scope->negated_clauses = [];
|
||||
}
|
||||
}
|
||||
|
||||
$if_scope->negated_types = Algebra::getTruthsFromFormula(
|
||||
Algebra::simplifyCNF(
|
||||
[...$context->clauses, ...$if_scope->negated_clauses],
|
||||
),
|
||||
);
|
||||
|
||||
$temp_else_context = clone $post_if_context;
|
||||
|
||||
$changed_var_ids = [];
|
||||
|
||||
if ($if_scope->negated_types) {
|
||||
[$temp_else_context->vars_in_scope, $temp_else_context->references_in_scope] =
|
||||
Reconciler::reconcileKeyedTypes(
|
||||
$if_scope->negated_types,
|
||||
[],
|
||||
$temp_else_context->vars_in_scope,
|
||||
$temp_else_context->references_in_scope,
|
||||
$changed_var_ids,
|
||||
[],
|
||||
$statements_analyzer,
|
||||
$statements_analyzer->getTemplateTypeMap() ?: [],
|
||||
$context->inside_loop,
|
||||
$context->check_variables
|
||||
? new CodeLocation(
|
||||
$statements_analyzer->getSource(),
|
||||
$stmt->cond instanceof PhpParser\Node\Expr\BooleanNot
|
||||
? $stmt->cond->expr
|
||||
: $stmt->cond,
|
||||
$context->include_location,
|
||||
) : null,
|
||||
);
|
||||
}
|
||||
|
||||
// we calculate the vars redefined in a hypothetical else statement to determine
|
||||
// which vars of the if we can safely change
|
||||
$pre_assignment_else_redefined_vars = array_intersect_key(
|
||||
$temp_else_context->getRedefinedVars($context->vars_in_scope, true),
|
||||
$changed_var_ids,
|
||||
);
|
||||
|
||||
// check the if
|
||||
if (IfAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$if_scope,
|
||||
$if_conditional_scope,
|
||||
$if_context,
|
||||
$context,
|
||||
$pre_assignment_else_redefined_vars,
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// this has to go on a separate line because the phar compactor messes with precedence
|
||||
$scope_to_clone = $if_scope->post_leaving_if_context ?? $post_if_context;
|
||||
$else_context = clone $scope_to_clone;
|
||||
$else_context->clauses = Algebra::simplifyCNF(
|
||||
[...$else_context->clauses, ...$if_scope->negated_clauses],
|
||||
);
|
||||
|
||||
// check the elseifs
|
||||
foreach ($stmt->elseifs as $elseif) {
|
||||
if (ElseIfAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$elseif,
|
||||
$if_scope,
|
||||
$else_context,
|
||||
$context,
|
||||
$codebase,
|
||||
$else_context->branch_point ?: (int) $stmt->getAttribute('startFilePos'),
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt->else) {
|
||||
if ($codebase->alter_code) {
|
||||
$else_context->branch_point =
|
||||
$else_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
|
||||
}
|
||||
}
|
||||
|
||||
if (ElseAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt->else,
|
||||
$if_scope,
|
||||
$else_context,
|
||||
$context,
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($if_scope->if_actions) && !in_array(ScopeAnalyzer::ACTION_NONE, $if_scope->if_actions, true)
|
||||
&& !$stmt->elseifs
|
||||
) {
|
||||
$context->clauses = $else_context->clauses;
|
||||
foreach ($else_context->vars_in_scope as $var_id => $type) {
|
||||
$context->vars_in_scope[$var_id] = $type;
|
||||
}
|
||||
|
||||
foreach ($pre_assignment_else_redefined_vars as $var_id => $reconciled_type) {
|
||||
$first_appearance = $statements_analyzer->getFirstAppearance($var_id);
|
||||
|
||||
if ($first_appearance
|
||||
&& isset($post_if_context->vars_in_scope[$var_id])
|
||||
&& $post_if_context->vars_in_scope[$var_id]->hasMixed()
|
||||
&& !$reconciled_type->hasMixed()
|
||||
) {
|
||||
if (!$post_if_context->collect_initializations
|
||||
&& !$post_if_context->collect_mutations
|
||||
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
|
||||
) {
|
||||
$parent_source = $statements_analyzer->getSource();
|
||||
|
||||
$functionlike_storage = $parent_source instanceof FunctionLikeAnalyzer
|
||||
? $parent_source->getFunctionLikeStorage($statements_analyzer)
|
||||
: null;
|
||||
|
||||
if (!$functionlike_storage
|
||||
|| (!$parent_source->getSource() instanceof TraitAnalyzer
|
||||
&& !isset($functionlike_storage->param_lookup[substr($var_id, 1)]))
|
||||
) {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
$codebase->analyzer->decrementMixedCount($statements_analyzer->getFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
IssueBuffer::remove(
|
||||
$statements_analyzer->getFilePath(),
|
||||
'MixedAssignment',
|
||||
$first_appearance->raw_file_start,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($context->loop_scope) {
|
||||
$context->loop_scope->final_actions = array_unique(
|
||||
array_merge(
|
||||
$context->loop_scope->final_actions,
|
||||
$if_scope->final_actions,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$context->vars_possibly_in_scope = array_merge(
|
||||
$context->vars_possibly_in_scope,
|
||||
$if_scope->new_vars_possibly_in_scope,
|
||||
);
|
||||
|
||||
$context->possibly_assigned_var_ids = array_merge(
|
||||
$context->possibly_assigned_var_ids,
|
||||
$if_scope->possibly_assigned_var_ids ?: [],
|
||||
);
|
||||
|
||||
// vars can only be defined/redefined if there was an else (defined in every block)
|
||||
$context->assigned_var_ids = array_merge(
|
||||
$context->assigned_var_ids,
|
||||
$if_scope->assigned_var_ids ?: [],
|
||||
);
|
||||
|
||||
if ($if_scope->new_vars) {
|
||||
foreach ($if_scope->new_vars as $var_id => &$type) {
|
||||
if (isset($context->vars_possibly_in_scope[$var_id])
|
||||
&& $statements_analyzer->data_flow_graph
|
||||
) {
|
||||
$type = $type->addParentNodes(
|
||||
$statements_analyzer->getParentNodesForPossiblyUndefinedVariable($var_id),
|
||||
);
|
||||
}
|
||||
|
||||
$context->vars_in_scope[$var_id] = $type;
|
||||
}
|
||||
unset($type);
|
||||
}
|
||||
|
||||
if ($if_scope->redefined_vars) {
|
||||
foreach ($if_scope->redefined_vars as $var_id => $type) {
|
||||
$context->vars_in_scope[$var_id] = $type;
|
||||
$if_scope->updated_vars[$var_id] = true;
|
||||
|
||||
if ($if_scope->reasonable_clauses) {
|
||||
$if_scope->reasonable_clauses = Context::filterClauses(
|
||||
$var_id,
|
||||
$if_scope->reasonable_clauses,
|
||||
$context->vars_in_scope[$var_id] ?? null,
|
||||
$statements_analyzer,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($if_scope->reasonable_clauses
|
||||
&& (count($if_scope->reasonable_clauses) > 1 || !$if_scope->reasonable_clauses[0]->wedge)
|
||||
) {
|
||||
$context->clauses = Algebra::simplifyCNF(
|
||||
[...$if_scope->reasonable_clauses, ...$context->clauses],
|
||||
);
|
||||
}
|
||||
|
||||
if ($if_scope->possibly_redefined_vars) {
|
||||
foreach ($if_scope->possibly_redefined_vars as $var_id => $type) {
|
||||
if (isset($context->vars_in_scope[$var_id])) {
|
||||
if (!$type->failed_reconciliation
|
||||
&& !isset($if_scope->updated_vars[$var_id])
|
||||
) {
|
||||
$combined_type = Type::combineUnionTypes(
|
||||
$context->vars_in_scope[$var_id],
|
||||
$type,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
if (!$combined_type->equals($context->vars_in_scope[$var_id])) {
|
||||
$context->removeDescendents($var_id, $combined_type);
|
||||
}
|
||||
|
||||
$context->vars_in_scope[$var_id] = $combined_type;
|
||||
} else {
|
||||
$context->vars_in_scope[$var_id] =
|
||||
$context->vars_in_scope[$var_id]->addParentNodes($type->parent_nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!in_array(ScopeAnalyzer::ACTION_NONE, $if_scope->final_actions, true)) {
|
||||
$context->has_returned = true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
670
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php
vendored
Normal file
670
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php
vendored
Normal file
@@ -0,0 +1,670 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Block;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Exception\ComplicatedExpressionException;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Algebra\FormulaGenerator;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Clause;
|
||||
use Psalm\Internal\PhpVisitor\AssignmentMapVisitor;
|
||||
use Psalm\Internal\PhpVisitor\NodeCleanerVisitor;
|
||||
use Psalm\Internal\Scope\LoopScope;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_unique;
|
||||
use function in_array;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class LoopAnalyzer
|
||||
{
|
||||
/**
|
||||
* Checks an array of statements in a loop
|
||||
*
|
||||
* @param list<PhpParser\Node\Stmt> $stmts
|
||||
* @param list<PhpParser\Node\Expr> $pre_conditions
|
||||
* @param PhpParser\Node\Expr[] $post_expressions
|
||||
* @return false|null
|
||||
*/
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
array $stmts,
|
||||
array $pre_conditions,
|
||||
array $post_expressions,
|
||||
LoopScope $loop_scope,
|
||||
Context &$continue_context = null,
|
||||
bool $is_do = false,
|
||||
bool $always_enters_loop = false
|
||||
): ?bool {
|
||||
$traverser = new PhpParser\NodeTraverser;
|
||||
|
||||
$loop_context = $loop_scope->loop_context;
|
||||
$loop_parent_context = $loop_scope->loop_parent_context;
|
||||
|
||||
$assignment_mapper = new AssignmentMapVisitor($loop_context->self);
|
||||
$traverser->addVisitor($assignment_mapper);
|
||||
|
||||
$traverser->traverse(array_merge($pre_conditions, $stmts, $post_expressions));
|
||||
|
||||
$assignment_map = $assignment_mapper->getAssignmentMap();
|
||||
|
||||
$assignment_depth = 0;
|
||||
|
||||
$always_assigned_before_loop_body_vars = [];
|
||||
|
||||
$pre_condition_clauses = [];
|
||||
|
||||
$original_protected_var_ids = $loop_parent_context->protected_var_ids;
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$inner_do_context = null;
|
||||
|
||||
if ($pre_conditions) {
|
||||
foreach ($pre_conditions as $i => $pre_condition) {
|
||||
$pre_condition_id = spl_object_id($pre_condition);
|
||||
|
||||
$pre_condition_clauses[$i] = FormulaGenerator::getFormula(
|
||||
$pre_condition_id,
|
||||
$pre_condition_id,
|
||||
$pre_condition,
|
||||
$loop_context->self,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$always_assigned_before_loop_body_vars = Context::getNewOrUpdatedVarIds(
|
||||
$loop_parent_context,
|
||||
$loop_context,
|
||||
);
|
||||
}
|
||||
|
||||
$final_actions = ScopeAnalyzer::getControlActions(
|
||||
$stmts,
|
||||
$statements_analyzer->node_data,
|
||||
[],
|
||||
);
|
||||
|
||||
$does_always_break = $final_actions === [ScopeAnalyzer::ACTION_BREAK];
|
||||
|
||||
if ($assignment_map) {
|
||||
$first_var_id = array_keys($assignment_map)[0];
|
||||
|
||||
$assignment_depth = self::getAssignmentMapDepth($first_var_id, $assignment_map);
|
||||
}
|
||||
|
||||
if ($assignment_depth === 0 || $does_always_break) {
|
||||
$continue_context = clone $loop_context;
|
||||
|
||||
foreach ($continue_context->vars_in_scope as $context_var_id => $context_type) {
|
||||
$continue_context->vars_in_scope[$context_var_id] = $context_type;
|
||||
}
|
||||
|
||||
$continue_context->loop_scope = $loop_scope;
|
||||
|
||||
foreach ($pre_conditions as $condition_offset => $pre_condition) {
|
||||
self::applyPreConditionToLoopContext(
|
||||
$statements_analyzer,
|
||||
$pre_condition,
|
||||
$pre_condition_clauses[$condition_offset],
|
||||
$continue_context,
|
||||
$loop_parent_context,
|
||||
$is_do,
|
||||
);
|
||||
}
|
||||
|
||||
$continue_context->protected_var_ids = $loop_scope->protected_var_ids;
|
||||
|
||||
$statements_analyzer->analyze($stmts, $continue_context);
|
||||
self::updateLoopScopeContexts($loop_scope, $loop_context, $continue_context, $loop_parent_context);
|
||||
|
||||
foreach ($post_expressions as $post_expression) {
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$post_expression,
|
||||
$loop_context,
|
||||
) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$loop_parent_context->vars_possibly_in_scope = array_merge(
|
||||
$continue_context->vars_possibly_in_scope,
|
||||
$loop_parent_context->vars_possibly_in_scope,
|
||||
);
|
||||
} else {
|
||||
$original_parent_context = clone $loop_parent_context;
|
||||
|
||||
$analyzer = $statements_analyzer->getCodebase()->analyzer;
|
||||
|
||||
$original_mixed_counts = $analyzer->getMixedCountsForFile($statements_analyzer->getFilePath());
|
||||
|
||||
// record all the vars that existed before we did the first pass through the loop
|
||||
$pre_loop_context = clone $loop_context;
|
||||
|
||||
IssueBuffer::startRecording();
|
||||
|
||||
if (!$is_do) {
|
||||
foreach ($pre_conditions as $condition_offset => $pre_condition) {
|
||||
self::applyPreConditionToLoopContext(
|
||||
$statements_analyzer,
|
||||
$pre_condition,
|
||||
$pre_condition_clauses[$condition_offset],
|
||||
$loop_context,
|
||||
$loop_parent_context,
|
||||
$is_do,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$continue_context = clone $loop_context;
|
||||
|
||||
$continue_context->loop_scope = $loop_scope;
|
||||
|
||||
$continue_context->protected_var_ids = $loop_scope->protected_var_ids;
|
||||
|
||||
$statements_analyzer->analyze($stmts, $continue_context);
|
||||
|
||||
self::updateLoopScopeContexts($loop_scope, $loop_context, $continue_context, $original_parent_context);
|
||||
|
||||
$continue_context->protected_var_ids = $original_protected_var_ids;
|
||||
|
||||
if ($is_do) {
|
||||
$inner_do_context = clone $continue_context;
|
||||
|
||||
foreach ($pre_conditions as $condition_offset => $pre_condition) {
|
||||
$always_assigned_before_loop_body_vars = [...self::applyPreConditionToLoopContext(
|
||||
$statements_analyzer,
|
||||
$pre_condition,
|
||||
$pre_condition_clauses[$condition_offset],
|
||||
$continue_context,
|
||||
$loop_parent_context,
|
||||
$is_do,
|
||||
), ...$always_assigned_before_loop_body_vars];
|
||||
}
|
||||
}
|
||||
|
||||
$always_assigned_before_loop_body_vars = array_unique($always_assigned_before_loop_body_vars);
|
||||
|
||||
foreach ($post_expressions as $post_expression) {
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $post_expression, $continue_context) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$recorded_issues = IssueBuffer::clearRecordingLevel();
|
||||
IssueBuffer::stopRecording();
|
||||
|
||||
for ($i = 0; $i < $assignment_depth; ++$i) {
|
||||
$vars_to_remove = [];
|
||||
|
||||
$loop_scope->iteration_count++;
|
||||
|
||||
$has_changes = false;
|
||||
|
||||
// reset the $continue_context to what it was before we started the analysis,
|
||||
// but union the types with what's in the loop scope
|
||||
|
||||
foreach ($continue_context->vars_in_scope as $var_id => $type) {
|
||||
if (in_array($var_id, $always_assigned_before_loop_body_vars, true)) {
|
||||
// set the vars to whatever the while/foreach loop expects them to be
|
||||
if (!isset($pre_loop_context->vars_in_scope[$var_id])
|
||||
|| !$type->equals($pre_loop_context->vars_in_scope[$var_id])
|
||||
) {
|
||||
$has_changes = true;
|
||||
}
|
||||
} elseif (isset($original_parent_context->vars_in_scope[$var_id])) {
|
||||
if (!$type->equals($original_parent_context->vars_in_scope[$var_id])) {
|
||||
$has_changes = true;
|
||||
|
||||
// widen the foreach context type with the initial context type
|
||||
$continue_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$continue_context->vars_in_scope[$var_id],
|
||||
$original_parent_context->vars_in_scope[$var_id],
|
||||
);
|
||||
|
||||
// if there's a change, invalidate related clauses
|
||||
$pre_loop_context->removeVarFromConflictingClauses($var_id);
|
||||
|
||||
$loop_parent_context->possibly_assigned_var_ids[$var_id] = true;
|
||||
}
|
||||
|
||||
if (isset($loop_context->vars_in_scope[$var_id])
|
||||
&& !$type->equals($loop_context->vars_in_scope[$var_id])
|
||||
) {
|
||||
$has_changes = true;
|
||||
|
||||
// widen the foreach context type with the initial context type
|
||||
$continue_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$continue_context->vars_in_scope[$var_id],
|
||||
$loop_context->vars_in_scope[$var_id],
|
||||
);
|
||||
|
||||
// if there's a change, invalidate related clauses
|
||||
$pre_loop_context->removeVarFromConflictingClauses($var_id);
|
||||
}
|
||||
} else {
|
||||
// give an opportunity to redeemed UndefinedVariable issues
|
||||
if ($recorded_issues) {
|
||||
$has_changes = true;
|
||||
}
|
||||
|
||||
// if we're in a do block we don't want to remove vars before evaluating
|
||||
// the where conditional
|
||||
if (!$is_do) {
|
||||
$vars_to_remove[] = $var_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$continue_context->has_returned = false;
|
||||
|
||||
$loop_parent_context->vars_possibly_in_scope = array_merge(
|
||||
$continue_context->vars_possibly_in_scope,
|
||||
$loop_parent_context->vars_possibly_in_scope,
|
||||
);
|
||||
|
||||
// if there are no changes to the types, no need to re-examine
|
||||
if (!$has_changes) {
|
||||
break;
|
||||
}
|
||||
|
||||
// remove vars that were defined in the foreach
|
||||
foreach ($vars_to_remove as $var_id) {
|
||||
$continue_context->removePossibleReference($var_id);
|
||||
}
|
||||
|
||||
$continue_context->clauses = $pre_loop_context->clauses;
|
||||
$continue_context->byref_constraints = $pre_loop_context->byref_constraints;
|
||||
|
||||
$analyzer->setMixedCountsForFile($statements_analyzer->getFilePath(), $original_mixed_counts);
|
||||
IssueBuffer::startRecording();
|
||||
|
||||
if (!$is_do) {
|
||||
foreach ($pre_conditions as $condition_offset => $pre_condition) {
|
||||
self::applyPreConditionToLoopContext(
|
||||
$statements_analyzer,
|
||||
$pre_condition,
|
||||
$pre_condition_clauses[$condition_offset],
|
||||
$continue_context,
|
||||
$loop_parent_context,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($always_assigned_before_loop_body_vars as $var_id) {
|
||||
if ((!isset($continue_context->vars_in_scope[$var_id])
|
||||
|| $continue_context->vars_in_scope[$var_id]->getId()
|
||||
!== $pre_loop_context->vars_in_scope[$var_id]->getId()
|
||||
|| $continue_context->vars_in_scope[$var_id]->from_docblock
|
||||
!== $pre_loop_context->vars_in_scope[$var_id]->from_docblock
|
||||
)
|
||||
) {
|
||||
if (isset($pre_loop_context->vars_in_scope[$var_id])) {
|
||||
$continue_context->vars_in_scope[$var_id] = $pre_loop_context->vars_in_scope[$var_id];
|
||||
} else {
|
||||
$continue_context->removePossibleReference($var_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$continue_context->clauses = $pre_loop_context->clauses;
|
||||
|
||||
$continue_context->protected_var_ids = $loop_scope->protected_var_ids;
|
||||
|
||||
$traverser = new PhpParser\NodeTraverser;
|
||||
|
||||
$traverser->addVisitor(
|
||||
new NodeCleanerVisitor(
|
||||
$statements_analyzer->node_data,
|
||||
),
|
||||
);
|
||||
$traverser->traverse($stmts);
|
||||
|
||||
$statements_analyzer->analyze($stmts, $continue_context);
|
||||
|
||||
self::updateLoopScopeContexts($loop_scope, $loop_context, $continue_context, $original_parent_context);
|
||||
|
||||
$continue_context->protected_var_ids = $original_protected_var_ids;
|
||||
|
||||
if ($is_do) {
|
||||
$inner_do_context = clone $continue_context;
|
||||
|
||||
foreach ($pre_conditions as $condition_offset => $pre_condition) {
|
||||
self::applyPreConditionToLoopContext(
|
||||
$statements_analyzer,
|
||||
$pre_condition,
|
||||
$pre_condition_clauses[$condition_offset],
|
||||
$continue_context,
|
||||
$loop_parent_context,
|
||||
$is_do,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($post_expressions as $post_expression) {
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$post_expression,
|
||||
$continue_context,
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$recorded_issues = IssueBuffer::clearRecordingLevel();
|
||||
|
||||
IssueBuffer::stopRecording();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$does_sometimes_break = in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true);
|
||||
$does_always_break = $loop_scope->final_actions === [ScopeAnalyzer::ACTION_BREAK];
|
||||
|
||||
if ($does_sometimes_break) {
|
||||
foreach ($loop_scope->possibly_redefined_loop_parent_vars as $var => $type) {
|
||||
$loop_parent_context->vars_in_scope[$var] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$loop_parent_context->vars_in_scope[$var],
|
||||
);
|
||||
|
||||
$loop_parent_context->possibly_assigned_var_ids[$var] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($loop_parent_context->vars_in_scope as $var_id => $type) {
|
||||
if (!isset($loop_context->vars_in_scope[$var_id])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($loop_context->vars_in_scope[$var_id]->getId() !== $type->getId()) {
|
||||
$loop_parent_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$loop_parent_context->vars_in_scope[$var_id],
|
||||
$loop_context->vars_in_scope[$var_id],
|
||||
);
|
||||
|
||||
$loop_parent_context->removeVarFromConflictingClauses($var_id);
|
||||
} else {
|
||||
$loop_parent_context->vars_in_scope[$var_id]
|
||||
= $loop_parent_context->vars_in_scope[$var_id]->addParentNodes(
|
||||
$loop_context->vars_in_scope[$var_id]->parent_nodes,
|
||||
)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$does_always_break) {
|
||||
foreach ($loop_parent_context->vars_in_scope as $var_id => $type) {
|
||||
if (!isset($continue_context->vars_in_scope[$var_id])) {
|
||||
$loop_parent_context->removePossibleReference($var_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($continue_context->vars_in_scope[$var_id]->hasMixed()) {
|
||||
$continue_context->vars_in_scope[$var_id]
|
||||
= $continue_context->vars_in_scope[$var_id]->addParentNodes(
|
||||
$loop_parent_context->vars_in_scope[$var_id]->parent_nodes,
|
||||
)
|
||||
;
|
||||
|
||||
$loop_parent_context->vars_in_scope[$var_id] =
|
||||
$continue_context->vars_in_scope[$var_id];
|
||||
$loop_parent_context->removeVarFromConflictingClauses($var_id);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($continue_context->vars_in_scope[$var_id]->getId() !== $type->getId()) {
|
||||
$loop_parent_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$loop_parent_context->vars_in_scope[$var_id],
|
||||
$continue_context->vars_in_scope[$var_id],
|
||||
);
|
||||
|
||||
$loop_parent_context->removeVarFromConflictingClauses($var_id);
|
||||
} else {
|
||||
$loop_parent_context->vars_in_scope[$var_id] =
|
||||
$loop_parent_context->vars_in_scope[$var_id]->setParentNodes(array_merge(
|
||||
$loop_parent_context->vars_in_scope[$var_id]->parent_nodes,
|
||||
$continue_context->vars_in_scope[$var_id]->parent_nodes,
|
||||
))
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($pre_conditions && $pre_condition_clauses && !$does_sometimes_break) {
|
||||
// if the loop contains an assertion and there are no break statements, we can negate that assertion
|
||||
// and apply it to the current context
|
||||
|
||||
try {
|
||||
$negated_pre_condition_clauses = Algebra::negateFormula(array_merge(...$pre_condition_clauses));
|
||||
} catch (ComplicatedExpressionException $e) {
|
||||
$negated_pre_condition_clauses = [];
|
||||
}
|
||||
|
||||
$negated_pre_condition_types = Algebra::getTruthsFromFormula($negated_pre_condition_clauses);
|
||||
|
||||
if ($negated_pre_condition_types) {
|
||||
$changed_var_ids = [];
|
||||
|
||||
[$vars_in_scope_reconciled, $_] = Reconciler::reconcileKeyedTypes(
|
||||
$negated_pre_condition_types,
|
||||
[],
|
||||
$continue_context->vars_in_scope,
|
||||
$continue_context->references_in_scope,
|
||||
$changed_var_ids,
|
||||
[],
|
||||
$statements_analyzer,
|
||||
[],
|
||||
true,
|
||||
new CodeLocation($statements_analyzer->getSource(), $pre_conditions[0]),
|
||||
);
|
||||
|
||||
foreach ($changed_var_ids as $var_id => $_) {
|
||||
if (isset($vars_in_scope_reconciled[$var_id])
|
||||
&& isset($loop_parent_context->vars_in_scope[$var_id])
|
||||
) {
|
||||
$loop_parent_context->vars_in_scope[$var_id] = $vars_in_scope_reconciled[$var_id];
|
||||
}
|
||||
|
||||
$loop_parent_context->removeVarFromConflictingClauses($var_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($always_enters_loop) {
|
||||
foreach ($continue_context->vars_in_scope as $var_id => $type) {
|
||||
// if there are break statements in the loop it's not certain
|
||||
// that the loop has finished executing, so the assertions at the end
|
||||
// the loop in the while conditional may not hold
|
||||
if (in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true)
|
||||
|| in_array(ScopeAnalyzer::ACTION_CONTINUE, $loop_scope->final_actions, true)
|
||||
) {
|
||||
if (isset($loop_scope->possibly_defined_loop_parent_vars[$var_id])) {
|
||||
$loop_parent_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$loop_scope->possibly_defined_loop_parent_vars[$var_id],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$loop_parent_context->vars_in_scope[$var_id] = $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($inner_do_context) {
|
||||
$continue_context = $inner_do_context;
|
||||
}
|
||||
|
||||
// Track references set in the loop to make sure they aren't reused later
|
||||
$loop_parent_context->updateReferencesPossiblyFromConfusingScope(
|
||||
$continue_context,
|
||||
$statements_analyzer,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function updateLoopScopeContexts(
|
||||
LoopScope $loop_scope,
|
||||
Context $loop_context,
|
||||
Context $continue_context,
|
||||
Context $pre_outer_context
|
||||
): void {
|
||||
if (!in_array(ScopeAnalyzer::ACTION_CONTINUE, $loop_scope->final_actions, true)) {
|
||||
$loop_context->vars_in_scope = $pre_outer_context->vars_in_scope;
|
||||
} else {
|
||||
$updated_loop_vars = [];
|
||||
|
||||
foreach ($loop_scope->redefined_loop_vars as $var => $type) {
|
||||
$continue_context->vars_in_scope[$var] = $type;
|
||||
$updated_loop_vars[$var] = true;
|
||||
}
|
||||
|
||||
foreach ($loop_scope->possibly_redefined_loop_vars as $var => $type) {
|
||||
if ($continue_context->hasVariable($var)) {
|
||||
if (!isset($updated_loop_vars[$var])) {
|
||||
$continue_context->vars_in_scope[$var] = Type::combineUnionTypes(
|
||||
$continue_context->vars_in_scope[$var],
|
||||
$type,
|
||||
);
|
||||
} else {
|
||||
$continue_context->vars_in_scope[$var] =
|
||||
$continue_context->vars_in_scope[$var]->addParentNodes($type->parent_nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// merge vars possibly in scope at the end of each loop
|
||||
$loop_context->vars_possibly_in_scope = array_merge(
|
||||
$loop_context->vars_possibly_in_scope,
|
||||
$loop_scope->vars_possibly_in_scope,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<Clause> $pre_condition_clauses
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function applyPreConditionToLoopContext(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr $pre_condition,
|
||||
array $pre_condition_clauses,
|
||||
Context $loop_context,
|
||||
Context $outer_context,
|
||||
bool $is_do
|
||||
): array {
|
||||
$pre_referenced_var_ids = $loop_context->cond_referenced_var_ids;
|
||||
$loop_context->cond_referenced_var_ids = [];
|
||||
|
||||
$was_inside_conditional = $loop_context->inside_conditional;
|
||||
|
||||
$loop_context->inside_conditional = true;
|
||||
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $pre_condition, $loop_context) === false) {
|
||||
$loop_context->inside_conditional = $was_inside_conditional;
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
$loop_context->inside_conditional = $was_inside_conditional;
|
||||
|
||||
$new_referenced_var_ids = $loop_context->cond_referenced_var_ids;
|
||||
$loop_context->cond_referenced_var_ids = array_merge($pre_referenced_var_ids, $new_referenced_var_ids);
|
||||
|
||||
$always_assigned_before_loop_body_vars = Context::getNewOrUpdatedVarIds($outer_context, $loop_context);
|
||||
|
||||
$loop_context->clauses = Algebra::simplifyCNF(
|
||||
[...$outer_context->clauses, ...$pre_condition_clauses],
|
||||
);
|
||||
|
||||
$active_while_types = [];
|
||||
|
||||
$reconcilable_while_types = Algebra::getTruthsFromFormula(
|
||||
$loop_context->clauses,
|
||||
spl_object_id($pre_condition),
|
||||
$new_referenced_var_ids,
|
||||
$active_while_types,
|
||||
);
|
||||
|
||||
$changed_var_ids = [];
|
||||
|
||||
if ($reconcilable_while_types) {
|
||||
[$loop_context->vars_in_scope, $loop_context->references_in_scope] = Reconciler::reconcileKeyedTypes(
|
||||
$reconcilable_while_types,
|
||||
$active_while_types,
|
||||
$loop_context->vars_in_scope,
|
||||
$loop_context->references_in_scope,
|
||||
$changed_var_ids,
|
||||
$new_referenced_var_ids,
|
||||
$statements_analyzer,
|
||||
[],
|
||||
true,
|
||||
new CodeLocation($statements_analyzer->getSource(), $pre_condition),
|
||||
);
|
||||
}
|
||||
|
||||
if ($is_do) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($always_assigned_before_loop_body_vars as $var_id) {
|
||||
$loop_context->clauses = Context::filterClauses(
|
||||
$var_id,
|
||||
$loop_context->clauses,
|
||||
null,
|
||||
$statements_analyzer,
|
||||
);
|
||||
}
|
||||
|
||||
return $always_assigned_before_loop_body_vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, bool>> $assignment_map
|
||||
*/
|
||||
private static function getAssignmentMapDepth(string $first_var_id, array $assignment_map): int
|
||||
{
|
||||
$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;
|
||||
}
|
||||
}
|
||||
228
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/SwitchAnalyzer.php
vendored
Normal file
228
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/SwitchAnalyzer.php
vendored
Normal file
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Block;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Scope\SwitchScope;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Reconciler;
|
||||
use SplFixedArray;
|
||||
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class SwitchAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Stmt\Switch_ $stmt,
|
||||
Context $context
|
||||
): void {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$was_inside_conditional = $context->inside_conditional;
|
||||
|
||||
$context->inside_conditional = true;
|
||||
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->cond, $context) === false) {
|
||||
$context->inside_conditional = $was_inside_conditional;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$context->inside_conditional = $was_inside_conditional;
|
||||
|
||||
$switch_var_id = ExpressionIdentifier::getExtendedVarId(
|
||||
$stmt->cond,
|
||||
null,
|
||||
$statements_analyzer,
|
||||
);
|
||||
|
||||
if (!$switch_var_id
|
||||
&& ($stmt->cond instanceof PhpParser\Node\Expr\FuncCall
|
||||
|| $stmt->cond instanceof PhpParser\Node\Expr\MethodCall
|
||||
|| $stmt->cond instanceof PhpParser\Node\Expr\StaticCall
|
||||
)
|
||||
) {
|
||||
$switch_var_id = '$__tmp_switch__' . (int) $stmt->cond->getAttribute('startFilePos');
|
||||
|
||||
$condition_type = $statements_analyzer->node_data->getType($stmt->cond) ?? Type::getMixed();
|
||||
|
||||
$context->vars_in_scope[$switch_var_id] = $condition_type;
|
||||
}
|
||||
|
||||
$original_context = clone $context;
|
||||
|
||||
// the last statement always breaks, by default
|
||||
$last_case_exit_type = 'break';
|
||||
|
||||
$case_exit_types = new SplFixedArray(count($stmt->cases));
|
||||
|
||||
$has_default = false;
|
||||
|
||||
$case_action_map = [];
|
||||
|
||||
// create a map of case statement -> ultimate exit type
|
||||
for ($i = count($stmt->cases) - 1; $i >= 0; --$i) {
|
||||
$case = $stmt->cases[$i];
|
||||
|
||||
$case_actions = $case_action_map[$i] = ScopeAnalyzer::getControlActions(
|
||||
$case->stmts,
|
||||
$statements_analyzer->node_data,
|
||||
['switch'],
|
||||
);
|
||||
|
||||
if (!in_array(ScopeAnalyzer::ACTION_NONE, $case_actions, true)) {
|
||||
if ($case_actions === [ScopeAnalyzer::ACTION_END]) {
|
||||
$last_case_exit_type = 'return_throw';
|
||||
} elseif ($case_actions === [ScopeAnalyzer::ACTION_CONTINUE]) {
|
||||
$last_case_exit_type = 'continue';
|
||||
} elseif (in_array(ScopeAnalyzer::ACTION_LEAVE_SWITCH, $case_actions, true)) {
|
||||
$last_case_exit_type = 'break';
|
||||
}
|
||||
} elseif (count($case_actions) !== 1) {
|
||||
$last_case_exit_type = 'hybrid';
|
||||
}
|
||||
|
||||
$case_exit_types[$i] = $last_case_exit_type;
|
||||
}
|
||||
|
||||
$switch_scope = new SwitchScope();
|
||||
|
||||
$was_caching_assertions = $statements_analyzer->node_data->cache_assertions;
|
||||
|
||||
$statements_analyzer->node_data->cache_assertions = false;
|
||||
|
||||
$all_options_returned = true;
|
||||
|
||||
for ($i = 0, $l = count($stmt->cases); $i < $l; $i++) {
|
||||
$case = $stmt->cases[$i];
|
||||
|
||||
/** @var string */
|
||||
$case_exit_type = $case_exit_types[$i];
|
||||
if ($case_exit_type !== 'return_throw') {
|
||||
$all_options_returned = false;
|
||||
}
|
||||
|
||||
$case_actions = $case_action_map[$i];
|
||||
|
||||
if (!$case->cond) {
|
||||
$has_default = true;
|
||||
}
|
||||
|
||||
if (SwitchCaseAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
$stmt,
|
||||
$switch_var_id,
|
||||
$case,
|
||||
$context,
|
||||
$original_context,
|
||||
$case_exit_type,
|
||||
$case_actions,
|
||||
$i === $l - 1,
|
||||
$switch_scope,
|
||||
) === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$all_options_matched = $has_default;
|
||||
|
||||
if (!$has_default && $switch_scope->negated_clauses && $switch_var_id) {
|
||||
$entry_clauses = Algebra::simplifyCNF(
|
||||
[...$original_context->clauses, ...$switch_scope->negated_clauses],
|
||||
);
|
||||
|
||||
$reconcilable_if_types = Algebra::getTruthsFromFormula($entry_clauses);
|
||||
|
||||
// if the if has an || in the conditional, we cannot easily reason about it
|
||||
if ($reconcilable_if_types && isset($reconcilable_if_types[$switch_var_id])) {
|
||||
$changed_var_ids = [];
|
||||
|
||||
[$case_vars_in_scope_reconciled, $_] =
|
||||
Reconciler::reconcileKeyedTypes(
|
||||
$reconcilable_if_types,
|
||||
[],
|
||||
$original_context->vars_in_scope,
|
||||
$original_context->references_in_scope,
|
||||
$changed_var_ids,
|
||||
[],
|
||||
$statements_analyzer,
|
||||
[],
|
||||
$original_context->inside_loop,
|
||||
);
|
||||
|
||||
if (isset($case_vars_in_scope_reconciled[$switch_var_id])
|
||||
&& $case_vars_in_scope_reconciled[$switch_var_id]->isNever()
|
||||
) {
|
||||
$all_options_matched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($was_caching_assertions) {
|
||||
$statements_analyzer->node_data->cache_assertions = true;
|
||||
}
|
||||
|
||||
// only update vars if there is a default or all possible cases accounted for
|
||||
// if the default has a throw/return/continue, that should be handled above
|
||||
if ($all_options_matched) {
|
||||
if ($switch_scope->new_vars_in_scope) {
|
||||
$context->vars_in_scope = array_merge($context->vars_in_scope, $switch_scope->new_vars_in_scope);
|
||||
}
|
||||
|
||||
if ($switch_scope->redefined_vars) {
|
||||
$context->vars_in_scope = array_merge($context->vars_in_scope, $switch_scope->redefined_vars);
|
||||
}
|
||||
|
||||
if ($switch_scope->possibly_redefined_vars) {
|
||||
foreach ($switch_scope->possibly_redefined_vars as $var_id => $type) {
|
||||
if (!isset($switch_scope->redefined_vars[$var_id])
|
||||
&& !isset($switch_scope->new_vars_in_scope[$var_id])
|
||||
&& isset($context->vars_in_scope[$var_id])
|
||||
) {
|
||||
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$context->vars_in_scope[$var_id],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$stmt->setAttribute('allMatched', true);
|
||||
} elseif ($switch_scope->possibly_redefined_vars) {
|
||||
foreach ($switch_scope->possibly_redefined_vars as $var_id => $type) {
|
||||
if (isset($context->vars_in_scope[$var_id])) {
|
||||
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$context->vars_in_scope[$var_id],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($switch_scope->new_assigned_var_ids) {
|
||||
$context->assigned_var_ids += $switch_scope->new_assigned_var_ids;
|
||||
}
|
||||
|
||||
$context->vars_possibly_in_scope = array_merge(
|
||||
$context->vars_possibly_in_scope,
|
||||
$switch_scope->new_vars_possibly_in_scope,
|
||||
);
|
||||
|
||||
//a switch can't return in all options without a default
|
||||
$context->has_returned = $all_options_returned && $has_default;
|
||||
}
|
||||
}
|
||||
740
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php
vendored
Normal file
740
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/SwitchCaseAnalyzer.php
vendored
Normal file
@@ -0,0 +1,740 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Block;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Context;
|
||||
use Psalm\Exception\ComplicatedExpressionException;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Algebra\FormulaGenerator;
|
||||
use Psalm\Internal\Analyzer\AlgebraAnalyzer;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\PhpVisitor\ConditionCloningVisitor;
|
||||
use Psalm\Internal\PhpVisitor\TypeMappingVisitor;
|
||||
use Psalm\Internal\Scope\CaseScope;
|
||||
use Psalm\Internal\Scope\SwitchScope;
|
||||
use Psalm\Issue\ContinueOutsideLoop;
|
||||
use Psalm\Issue\ParadoxicalCondition;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Node\Expr\BinaryOp\VirtualBooleanOr;
|
||||
use Psalm\Node\Expr\BinaryOp\VirtualEqual;
|
||||
use Psalm\Node\Expr\BinaryOp\VirtualIdentical;
|
||||
use Psalm\Node\Expr\VirtualArray;
|
||||
use Psalm\Node\Expr\VirtualArrayItem;
|
||||
use Psalm\Node\Expr\VirtualBooleanNot;
|
||||
use Psalm\Node\Expr\VirtualConstFetch;
|
||||
use Psalm\Node\Expr\VirtualFuncCall;
|
||||
use Psalm\Node\Expr\VirtualVariable;
|
||||
use Psalm\Node\Name\VirtualFullyQualified;
|
||||
use Psalm\Node\Scalar\VirtualLNumber;
|
||||
use Psalm\Node\Stmt\VirtualIf;
|
||||
use Psalm\Node\VirtualArg;
|
||||
use Psalm\Node\VirtualName;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TDependentGetClass;
|
||||
use Psalm\Type\Atomic\TDependentGetDebugType;
|
||||
use Psalm\Type\Atomic\TDependentGetType;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
use function array_diff_key;
|
||||
use function array_intersect_key;
|
||||
use function array_merge;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
use function spl_object_id;
|
||||
use function strpos;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class SwitchCaseAnalyzer
|
||||
{
|
||||
/**
|
||||
* @return null|false
|
||||
*/
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
Codebase $codebase,
|
||||
PhpParser\Node\Stmt\Switch_ $stmt,
|
||||
?string $switch_var_id,
|
||||
PhpParser\Node\Stmt\Case_ $case,
|
||||
Context $context,
|
||||
Context $original_context,
|
||||
string $case_exit_type,
|
||||
array $case_actions,
|
||||
bool $is_last,
|
||||
SwitchScope $switch_scope
|
||||
): ?bool {
|
||||
// has a return/throw at end
|
||||
$has_ending_statements = $case_actions === [ScopeAnalyzer::ACTION_END];
|
||||
$has_leaving_statements = $has_ending_statements
|
||||
|| (count($case_actions) && !in_array(ScopeAnalyzer::ACTION_NONE, $case_actions, true));
|
||||
|
||||
$case_context = clone $original_context;
|
||||
|
||||
if ($codebase->alter_code) {
|
||||
$case_context->branch_point = $case_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
|
||||
}
|
||||
|
||||
$case_scope = $case_context->case_scope = new CaseScope($case_context);
|
||||
|
||||
$case_equality_expr = null;
|
||||
|
||||
$old_node_data = $statements_analyzer->node_data;
|
||||
|
||||
$fake_switch_condition = false;
|
||||
|
||||
if ($switch_var_id && strpos($switch_var_id, '$__tmp_switch__') === 0) {
|
||||
$switch_condition = new VirtualVariable(
|
||||
substr($switch_var_id, 1),
|
||||
$stmt->cond->getAttributes(),
|
||||
);
|
||||
|
||||
$fake_switch_condition = true;
|
||||
} else {
|
||||
$switch_condition = $stmt->cond;
|
||||
}
|
||||
|
||||
if ($case->cond) {
|
||||
$was_inside_conditional = $case_context->inside_conditional;
|
||||
$case_context->inside_conditional = true;
|
||||
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $case->cond, $case_context) === false) {
|
||||
unset($case_scope->parent_context);
|
||||
unset($case_context->case_scope);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$case_context->inside_conditional = $was_inside_conditional;
|
||||
|
||||
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
||||
|
||||
$traverser = new PhpParser\NodeTraverser;
|
||||
$traverser->addVisitor(
|
||||
new ConditionCloningVisitor(
|
||||
$statements_analyzer->node_data,
|
||||
),
|
||||
);
|
||||
|
||||
/** @var PhpParser\Node\Expr */
|
||||
$switch_condition = $traverser->traverse([$switch_condition])[0];
|
||||
|
||||
if ($fake_switch_condition) {
|
||||
$statements_analyzer->node_data->setType(
|
||||
$switch_condition,
|
||||
$case_context->vars_in_scope[$switch_var_id] ?? Type::getMixed(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($switch_condition instanceof PhpParser\Node\Expr\Variable
|
||||
&& is_string($switch_condition->name)
|
||||
&& isset($context->vars_in_scope['$' . $switch_condition->name])
|
||||
) {
|
||||
$switch_var_type = $context->vars_in_scope['$' . $switch_condition->name];
|
||||
|
||||
$type_statements = [];
|
||||
|
||||
foreach ($switch_var_type->getAtomicTypes() as $type) {
|
||||
if ($type instanceof TDependentGetClass) {
|
||||
$type_statements[] = new VirtualFuncCall(
|
||||
new VirtualName(['get_class']),
|
||||
[
|
||||
new VirtualArg(
|
||||
new VirtualVariable(
|
||||
substr($type->typeof, 1),
|
||||
$stmt->cond->getAttributes(),
|
||||
),
|
||||
false,
|
||||
false,
|
||||
$stmt->cond->getAttributes(),
|
||||
),
|
||||
],
|
||||
$stmt->cond->getAttributes(),
|
||||
);
|
||||
} elseif ($type instanceof TDependentGetType) {
|
||||
$type_statements[] = new VirtualFuncCall(
|
||||
new VirtualName(['gettype']),
|
||||
[
|
||||
new VirtualArg(
|
||||
new VirtualVariable(
|
||||
substr($type->typeof, 1),
|
||||
$stmt->cond->getAttributes(),
|
||||
),
|
||||
false,
|
||||
false,
|
||||
$stmt->cond->getAttributes(),
|
||||
),
|
||||
],
|
||||
$stmt->cond->getAttributes(),
|
||||
);
|
||||
} elseif ($type instanceof TDependentGetDebugType) {
|
||||
$type_statements[] = new VirtualFuncCall(
|
||||
new VirtualName(['get_debug_type']),
|
||||
[
|
||||
new VirtualArg(
|
||||
new VirtualVariable(
|
||||
substr($type->typeof, 1),
|
||||
$stmt->cond->getAttributes(),
|
||||
),
|
||||
false,
|
||||
false,
|
||||
$stmt->cond->getAttributes(),
|
||||
),
|
||||
],
|
||||
$stmt->cond->getAttributes(),
|
||||
);
|
||||
} else {
|
||||
$type_statements = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($type_statements && count($type_statements) === 1) {
|
||||
$switch_condition = $type_statements[0];
|
||||
|
||||
if ($fake_switch_condition) {
|
||||
$statements_analyzer->node_data->setType(
|
||||
$switch_condition,
|
||||
$case_context->vars_in_scope[$switch_var_id] ?? Type::getMixed(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($switch_condition instanceof PhpParser\Node\Expr\ConstFetch
|
||||
&& $switch_condition->name->parts === ['true']
|
||||
) {
|
||||
$case_equality_expr = $case->cond;
|
||||
} elseif (($switch_condition_type = $statements_analyzer->node_data->getType($switch_condition))
|
||||
&& ($case_cond_type = $statements_analyzer->node_data->getType($case->cond))
|
||||
&& (($switch_condition_type->isString() && $case_cond_type->isString())
|
||||
|| ($switch_condition_type->isInt() && $case_cond_type->isInt())
|
||||
|| ($switch_condition_type->isFloat() && $case_cond_type->isFloat())
|
||||
)
|
||||
) {
|
||||
$case_equality_expr = new VirtualIdentical(
|
||||
$switch_condition,
|
||||
$case->cond,
|
||||
$case->cond->getAttributes(),
|
||||
);
|
||||
} else {
|
||||
$case_equality_expr = new VirtualEqual(
|
||||
$switch_condition,
|
||||
$case->cond,
|
||||
$case->cond->getAttributes(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$continue_case_equality_expr = false;
|
||||
|
||||
if ($case->stmts) {
|
||||
$case_stmts = array_merge($switch_scope->leftover_statements, $case->stmts);
|
||||
} else {
|
||||
$continue_case_equality_expr = count($switch_scope->leftover_statements) === 1;
|
||||
$case_stmts = $switch_scope->leftover_statements;
|
||||
}
|
||||
|
||||
if (!$has_leaving_statements && !$is_last) {
|
||||
if (!$case_equality_expr) {
|
||||
$case_equality_expr = new VirtualFuncCall(
|
||||
new VirtualFullyQualified(['rand']),
|
||||
[
|
||||
new VirtualArg(new VirtualLNumber(0)),
|
||||
new VirtualArg(new VirtualLNumber(1)),
|
||||
],
|
||||
$case->getAttributes(),
|
||||
);
|
||||
}
|
||||
|
||||
$switch_scope->leftover_case_equality_expr = $switch_scope->leftover_case_equality_expr
|
||||
? new VirtualBooleanOr(
|
||||
$switch_scope->leftover_case_equality_expr,
|
||||
$case_equality_expr,
|
||||
$case->cond ? $case->cond->getAttributes() : $case->getAttributes(),
|
||||
)
|
||||
: $case_equality_expr;
|
||||
|
||||
if ($continue_case_equality_expr
|
||||
&& $switch_scope->leftover_statements[0] instanceof PhpParser\Node\Stmt\If_
|
||||
) {
|
||||
$case_if_stmt = $switch_scope->leftover_statements[0];
|
||||
$case_if_stmt->cond = $switch_scope->leftover_case_equality_expr;
|
||||
} else {
|
||||
$case_if_stmt = new VirtualIf(
|
||||
$switch_scope->leftover_case_equality_expr,
|
||||
['stmts' => $case_stmts],
|
||||
$case->getAttributes(),
|
||||
);
|
||||
|
||||
$switch_scope->leftover_statements = [$case_if_stmt];
|
||||
}
|
||||
|
||||
unset($case_scope->parent_context);
|
||||
unset($case_context->case_scope);
|
||||
|
||||
$statements_analyzer->node_data = $old_node_data;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($switch_scope->leftover_case_equality_expr) {
|
||||
$case_or_default_equality_expr = $case_equality_expr;
|
||||
|
||||
if (!$case_or_default_equality_expr) {
|
||||
$case_or_default_equality_expr = new VirtualFuncCall(
|
||||
new VirtualFullyQualified(['rand']),
|
||||
[
|
||||
new VirtualArg(new VirtualLNumber(0)),
|
||||
new VirtualArg(new VirtualLNumber(1)),
|
||||
],
|
||||
$case->getAttributes(),
|
||||
);
|
||||
}
|
||||
|
||||
$case_equality_expr = new VirtualBooleanOr(
|
||||
$switch_scope->leftover_case_equality_expr,
|
||||
$case_or_default_equality_expr,
|
||||
$case_or_default_equality_expr->getAttributes(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($case_equality_expr
|
||||
&& $switch_condition instanceof PhpParser\Node\Expr\Variable
|
||||
&& is_string($switch_condition->name)
|
||||
&& isset($context->vars_in_scope['$' . $switch_condition->name])
|
||||
) {
|
||||
$new_case_equality_expr = self::simplifyCaseEqualityExpression(
|
||||
$case_equality_expr,
|
||||
$switch_condition,
|
||||
);
|
||||
|
||||
if ($new_case_equality_expr) {
|
||||
$was_inside_conditional = $case_context->inside_conditional;
|
||||
|
||||
$case_context->inside_conditional = true;
|
||||
|
||||
ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$new_case_equality_expr->getArgs()[1]->value,
|
||||
$case_context,
|
||||
);
|
||||
|
||||
$case_context->inside_conditional = $was_inside_conditional;
|
||||
|
||||
$case_equality_expr = $new_case_equality_expr;
|
||||
}
|
||||
}
|
||||
|
||||
$case_context->break_types[] = 'switch';
|
||||
|
||||
$switch_scope->leftover_statements = [];
|
||||
$switch_scope->leftover_case_equality_expr = null;
|
||||
|
||||
$case_clauses = [];
|
||||
|
||||
if ($case_equality_expr) {
|
||||
$case_equality_expr_id = spl_object_id($case_equality_expr);
|
||||
$case_clauses = FormulaGenerator::getFormula(
|
||||
$case_equality_expr_id,
|
||||
$case_equality_expr_id,
|
||||
$case_equality_expr,
|
||||
$context->self,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if ($switch_scope->negated_clauses && count($switch_scope->negated_clauses) < 50) {
|
||||
$entry_clauses = Algebra::simplifyCNF(
|
||||
[...$original_context->clauses, ...$switch_scope->negated_clauses],
|
||||
);
|
||||
} else {
|
||||
$entry_clauses = $original_context->clauses;
|
||||
}
|
||||
|
||||
if ($case_clauses && $case->cond) {
|
||||
// this will see whether any of the clauses in set A conflict with the clauses in set B
|
||||
AlgebraAnalyzer::checkForParadox(
|
||||
$entry_clauses,
|
||||
$case_clauses,
|
||||
$statements_analyzer,
|
||||
$case->cond,
|
||||
[],
|
||||
);
|
||||
|
||||
if (count($entry_clauses) + count($case_clauses) < 50) {
|
||||
$case_context->clauses = Algebra::simplifyCNF([...$entry_clauses, ...$case_clauses]);
|
||||
} else {
|
||||
$case_context->clauses = [...$entry_clauses, ...$case_clauses];
|
||||
}
|
||||
} else {
|
||||
$case_context->clauses = $entry_clauses;
|
||||
}
|
||||
|
||||
$reconcilable_if_types = Algebra::getTruthsFromFormula($case_context->clauses);
|
||||
|
||||
// if the if has an || in the conditional, we cannot easily reason about it
|
||||
if ($reconcilable_if_types) {
|
||||
$changed_var_ids = [];
|
||||
|
||||
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
|
||||
|
||||
if (!in_array('RedundantCondition', $suppressed_issues, true)) {
|
||||
$statements_analyzer->addSuppressedIssues(['RedundantCondition']);
|
||||
}
|
||||
|
||||
if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
|
||||
$statements_analyzer->addSuppressedIssues(['RedundantConditionGivenDocblockType']);
|
||||
}
|
||||
|
||||
[$case_vars_in_scope_reconciled, $case_references_in_scope_reconciled] =
|
||||
Reconciler::reconcileKeyedTypes(
|
||||
$reconcilable_if_types,
|
||||
[],
|
||||
$case_context->vars_in_scope,
|
||||
$case_context->references_in_scope,
|
||||
$changed_var_ids,
|
||||
$case->cond && $switch_var_id ? [$switch_var_id => true] : [],
|
||||
$statements_analyzer,
|
||||
[],
|
||||
$case_context->inside_loop,
|
||||
new CodeLocation(
|
||||
$statements_analyzer->getSource(),
|
||||
$case->cond ?? $case,
|
||||
$context->include_location,
|
||||
),
|
||||
);
|
||||
|
||||
if (!in_array('RedundantCondition', $suppressed_issues, true)) {
|
||||
$statements_analyzer->removeSuppressedIssues(['RedundantCondition']);
|
||||
}
|
||||
|
||||
if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
|
||||
$statements_analyzer->removeSuppressedIssues(['RedundantConditionGivenDocblockType']);
|
||||
}
|
||||
|
||||
$case_context->vars_in_scope = $case_vars_in_scope_reconciled;
|
||||
$case_context->references_in_scope = $case_references_in_scope_reconciled;
|
||||
foreach ($reconcilable_if_types as $var_id => $_) {
|
||||
$case_context->vars_possibly_in_scope[$var_id] = true;
|
||||
}
|
||||
|
||||
if ($changed_var_ids) {
|
||||
$case_context->clauses = Context::removeReconciledClauses($case_context->clauses, $changed_var_ids)[0];
|
||||
}
|
||||
}
|
||||
|
||||
if ($case_clauses && $case_equality_expr) {
|
||||
try {
|
||||
$negated_case_clauses = Algebra::negateFormula($case_clauses);
|
||||
} catch (ComplicatedExpressionException $e) {
|
||||
$case_equality_expr_id = spl_object_id($case_equality_expr);
|
||||
|
||||
try {
|
||||
$negated_case_clauses = FormulaGenerator::getFormula(
|
||||
$case_equality_expr_id,
|
||||
$case_equality_expr_id,
|
||||
new VirtualBooleanNot($case_equality_expr),
|
||||
$context->self,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
} catch (ComplicatedExpressionException $e) {
|
||||
$negated_case_clauses = [];
|
||||
}
|
||||
}
|
||||
|
||||
$switch_scope->negated_clauses = [...$switch_scope->negated_clauses, ...$negated_case_clauses];
|
||||
}
|
||||
|
||||
$statements_analyzer->analyze($case_stmts, $case_context);
|
||||
|
||||
$traverser = new PhpParser\NodeTraverser;
|
||||
$traverser->addVisitor(
|
||||
new TypeMappingVisitor(
|
||||
$statements_analyzer->node_data,
|
||||
$old_node_data,
|
||||
),
|
||||
);
|
||||
|
||||
$traverser->traverse([$case]);
|
||||
|
||||
$statements_analyzer->node_data = $old_node_data;
|
||||
|
||||
if ($case_exit_type !== 'return_throw') {
|
||||
if (self::handleNonReturningCase(
|
||||
$statements_analyzer,
|
||||
$switch_var_id,
|
||||
$case,
|
||||
$context,
|
||||
$case_context,
|
||||
$original_context,
|
||||
$case_exit_type,
|
||||
$switch_scope,
|
||||
) === false) {
|
||||
unset($case_scope->parent_context);
|
||||
unset($case_context->case_scope);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// augment the information with data from break statements
|
||||
if ($case_scope->break_vars !== null) {
|
||||
if ($switch_scope->possibly_redefined_vars === null) {
|
||||
$switch_scope->possibly_redefined_vars = array_intersect_key(
|
||||
$case_scope->break_vars,
|
||||
$context->vars_in_scope,
|
||||
);
|
||||
} else {
|
||||
foreach ($case_scope->break_vars as $var_id => $type) {
|
||||
if (isset($context->vars_in_scope[$var_id])) {
|
||||
$switch_scope->possibly_redefined_vars[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$switch_scope->possibly_redefined_vars[$var_id] ?? null,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($switch_scope->new_vars_in_scope !== null) {
|
||||
foreach ($switch_scope->new_vars_in_scope as $var_id => $type) {
|
||||
if (isset($case_scope->break_vars[$var_id])) {
|
||||
if (!isset($case_context->vars_in_scope[$var_id])) {
|
||||
unset($switch_scope->new_vars_in_scope[$var_id]);
|
||||
} else {
|
||||
$switch_scope->new_vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$case_scope->break_vars[$var_id],
|
||||
$type,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
unset($switch_scope->new_vars_in_scope[$var_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($switch_scope->redefined_vars !== null) {
|
||||
foreach ($switch_scope->redefined_vars as $var_id => $type) {
|
||||
if (isset($case_scope->break_vars[$var_id])) {
|
||||
$switch_scope->redefined_vars[$var_id] = Type::combineUnionTypes(
|
||||
$case_scope->break_vars[$var_id],
|
||||
$type,
|
||||
);
|
||||
} else {
|
||||
unset($switch_scope->redefined_vars[$var_id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unset($case_scope->parent_context);
|
||||
unset($case_context->case_scope);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|false
|
||||
*/
|
||||
private static function handleNonReturningCase(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
?string $switch_var_id,
|
||||
PhpParser\Node\Stmt\Case_ $case,
|
||||
Context $context,
|
||||
Context $case_context,
|
||||
Context $original_context,
|
||||
string $case_exit_type,
|
||||
SwitchScope $switch_scope
|
||||
): ?bool {
|
||||
if (!$case->cond
|
||||
&& $switch_var_id
|
||||
&& isset($case_context->vars_in_scope[$switch_var_id])
|
||||
&& $case_context->vars_in_scope[$switch_var_id]->isNever()
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new ParadoxicalCondition(
|
||||
'All possible case statements have been met, default is impossible here',
|
||||
new CodeLocation($statements_analyzer->getSource(), $case),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// if we're leaving this block, add vars to outer for loop scope
|
||||
if ($case_exit_type === 'continue') {
|
||||
if (!$context->loop_scope) {
|
||||
if (IssueBuffer::accepts(
|
||||
new ContinueOutsideLoop(
|
||||
'Continue called when not in loop',
|
||||
new CodeLocation($statements_analyzer->getSource(), $case),
|
||||
),
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$case_redefined_vars = $case_context->getRedefinedVars($original_context->vars_in_scope);
|
||||
|
||||
if ($switch_scope->possibly_redefined_vars === null) {
|
||||
$switch_scope->possibly_redefined_vars = $case_redefined_vars;
|
||||
} else {
|
||||
foreach ($case_redefined_vars as $var_id => $type) {
|
||||
$switch_scope->possibly_redefined_vars[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$switch_scope->possibly_redefined_vars[$var_id] ?? null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($switch_scope->redefined_vars === null) {
|
||||
$switch_scope->redefined_vars = $case_redefined_vars;
|
||||
} else {
|
||||
foreach ($switch_scope->redefined_vars as $var_id => $type) {
|
||||
if (!isset($case_redefined_vars[$var_id])) {
|
||||
unset($switch_scope->redefined_vars[$var_id]);
|
||||
} else {
|
||||
$switch_scope->redefined_vars[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$case_redefined_vars[$var_id],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$context_new_vars = array_diff_key($case_context->vars_in_scope, $context->vars_in_scope);
|
||||
|
||||
if ($switch_scope->new_vars_in_scope === null) {
|
||||
$switch_scope->new_vars_in_scope = $context_new_vars;
|
||||
$switch_scope->new_vars_possibly_in_scope = array_diff_key(
|
||||
$case_context->vars_possibly_in_scope,
|
||||
$context->vars_possibly_in_scope,
|
||||
);
|
||||
} else {
|
||||
foreach ($switch_scope->new_vars_in_scope as $new_var => $type) {
|
||||
if (!$case_context->hasVariable($new_var)) {
|
||||
unset($switch_scope->new_vars_in_scope[$new_var]);
|
||||
} else {
|
||||
$switch_scope->new_vars_in_scope[$new_var] =
|
||||
Type::combineUnionTypes($case_context->vars_in_scope[$new_var], $type);
|
||||
}
|
||||
}
|
||||
|
||||
$switch_scope->new_vars_possibly_in_scope = array_merge(
|
||||
array_diff_key(
|
||||
$case_context->vars_possibly_in_scope,
|
||||
$context->vars_possibly_in_scope,
|
||||
),
|
||||
$switch_scope->new_vars_possibly_in_scope,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($context->collect_exceptions) {
|
||||
$context->mergeExceptions($case_context);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static function simplifyCaseEqualityExpression(
|
||||
PhpParser\Node\Expr $case_equality_expr,
|
||||
PhpParser\Node\Expr\Variable $var
|
||||
): ?PhpParser\Node\Expr\FuncCall {
|
||||
if ($case_equality_expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
|
||||
$nested_or_options = self::getOptionsFromNestedOr($case_equality_expr, $var);
|
||||
|
||||
if ($nested_or_options) {
|
||||
return new VirtualFuncCall(
|
||||
new VirtualFullyQualified(['in_array']),
|
||||
[
|
||||
new VirtualArg(
|
||||
$var,
|
||||
false,
|
||||
false,
|
||||
$var->getAttributes(),
|
||||
),
|
||||
new VirtualArg(
|
||||
new VirtualArray(
|
||||
$nested_or_options,
|
||||
$case_equality_expr->getAttributes(),
|
||||
),
|
||||
false,
|
||||
false,
|
||||
$case_equality_expr->getAttributes(),
|
||||
),
|
||||
new VirtualArg(
|
||||
new VirtualConstFetch(
|
||||
new VirtualFullyQualified(['true']),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<PhpParser\Node\Expr\ArrayItem> $in_array_values
|
||||
* @return ?array<PhpParser\Node\Expr\ArrayItem>
|
||||
*/
|
||||
private static function getOptionsFromNestedOr(
|
||||
PhpParser\Node\Expr $case_equality_expr,
|
||||
PhpParser\Node\Expr\Variable $var,
|
||||
array $in_array_values = []
|
||||
): ?array {
|
||||
if ($case_equality_expr instanceof PhpParser\Node\Expr\BinaryOp\Identical
|
||||
&& $case_equality_expr->left instanceof PhpParser\Node\Expr\Variable
|
||||
&& $case_equality_expr->left->name === $var->name
|
||||
) {
|
||||
$in_array_values[] = new VirtualArrayItem(
|
||||
$case_equality_expr->right,
|
||||
null,
|
||||
false,
|
||||
$case_equality_expr->right->getAttributes(),
|
||||
);
|
||||
|
||||
return $in_array_values;
|
||||
}
|
||||
|
||||
if (!$case_equality_expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$case_equality_expr->right instanceof PhpParser\Node\Expr\BinaryOp\Identical
|
||||
|| !$case_equality_expr->right->left instanceof PhpParser\Node\Expr\Variable
|
||||
|| $case_equality_expr->right->left->name !== $var->name
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$in_array_values[] = new VirtualArrayItem(
|
||||
$case_equality_expr->right->right,
|
||||
null,
|
||||
false,
|
||||
$case_equality_expr->right->right->getAttributes(),
|
||||
);
|
||||
|
||||
return self::getOptionsFromNestedOr(
|
||||
$case_equality_expr->left,
|
||||
$var,
|
||||
$in_array_values,
|
||||
);
|
||||
}
|
||||
}
|
||||
486
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php
vendored
Normal file
486
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php
vendored
Normal file
@@ -0,0 +1,486 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Block;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\ClassLikeNameOptions;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Codebase\VariableUseGraph;
|
||||
use Psalm\Internal\DataFlow\DataFlowNode;
|
||||
use Psalm\Internal\Scope\FinallyScope;
|
||||
use Psalm\Issue\InvalidCatch;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Union;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_intersect_key;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
use function strtolower;
|
||||
use function version_compare;
|
||||
|
||||
use const PHP_VERSION;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class TryAnalyzer
|
||||
{
|
||||
/**
|
||||
* @return false|null
|
||||
*/
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Stmt\TryCatch $stmt,
|
||||
Context $context
|
||||
): ?bool {
|
||||
$catch_actions = [];
|
||||
$all_catches_leave = true;
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
/** @var int $i */
|
||||
foreach ($stmt->catches as $i => $catch) {
|
||||
$catch_actions[$i] = ScopeAnalyzer::getControlActions(
|
||||
$catch->stmts,
|
||||
$statements_analyzer->node_data,
|
||||
[],
|
||||
);
|
||||
$all_catches_leave = $all_catches_leave && !in_array(ScopeAnalyzer::ACTION_NONE, $catch_actions[$i], true);
|
||||
}
|
||||
|
||||
$existing_thrown_exceptions = $context->possibly_thrown_exceptions;
|
||||
|
||||
/**
|
||||
* @var array<string, array<array-key, CodeLocation>> $context->possibly_thrown_exceptions
|
||||
*/
|
||||
$context->possibly_thrown_exceptions = [];
|
||||
|
||||
$old_context = clone $context;
|
||||
|
||||
$try_context = clone $context;
|
||||
|
||||
if ($codebase->alter_code) {
|
||||
$try_context->branch_point = $try_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
|
||||
}
|
||||
|
||||
if ($stmt->finally) {
|
||||
$try_context->finally_scope = new FinallyScope($try_context->vars_in_scope);
|
||||
}
|
||||
|
||||
$assigned_var_ids = $try_context->assigned_var_ids;
|
||||
$context->assigned_var_ids = [];
|
||||
|
||||
$was_inside_try = $context->inside_try;
|
||||
$context->inside_try = true;
|
||||
if ($statements_analyzer->analyze($stmt->stmts, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
$context->inside_try = $was_inside_try;
|
||||
|
||||
$context->has_returned = false;
|
||||
|
||||
$try_block_control_actions = ScopeAnalyzer::getControlActions(
|
||||
$stmt->stmts,
|
||||
$statements_analyzer->node_data,
|
||||
[],
|
||||
);
|
||||
|
||||
/** @var array<string, int> */
|
||||
$newly_assigned_var_ids = $context->assigned_var_ids;
|
||||
|
||||
$context->assigned_var_ids = array_merge(
|
||||
$assigned_var_ids,
|
||||
$newly_assigned_var_ids,
|
||||
);
|
||||
|
||||
foreach ($context->vars_in_scope as $var_id => $type) {
|
||||
if (!isset($try_context->vars_in_scope[$var_id])) {
|
||||
$try_context->vars_in_scope[$var_id] = $type;
|
||||
|
||||
$context->vars_in_scope[$var_id] = $type->setPossiblyUndefined(true, true);
|
||||
} else {
|
||||
$try_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$try_context->vars_in_scope[$var_id],
|
||||
$type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($try_context->finally_scope) {
|
||||
foreach ($context->vars_in_scope as $var_id => $type) {
|
||||
$try_context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$try_context->finally_scope->vars_in_scope[$var_id] ?? null,
|
||||
$type,
|
||||
$statements_analyzer->getCodebase(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$try_context->vars_possibly_in_scope = $context->vars_possibly_in_scope;
|
||||
$try_context->possibly_thrown_exceptions = $context->possibly_thrown_exceptions;
|
||||
|
||||
$try_leaves_loop = $context->loop_scope
|
||||
&& $context->loop_scope->final_actions
|
||||
&& !in_array(ScopeAnalyzer::ACTION_NONE, $context->loop_scope->final_actions, true);
|
||||
|
||||
if (!$all_catches_leave) {
|
||||
foreach ($newly_assigned_var_ids as $assigned_var_id => $_) {
|
||||
$context->removeVarFromConflictingClauses($assigned_var_id);
|
||||
}
|
||||
} else {
|
||||
foreach ($newly_assigned_var_ids as $assigned_var_id => $_) {
|
||||
$try_context->removeVarFromConflictingClauses($assigned_var_id);
|
||||
}
|
||||
}
|
||||
|
||||
// at this point we have two contexts – $context, in which it is assumed that everything was fine,
|
||||
// and $try_context - which allows all variables to have the union of the values before and after
|
||||
// the try was applied
|
||||
$original_context = clone $try_context;
|
||||
|
||||
$issues_to_suppress = [
|
||||
'RedundantCondition',
|
||||
'RedundantConditionGivenDocblockType',
|
||||
'TypeDoesNotContainNull',
|
||||
'TypeDoesNotContainType',
|
||||
];
|
||||
|
||||
$definitely_newly_assigned_var_ids = $newly_assigned_var_ids;
|
||||
|
||||
/** @var int $i */
|
||||
foreach ($stmt->catches as $i => $catch) {
|
||||
$catch_context = clone $original_context;
|
||||
$catch_context->has_returned = false;
|
||||
|
||||
foreach ($catch_context->vars_in_scope as $var_id => $type) {
|
||||
if (!isset($old_context->vars_in_scope[$var_id])) {
|
||||
$catch_context->vars_in_scope[$var_id] = $type->setPossiblyUndefined(
|
||||
$catch_context->vars_in_scope[$var_id]->possibly_undefined,
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
$catch_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$old_context->vars_in_scope[$var_id],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$fq_catch_classes = [];
|
||||
|
||||
if (!$catch->types) {
|
||||
throw new UnexpectedValueException('Very bad');
|
||||
}
|
||||
|
||||
foreach ($catch->types as $catch_type) {
|
||||
$fq_catch_class = ClassLikeAnalyzer::getFQCLNFromNameObject(
|
||||
$catch_type,
|
||||
$statements_analyzer->getAliases(),
|
||||
);
|
||||
|
||||
$fq_catch_class = $codebase->classlikes->getUnAliasedName($fq_catch_class);
|
||||
|
||||
if ($codebase->alter_code && $fq_catch_class) {
|
||||
$codebase->classlikes->handleClassLikeReferenceInMigration(
|
||||
$codebase,
|
||||
$statements_analyzer,
|
||||
$catch_type,
|
||||
$fq_catch_class,
|
||||
$context->calling_method_id,
|
||||
);
|
||||
}
|
||||
|
||||
if ($original_context->check_classes) {
|
||||
ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
|
||||
$statements_analyzer,
|
||||
$fq_catch_class,
|
||||
new CodeLocation($statements_analyzer->getSource(), $catch_type, $context->include_location),
|
||||
$context->self,
|
||||
$context->calling_method_id,
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
new ClassLikeNameOptions(true),
|
||||
);
|
||||
}
|
||||
|
||||
if (($codebase->classExists($fq_catch_class)
|
||||
&& strtolower($fq_catch_class) !== 'exception'
|
||||
&& !($codebase->classExtends($fq_catch_class, 'Exception')
|
||||
|| $codebase->classImplements($fq_catch_class, 'Throwable')))
|
||||
|| ($codebase->interfaceExists($fq_catch_class)
|
||||
&& strtolower($fq_catch_class) !== 'throwable'
|
||||
&& !$codebase->interfaceExtends($fq_catch_class, 'Throwable'))
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidCatch(
|
||||
'Class/interface ' . $fq_catch_class . ' cannot be caught',
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt),
|
||||
$fq_catch_class,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
|
||||
$fq_catch_classes[] = $fq_catch_class;
|
||||
}
|
||||
|
||||
if ($catch_context->collect_exceptions) {
|
||||
foreach ($fq_catch_classes as $fq_catch_class) {
|
||||
$fq_catch_class_lower = strtolower($fq_catch_class);
|
||||
|
||||
foreach ($catch_context->possibly_thrown_exceptions as $exception_fqcln => $_) {
|
||||
$exception_fqcln_lower = strtolower($exception_fqcln);
|
||||
|
||||
if ($exception_fqcln_lower === $fq_catch_class_lower
|
||||
|| ($codebase->classExists($exception_fqcln)
|
||||
&& $codebase->classExtendsOrImplements($exception_fqcln, $fq_catch_class))
|
||||
|| ($codebase->interfaceExists($exception_fqcln)
|
||||
&& $codebase->interfaceExtends($exception_fqcln, $fq_catch_class))
|
||||
) {
|
||||
unset($original_context->possibly_thrown_exceptions[$exception_fqcln]);
|
||||
unset($context->possibly_thrown_exceptions[$exception_fqcln]);
|
||||
unset($catch_context->possibly_thrown_exceptions[$exception_fqcln]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$catch_context->possibly_thrown_exceptions = [];
|
||||
}
|
||||
|
||||
// discard all clauses because crazy stuff may have happened in try block
|
||||
$catch_context->clauses = [];
|
||||
|
||||
if ($catch->var && is_string($catch->var->name)) {
|
||||
$catch_var_id = '$' . $catch->var->name;
|
||||
|
||||
$catch_context->vars_in_scope[$catch_var_id] = new Union(
|
||||
array_map(
|
||||
static fn(string $fq_catch_class): TNamedObject => new TNamedObject(
|
||||
$fq_catch_class,
|
||||
false,
|
||||
false,
|
||||
version_compare(PHP_VERSION, '7.0.0dev', '>=')
|
||||
&& strtolower($fq_catch_class) !== 'throwable'
|
||||
&& $codebase->interfaceExists($fq_catch_class)
|
||||
&& !$codebase->interfaceExtends($fq_catch_class, 'Throwable')
|
||||
? ['Throwable' => new TNamedObject('Throwable')]
|
||||
: [],
|
||||
),
|
||||
$fq_catch_classes,
|
||||
),
|
||||
);
|
||||
|
||||
// removes dependent vars from $context
|
||||
$catch_context->removeDescendents(
|
||||
$catch_var_id,
|
||||
$catch_context->vars_in_scope[$catch_var_id],
|
||||
$catch_context->vars_in_scope[$catch_var_id],
|
||||
$statements_analyzer,
|
||||
);
|
||||
|
||||
$catch_context->vars_possibly_in_scope[$catch_var_id] = true;
|
||||
|
||||
$location = new CodeLocation($statements_analyzer->getSource(), $catch->var);
|
||||
|
||||
if (!$statements_analyzer->hasVariable($catch_var_id)) {
|
||||
$statements_analyzer->registerVariable(
|
||||
$catch_var_id,
|
||||
$location,
|
||||
$catch_context->branch_point,
|
||||
);
|
||||
} else {
|
||||
$statements_analyzer->registerVariableAssignment(
|
||||
$catch_var_id,
|
||||
$location,
|
||||
);
|
||||
}
|
||||
|
||||
if ($statements_analyzer->data_flow_graph) {
|
||||
$catch_var_node = DataFlowNode::getForAssignment($catch_var_id, $location);
|
||||
|
||||
$catch_context->vars_in_scope[$catch_var_id] =
|
||||
$catch_context->vars_in_scope[$catch_var_id]->addParentNodes([
|
||||
$catch_var_node->id => $catch_var_node,
|
||||
])
|
||||
;
|
||||
|
||||
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) {
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$catch_var_node,
|
||||
new DataFlowNode('variable-use', 'variable use', null),
|
||||
'variable-use',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
|
||||
|
||||
foreach ($issues_to_suppress as $issue_to_suppress) {
|
||||
if (!in_array($issue_to_suppress, $suppressed_issues, true)) {
|
||||
$statements_analyzer->addSuppressedIssues([$issue_to_suppress]);
|
||||
}
|
||||
}
|
||||
|
||||
$old_catch_assigned_var_ids = $catch_context->assigned_var_ids;
|
||||
|
||||
$catch_context->assigned_var_ids = [];
|
||||
|
||||
$statements_analyzer->analyze($catch->stmts, $catch_context);
|
||||
|
||||
// recalculate in case there's a no-return clause
|
||||
$catch_actions[$i] = ScopeAnalyzer::getControlActions(
|
||||
$catch->stmts,
|
||||
$statements_analyzer->node_data,
|
||||
[],
|
||||
);
|
||||
|
||||
foreach ($issues_to_suppress as $issue_to_suppress) {
|
||||
if (!in_array($issue_to_suppress, $suppressed_issues, true)) {
|
||||
$statements_analyzer->removeSuppressedIssues([$issue_to_suppress]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<string, bool> */
|
||||
$new_catch_assigned_var_ids = $catch_context->assigned_var_ids;
|
||||
|
||||
$catch_context->assigned_var_ids += $old_catch_assigned_var_ids;
|
||||
|
||||
if ($catch_context->collect_exceptions) {
|
||||
$context->mergeExceptions($catch_context);
|
||||
}
|
||||
|
||||
$catch_doesnt_leave_parent_scope = $catch_actions[$i] !== [ScopeAnalyzer::ACTION_END]
|
||||
&& $catch_actions[$i] !== [ScopeAnalyzer::ACTION_CONTINUE]
|
||||
&& $catch_actions[$i] !== [ScopeAnalyzer::ACTION_BREAK];
|
||||
|
||||
if ($catch_doesnt_leave_parent_scope) {
|
||||
$definitely_newly_assigned_var_ids = array_intersect_key(
|
||||
$new_catch_assigned_var_ids,
|
||||
$definitely_newly_assigned_var_ids,
|
||||
);
|
||||
|
||||
foreach ($catch_context->vars_in_scope as $var_id => $type) {
|
||||
if ($try_block_control_actions === [ScopeAnalyzer::ACTION_END]) {
|
||||
$context->vars_in_scope[$var_id] = $type;
|
||||
} elseif (isset($context->vars_in_scope[$var_id])) {
|
||||
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$context->vars_in_scope[$var_id],
|
||||
$type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$context->vars_possibly_in_scope = array_merge(
|
||||
$catch_context->vars_possibly_in_scope,
|
||||
$context->vars_possibly_in_scope,
|
||||
);
|
||||
} else {
|
||||
if ($stmt->finally) {
|
||||
$context->vars_possibly_in_scope = array_merge(
|
||||
$catch_context->vars_possibly_in_scope,
|
||||
$context->vars_possibly_in_scope,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($try_context->finally_scope) {
|
||||
foreach ($catch_context->vars_in_scope as $var_id => &$type) {
|
||||
if (isset($try_context->finally_scope->vars_in_scope[$var_id])) {
|
||||
if ($try_context->finally_scope->vars_in_scope[$var_id] !== $type) {
|
||||
$try_context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$try_context->finally_scope->vars_in_scope[$var_id],
|
||||
$type,
|
||||
$statements_analyzer->getCodebase(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$try_context->finally_scope->vars_in_scope[$var_id] = $type->setPossiblyUndefined(
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
unset($type);
|
||||
}
|
||||
}
|
||||
|
||||
if ($context->loop_scope
|
||||
&& !$try_leaves_loop
|
||||
&& !in_array(ScopeAnalyzer::ACTION_NONE, $context->loop_scope->final_actions, true)
|
||||
) {
|
||||
$context->loop_scope->final_actions[] = ScopeAnalyzer::ACTION_NONE;
|
||||
}
|
||||
|
||||
$finally_has_returned = false;
|
||||
if ($stmt->finally) {
|
||||
if ($try_context->finally_scope) {
|
||||
$finally_context = clone $context;
|
||||
|
||||
$finally_context->assigned_var_ids = [];
|
||||
$finally_context->possibly_assigned_var_ids = [];
|
||||
|
||||
$finally_context->vars_in_scope = $try_context->finally_scope->vars_in_scope;
|
||||
|
||||
$statements_analyzer->analyze($stmt->finally->stmts, $finally_context);
|
||||
|
||||
$finally_has_returned = $finally_context->has_returned;
|
||||
|
||||
/** @var string $var_id */
|
||||
foreach ($finally_context->assigned_var_ids as $var_id => $_) {
|
||||
if (isset($context->vars_in_scope[$var_id])
|
||||
&& isset($finally_context->vars_in_scope[$var_id])
|
||||
) {
|
||||
$possibly_undefined = $context->vars_in_scope[$var_id]->possibly_undefined
|
||||
&& $context->vars_in_scope[$var_id]->possibly_undefined_from_try;
|
||||
|
||||
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$context->vars_in_scope[$var_id],
|
||||
$finally_context->vars_in_scope[$var_id],
|
||||
$codebase,
|
||||
);
|
||||
if ($possibly_undefined) {
|
||||
/** @psalm-suppress InaccessibleProperty We just created this type */
|
||||
$context->vars_in_scope[$var_id]->possibly_undefined = false;
|
||||
/** @psalm-suppress InaccessibleProperty We just created this type */
|
||||
$context->vars_in_scope[$var_id]->possibly_undefined_from_try = false;
|
||||
}
|
||||
} elseif (isset($finally_context->vars_in_scope[$var_id])) {
|
||||
$context->vars_in_scope[$var_id] = $finally_context->vars_in_scope[$var_id];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($definitely_newly_assigned_var_ids as $var_id => $_) {
|
||||
if (isset($context->vars_in_scope[$var_id])) {
|
||||
if ($context->vars_in_scope[$var_id]->possibly_undefined_from_try) {
|
||||
$context->vars_in_scope[$var_id] =
|
||||
$context->vars_in_scope[$var_id]->setPossiblyUndefined(
|
||||
false,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($existing_thrown_exceptions as $possibly_thrown_exception => $codelocations) {
|
||||
foreach ($codelocations as $hash => $codelocation) {
|
||||
$context->possibly_thrown_exceptions[$possibly_thrown_exception][$hash] = $codelocation;
|
||||
}
|
||||
}
|
||||
|
||||
$body_has_returned = !in_array(ScopeAnalyzer::ACTION_NONE, $try_block_control_actions, true);
|
||||
$context->has_returned = ($body_has_returned && $all_catches_leave) || $finally_has_returned;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
131
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php
vendored
Normal file
131
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Block/WhileAnalyzer.php
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Block;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Scope\LoopScope;
|
||||
use Psalm\Type;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_merge;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class WhileAnalyzer
|
||||
{
|
||||
/**
|
||||
* @return false|null
|
||||
*/
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Stmt\While_ $stmt,
|
||||
Context $context
|
||||
): ?bool {
|
||||
$while_true = ($stmt->cond instanceof PhpParser\Node\Expr\ConstFetch && $stmt->cond->name->parts === ['true'])
|
||||
|| (($t = $statements_analyzer->node_data->getType($stmt->cond))
|
||||
&& $t->isAlwaysTruthy());
|
||||
|
||||
$pre_context = null;
|
||||
|
||||
if ($while_true) {
|
||||
$pre_context = clone $context;
|
||||
}
|
||||
|
||||
$while_context = clone $context;
|
||||
|
||||
$while_context->inside_loop = true;
|
||||
$while_context->break_types[] = 'loop';
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
if ($codebase->alter_code) {
|
||||
$while_context->branch_point = $while_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
|
||||
}
|
||||
|
||||
$loop_scope = new LoopScope($while_context, $context);
|
||||
$loop_scope->protected_var_ids = $context->protected_var_ids;
|
||||
|
||||
if (LoopAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt->stmts,
|
||||
self::getAndExpressions($stmt->cond),
|
||||
[],
|
||||
$loop_scope,
|
||||
$inner_loop_context,
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$inner_loop_context) {
|
||||
throw new UnexpectedValueException('There should be an inner loop context');
|
||||
}
|
||||
|
||||
$always_enters_loop = false;
|
||||
|
||||
if ($stmt_cond_type = $statements_analyzer->node_data->getType($stmt->cond)) {
|
||||
$always_enters_loop = $stmt_cond_type->isAlwaysTruthy();
|
||||
}
|
||||
|
||||
if ($while_true) {
|
||||
$always_enters_loop = true;
|
||||
}
|
||||
|
||||
$can_leave_loop = !$while_true
|
||||
|| in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true);
|
||||
|
||||
if ($always_enters_loop && $can_leave_loop) {
|
||||
foreach ($inner_loop_context->vars_in_scope as $var_id => $type) {
|
||||
// if there are break statements in the loop it's not certain
|
||||
// that the loop has finished executing, so the assertions at the end
|
||||
// the loop in the while conditional may not hold
|
||||
if (in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true)
|
||||
|| in_array(ScopeAnalyzer::ACTION_CONTINUE, $loop_scope->final_actions, true)
|
||||
) {
|
||||
if (isset($loop_scope->possibly_defined_loop_parent_vars[$var_id])) {
|
||||
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$loop_scope->possibly_defined_loop_parent_vars[$var_id],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$context->vars_in_scope[$var_id] = $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$while_context->loop_scope = null;
|
||||
|
||||
if ($can_leave_loop) {
|
||||
$context->vars_possibly_in_scope = array_merge(
|
||||
$context->vars_possibly_in_scope,
|
||||
$while_context->vars_possibly_in_scope,
|
||||
);
|
||||
} elseif ($pre_context) {
|
||||
$context->vars_possibly_in_scope = $pre_context->vars_possibly_in_scope;
|
||||
}
|
||||
|
||||
if ($context->collect_exceptions) {
|
||||
$context->mergeExceptions($while_context);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<PhpParser\Node\Expr>
|
||||
*/
|
||||
public static function getAndExpressions(
|
||||
PhpParser\Node\Expr $expr
|
||||
): array {
|
||||
if ($expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) {
|
||||
return [...self::getAndExpressions($expr->left), ...self::getAndExpressions($expr->right)];
|
||||
}
|
||||
|
||||
return [$expr];
|
||||
}
|
||||
}
|
||||
92
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php
vendored
Normal file
92
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/BreakAnalyzer.php
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Type;
|
||||
|
||||
use function end;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class BreakAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Stmt\Break_ $stmt,
|
||||
Context $context
|
||||
): void {
|
||||
$loop_scope = $context->loop_scope;
|
||||
|
||||
$leaving_switch = true;
|
||||
|
||||
if ($loop_scope) {
|
||||
if ($context->break_types
|
||||
&& end($context->break_types) === 'switch'
|
||||
&& (!$stmt->num instanceof PhpParser\Node\Scalar\LNumber || $stmt->num->value < 2)
|
||||
) {
|
||||
$loop_scope->final_actions[] = ScopeAnalyzer::ACTION_LEAVE_SWITCH;
|
||||
} else {
|
||||
$leaving_switch = false;
|
||||
|
||||
$loop_scope->final_actions[] = ScopeAnalyzer::ACTION_BREAK;
|
||||
}
|
||||
|
||||
$redefined_vars = $context->getRedefinedVars($loop_scope->loop_parent_context->vars_in_scope);
|
||||
|
||||
foreach ($redefined_vars as $var => $type) {
|
||||
$loop_scope->possibly_redefined_loop_parent_vars[$var] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$loop_scope->possibly_redefined_loop_parent_vars[$var] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
if ($loop_scope->iteration_count === 0) {
|
||||
foreach ($context->vars_in_scope as $var_id => $type) {
|
||||
if (!isset($loop_scope->loop_parent_context->vars_in_scope[$var_id])) {
|
||||
$loop_scope->possibly_defined_loop_parent_vars[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$loop_scope->possibly_defined_loop_parent_vars[$var_id] ?? null,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($context->finally_scope) {
|
||||
foreach ($context->vars_in_scope as $var_id => &$type) {
|
||||
if (isset($context->finally_scope->vars_in_scope[$var_id])) {
|
||||
$context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$context->finally_scope->vars_in_scope[$var_id],
|
||||
$type,
|
||||
$statements_analyzer->getCodebase(),
|
||||
);
|
||||
} else {
|
||||
$type = $type->setPossiblyUndefined(true, true);
|
||||
$context->finally_scope->vars_in_scope[$var_id] = $type;
|
||||
}
|
||||
}
|
||||
unset($type);
|
||||
}
|
||||
}
|
||||
|
||||
$case_scope = $context->case_scope;
|
||||
if ($case_scope && $leaving_switch) {
|
||||
foreach ($context->vars_in_scope as $var_id => $type) {
|
||||
if ($case_scope->break_vars === null) {
|
||||
$case_scope->break_vars = [];
|
||||
}
|
||||
|
||||
$case_scope->break_vars[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$case_scope->break_vars[$var_id] ?? null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$context->has_returned = true;
|
||||
}
|
||||
}
|
||||
98
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php
vendored
Normal file
98
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/ContinueAnalyzer.php
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\ScopeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Issue\ContinueOutsideLoop;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
|
||||
use function end;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ContinueAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Stmt\Continue_ $stmt,
|
||||
Context $context
|
||||
): void {
|
||||
$count = $stmt->num instanceof PhpParser\Node\Scalar\LNumber? $stmt->num->value : 1;
|
||||
|
||||
$loop_scope = $context->loop_scope;
|
||||
|
||||
if ($count === 2 && isset($loop_scope->loop_parent_context->loop_scope)) {
|
||||
$loop_scope = $loop_scope->loop_parent_context->loop_scope;
|
||||
}
|
||||
|
||||
if ($count === 3 && isset($loop_scope->loop_parent_context->loop_scope)) {
|
||||
$loop_scope = $loop_scope->loop_parent_context->loop_scope;
|
||||
}
|
||||
|
||||
if ($loop_scope === null) {
|
||||
if (!$context->break_types) {
|
||||
if (IssueBuffer::accepts(
|
||||
new ContinueOutsideLoop(
|
||||
'Continue call outside loop context',
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
),
|
||||
$statements_analyzer->getSource()->getSuppressedIssues(),
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($context->break_types
|
||||
&& end($context->break_types) === 'switch'
|
||||
&& $count < 2
|
||||
) {
|
||||
$loop_scope->final_actions[] = ScopeAnalyzer::ACTION_LEAVE_SWITCH;
|
||||
} else {
|
||||
$loop_scope->final_actions[] = ScopeAnalyzer::ACTION_CONTINUE;
|
||||
}
|
||||
|
||||
$redefined_vars = $context->getRedefinedVars($loop_scope->loop_parent_context->vars_in_scope);
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($redefined_vars as $var => $type) {
|
||||
$loop_scope->possibly_redefined_loop_vars[$var] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$loop_scope->possibly_redefined_loop_vars[$var] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
if ($context->finally_scope) {
|
||||
foreach ($context->vars_in_scope as $var_id => &$type) {
|
||||
if (isset($context->finally_scope->vars_in_scope[$var_id])) {
|
||||
$context->finally_scope->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$context->finally_scope->vars_in_scope[$var_id],
|
||||
$type,
|
||||
$statements_analyzer->getCodebase(),
|
||||
);
|
||||
} else {
|
||||
$type = $type->setPossiblyUndefined(true, true);
|
||||
$context->finally_scope->vars_in_scope[$var_id] = $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$context->has_returned = true;
|
||||
}
|
||||
}
|
||||
127
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php
vendored
Normal file
127
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\CastAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Codebase\TaintFlowGraph;
|
||||
use Psalm\Internal\DataFlow\TaintSink;
|
||||
use Psalm\Issue\ForbiddenCode;
|
||||
use Psalm\Issue\ImpureFunctionCall;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Storage\FunctionLikeParameter;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\TaintKind;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class EchoAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Stmt\Echo_ $stmt,
|
||||
Context $context
|
||||
): bool {
|
||||
$echo_param = new FunctionLikeParameter(
|
||||
'var',
|
||||
false,
|
||||
);
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
foreach ($stmt->exprs as $i => $expr) {
|
||||
$context->inside_call = true;
|
||||
ExpressionAnalyzer::analyze($statements_analyzer, $expr, $context);
|
||||
$context->inside_call = false;
|
||||
|
||||
$expr_type = $statements_analyzer->node_data->getType($expr);
|
||||
|
||||
if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph) {
|
||||
if ($expr_type && $expr_type->hasObjectType()) {
|
||||
$expr_type = CastAnalyzer::castStringAttempt(
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
$expr_type,
|
||||
$expr,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
$call_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
|
||||
|
||||
$echo_param_sink = TaintSink::getForMethodArgument(
|
||||
'echo',
|
||||
'echo',
|
||||
(int) $i,
|
||||
null,
|
||||
$call_location,
|
||||
);
|
||||
|
||||
$echo_param_sink->taints = [
|
||||
TaintKind::INPUT_HTML,
|
||||
TaintKind::INPUT_HAS_QUOTES,
|
||||
TaintKind::USER_SECRET,
|
||||
TaintKind::SYSTEM_SECRET,
|
||||
];
|
||||
|
||||
$statements_analyzer->data_flow_graph->addSink($echo_param_sink);
|
||||
}
|
||||
|
||||
if (ArgumentAnalyzer::verifyType(
|
||||
$statements_analyzer,
|
||||
$expr_type ?? Type::getMixed(),
|
||||
Type::getString(),
|
||||
null,
|
||||
'echo',
|
||||
null,
|
||||
(int)$i,
|
||||
new CodeLocation($statements_analyzer->getSource(), $expr),
|
||||
$expr,
|
||||
$context,
|
||||
$echo_param,
|
||||
false,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($codebase->config->forbidden_functions['echo'])) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new ForbiddenCode(
|
||||
'Use of echo',
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
),
|
||||
$statements_analyzer->getSource()->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
|
||||
if (!$context->collect_initializations && !$context->collect_mutations) {
|
||||
if ($context->mutation_free || $context->external_mutation_free) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new ImpureFunctionCall(
|
||||
'Cannot call echo from a mutation-free context',
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} elseif ($statements_analyzer->getSource() instanceof FunctionLikeAnalyzer
|
||||
&& $statements_analyzer->getSource()->track_mutations
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
643
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php
vendored
Normal file
643
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php
vendored
Normal file
@@ -0,0 +1,643 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Codebase\ConstantTypeResolver;
|
||||
use Psalm\Internal\Codebase\TaintFlowGraph;
|
||||
use Psalm\Internal\Codebase\VariableUseGraph;
|
||||
use Psalm\Internal\DataFlow\DataFlowNode;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\Type\TypeCombiner;
|
||||
use Psalm\Issue\DuplicateArrayKey;
|
||||
use Psalm\Issue\InvalidArrayOffset;
|
||||
use Psalm\Issue\InvalidOperand;
|
||||
use Psalm\Issue\MixedArrayOffset;
|
||||
use Psalm\Issue\ParseError;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TArrayKey;
|
||||
use Psalm\Type\Atomic\TBool;
|
||||
use Psalm\Type\Atomic\TFalse;
|
||||
use Psalm\Type\Atomic\TFloat;
|
||||
use Psalm\Type\Atomic\TInt;
|
||||
use Psalm\Type\Atomic\TKeyedArray;
|
||||
use Psalm\Type\Atomic\TList;
|
||||
use Psalm\Type\Atomic\TLiteralClassString;
|
||||
use Psalm\Type\Atomic\TLiteralFloat;
|
||||
use Psalm\Type\Atomic\TLiteralInt;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Atomic\TMixed;
|
||||
use Psalm\Type\Atomic\TNonEmptyArray;
|
||||
use Psalm\Type\Atomic\TObjectWithProperties;
|
||||
use Psalm\Type\Atomic\TString;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Atomic\TTrue;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function is_string;
|
||||
use function preg_match;
|
||||
|
||||
use const PHP_INT_MAX;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ArrayAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\Array_ $stmt,
|
||||
Context $context
|
||||
): bool {
|
||||
// if the array is empty, this special type allows us to match any other array type against it
|
||||
if (count($stmt->items) === 0) {
|
||||
$statements_analyzer->node_data->setType($stmt, Type::getEmptyArray());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$array_creation_info = new ArrayCreationInfo();
|
||||
|
||||
foreach ($stmt->items as $item) {
|
||||
if ($item === null) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new ParseError(
|
||||
'Array element cannot be empty',
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
),
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
self::analyzeArrayItem(
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
$array_creation_info,
|
||||
$item,
|
||||
$codebase,
|
||||
);
|
||||
}
|
||||
|
||||
if (count($array_creation_info->item_key_atomic_types) !== 0) {
|
||||
$item_key_type = TypeCombiner::combine(
|
||||
$array_creation_info->item_key_atomic_types,
|
||||
$codebase,
|
||||
);
|
||||
} else {
|
||||
$item_key_type = null;
|
||||
}
|
||||
|
||||
if (count($array_creation_info->item_value_atomic_types) !== 0) {
|
||||
$item_value_type = TypeCombiner::combine(
|
||||
$array_creation_info->item_value_atomic_types,
|
||||
$codebase,
|
||||
);
|
||||
} else {
|
||||
$item_value_type = null;
|
||||
}
|
||||
|
||||
// if this array looks like an object-like array, let's return that instead
|
||||
if (count($array_creation_info->property_types) !== 0) {
|
||||
$atomic_type = new TKeyedArray(
|
||||
$array_creation_info->property_types,
|
||||
$array_creation_info->class_strings,
|
||||
$array_creation_info->can_create_objectlike
|
||||
? null :
|
||||
[$item_key_type ?? Type::getArrayKey(), $item_value_type ?? Type::getMixed()],
|
||||
$array_creation_info->all_list,
|
||||
);
|
||||
|
||||
$stmt_type = new Union([$atomic_type], [
|
||||
'parent_nodes' => $array_creation_info->parent_taint_nodes,
|
||||
]);
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, $stmt_type);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($item_key_type === null && $item_value_type === null) {
|
||||
$statements_analyzer->node_data->setType($stmt, Type::getEmptyArray());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($array_creation_info->all_list) {
|
||||
if ($array_creation_info->can_be_empty) {
|
||||
$array_type = Type::getListAtomic($item_value_type ?? Type::getMixed());
|
||||
} else {
|
||||
$array_type = Type::getNonEmptyListAtomic($item_value_type ?? Type::getMixed());
|
||||
}
|
||||
|
||||
$stmt_type = new Union([
|
||||
$array_type,
|
||||
], [
|
||||
'parent_nodes' => $array_creation_info->parent_taint_nodes,
|
||||
]);
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, $stmt_type);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($item_key_type) {
|
||||
$bad_types = [];
|
||||
$good_types = [];
|
||||
|
||||
foreach ($item_key_type->getAtomicTypes() as $atomic_key_type) {
|
||||
if ($atomic_key_type instanceof TMixed) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new MixedArrayOffset(
|
||||
'Cannot create mixed offset – expecting array-key',
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
$bad_types[] = $atomic_key_type;
|
||||
|
||||
$good_types[] = new TArrayKey;
|
||||
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$atomic_key_type instanceof TString
|
||||
&& !$atomic_key_type instanceof TInt
|
||||
&& !$atomic_key_type instanceof TArrayKey
|
||||
&& !$atomic_key_type instanceof TTemplateParam
|
||||
&& !(
|
||||
$atomic_key_type instanceof TObjectWithProperties
|
||||
&& isset($atomic_key_type->methods['__tostring'])
|
||||
)
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidArrayOffset(
|
||||
'Cannot create offset of type ' . $item_key_type->getKey() . ', expecting array-key',
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
$bad_types[] = $atomic_key_type;
|
||||
|
||||
if ($atomic_key_type instanceof TFalse) {
|
||||
$good_types[] = new TLiteralInt(0);
|
||||
} elseif ($atomic_key_type instanceof TTrue) {
|
||||
$good_types[] = new TLiteralInt(1);
|
||||
} elseif ($atomic_key_type instanceof TBool) {
|
||||
$good_types[] = new TLiteralInt(0);
|
||||
$good_types[] = new TLiteralInt(1);
|
||||
} elseif ($atomic_key_type instanceof TLiteralFloat) {
|
||||
$good_types[] = new TLiteralInt((int) $atomic_key_type->value);
|
||||
} elseif ($atomic_key_type instanceof TFloat) {
|
||||
$good_types[] = new TInt;
|
||||
} else {
|
||||
$good_types[] = new TArrayKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($bad_types && $good_types) {
|
||||
$item_key_type = $item_key_type->getBuilder()->substitute(
|
||||
TypeCombiner::combine($bad_types, $codebase),
|
||||
TypeCombiner::combine($good_types, $codebase),
|
||||
)->freeze();
|
||||
}
|
||||
}
|
||||
|
||||
$array_args = [
|
||||
$item_key_type && !$item_key_type->hasMixed() ? $item_key_type : Type::getArrayKey(),
|
||||
$item_value_type ?? Type::getMixed(),
|
||||
];
|
||||
$array_type = $array_creation_info->can_be_empty ? new TArray($array_args) : new TNonEmptyArray($array_args);
|
||||
|
||||
$stmt_type = new Union([
|
||||
$array_type,
|
||||
], [
|
||||
'parent_nodes' => $array_creation_info->parent_taint_nodes,
|
||||
]);
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, $stmt_type);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function analyzeArrayItem(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
Context $context,
|
||||
ArrayCreationInfo $array_creation_info,
|
||||
PhpParser\Node\Expr\ArrayItem $item,
|
||||
Codebase $codebase
|
||||
): void {
|
||||
if ($item->unpack) {
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $item->value, $context) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$unpacked_array_type = $statements_analyzer->node_data->getType($item->value);
|
||||
|
||||
if (!$unpacked_array_type) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::handleUnpackedArray(
|
||||
$statements_analyzer,
|
||||
$array_creation_info,
|
||||
$item,
|
||||
$unpacked_array_type,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
if (($data_flow_graph = $statements_analyzer->data_flow_graph)
|
||||
&& $data_flow_graph instanceof VariableUseGraph
|
||||
&& $unpacked_array_type->parent_nodes
|
||||
) {
|
||||
$var_location = new CodeLocation($statements_analyzer->getSource(), $item->value);
|
||||
|
||||
$new_parent_node = DataFlowNode::getForAssignment(
|
||||
'array',
|
||||
$var_location,
|
||||
);
|
||||
|
||||
$data_flow_graph->addNode($new_parent_node);
|
||||
|
||||
foreach ($unpacked_array_type->parent_nodes as $parent_node) {
|
||||
$data_flow_graph->addPath(
|
||||
$parent_node,
|
||||
$new_parent_node,
|
||||
'arrayvalue-assignment',
|
||||
);
|
||||
}
|
||||
|
||||
$array_creation_info->parent_taint_nodes += [$new_parent_node->id => $new_parent_node];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$item_key_value = null;
|
||||
$item_key_type = null;
|
||||
$item_is_list_item = false;
|
||||
|
||||
$array_creation_info->can_be_empty = false;
|
||||
|
||||
if ($item->key) {
|
||||
$was_inside_general_use = $context->inside_general_use;
|
||||
$context->inside_general_use = true;
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $item->key, $context) === false) {
|
||||
$context->inside_general_use = $was_inside_general_use;
|
||||
|
||||
return;
|
||||
}
|
||||
$context->inside_general_use = $was_inside_general_use;
|
||||
|
||||
if ($item_key_type = $statements_analyzer->node_data->getType($item->key)) {
|
||||
$key_type = $item_key_type;
|
||||
|
||||
if ($key_type->isNull()) {
|
||||
$key_type = Type::getString('');
|
||||
}
|
||||
|
||||
if ($item->key instanceof PhpParser\Node\Scalar\String_
|
||||
&& preg_match('/^(0|[1-9][0-9]*)$/', $item->key->value)
|
||||
&& (
|
||||
(int) $item->key->value < PHP_INT_MAX ||
|
||||
$item->key->value === (string) PHP_INT_MAX
|
||||
)
|
||||
) {
|
||||
$key_type = Type::getInt(false, (int) $item->key->value);
|
||||
}
|
||||
|
||||
if ($key_type->isSingleStringLiteral()) {
|
||||
$item_key_literal_type = $key_type->getSingleStringLiteral();
|
||||
$item_key_value = $item_key_literal_type->value;
|
||||
|
||||
if ($item_key_literal_type instanceof TLiteralClassString) {
|
||||
$array_creation_info->class_strings[$item_key_value] = true;
|
||||
}
|
||||
} elseif ($key_type->isSingleIntLiteral()) {
|
||||
$item_key_value = $key_type->getSingleIntLiteral()->value;
|
||||
|
||||
if ($item_key_value >= $array_creation_info->int_offset) {
|
||||
if ($item_key_value === $array_creation_info->int_offset) {
|
||||
$item_is_list_item = true;
|
||||
}
|
||||
$array_creation_info->int_offset = $item_key_value + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$key_type = Type::getArrayKey();
|
||||
}
|
||||
} else {
|
||||
$item_is_list_item = true;
|
||||
$item_key_value = $array_creation_info->int_offset++;
|
||||
$key_atomic_type = new TLiteralInt($item_key_value);
|
||||
$array_creation_info->item_key_atomic_types[] = $key_atomic_type;
|
||||
$key_type = new Union([$key_atomic_type]);
|
||||
}
|
||||
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $item->value, $context) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$array_creation_info->all_list = $array_creation_info->all_list && $item_is_list_item;
|
||||
|
||||
if ($item_key_value !== null) {
|
||||
if (isset($array_creation_info->array_keys[$item_key_value])) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new DuplicateArrayKey(
|
||||
'Key \'' . $item_key_value . '\' already exists on array',
|
||||
new CodeLocation($statements_analyzer->getSource(), $item),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
|
||||
$array_creation_info->array_keys[$item_key_value] = true;
|
||||
}
|
||||
|
||||
|
||||
if (($data_flow_graph = $statements_analyzer->data_flow_graph)
|
||||
&& ($data_flow_graph instanceof VariableUseGraph
|
||||
|| !in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()))
|
||||
) {
|
||||
if ($item_value_type = $statements_analyzer->node_data->getType($item->value)) {
|
||||
if ($item_value_type->parent_nodes
|
||||
&& !($item_value_type->isSingle()
|
||||
&& $item_value_type->hasLiteralValue()
|
||||
&& $data_flow_graph instanceof TaintFlowGraph)
|
||||
) {
|
||||
$var_location = new CodeLocation($statements_analyzer->getSource(), $item);
|
||||
|
||||
$new_parent_node = DataFlowNode::getForAssignment(
|
||||
'array'
|
||||
. ($item_key_value !== null ? '[\'' . $item_key_value . '\']' : ''),
|
||||
$var_location,
|
||||
);
|
||||
|
||||
$data_flow_graph->addNode($new_parent_node);
|
||||
|
||||
$event = new AddRemoveTaintsEvent($item, $context, $statements_analyzer, $codebase);
|
||||
|
||||
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
|
||||
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
|
||||
|
||||
foreach ($item_value_type->parent_nodes as $parent_node) {
|
||||
$data_flow_graph->addPath(
|
||||
$parent_node,
|
||||
$new_parent_node,
|
||||
'arrayvalue-assignment'
|
||||
. ($item_key_value !== null ? '-\'' . $item_key_value . '\'' : ''),
|
||||
$added_taints,
|
||||
$removed_taints,
|
||||
);
|
||||
}
|
||||
|
||||
$array_creation_info->parent_taint_nodes += [$new_parent_node->id => $new_parent_node];
|
||||
}
|
||||
|
||||
if ($item_key_type
|
||||
&& $item_key_type->parent_nodes
|
||||
&& $item_key_value === null
|
||||
&& !($item_key_type->isSingle()
|
||||
&& $item_key_type->hasLiteralValue()
|
||||
&& $data_flow_graph instanceof TaintFlowGraph)
|
||||
) {
|
||||
$var_location = new CodeLocation($statements_analyzer->getSource(), $item);
|
||||
|
||||
$new_parent_node = DataFlowNode::getForAssignment(
|
||||
'array',
|
||||
$var_location,
|
||||
);
|
||||
|
||||
$data_flow_graph->addNode($new_parent_node);
|
||||
|
||||
$event = new AddRemoveTaintsEvent($item, $context, $statements_analyzer, $codebase);
|
||||
|
||||
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
|
||||
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
|
||||
|
||||
foreach ($item_key_type->parent_nodes as $parent_node) {
|
||||
$data_flow_graph->addPath(
|
||||
$parent_node,
|
||||
$new_parent_node,
|
||||
'arraykey-assignment',
|
||||
$added_taints,
|
||||
$removed_taints,
|
||||
);
|
||||
}
|
||||
|
||||
$array_creation_info->parent_taint_nodes += [$new_parent_node->id => $new_parent_node];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($item->byRef) {
|
||||
$var_id = ExpressionIdentifier::getExtendedVarId(
|
||||
$item->value,
|
||||
$statements_analyzer->getFQCLN(),
|
||||
$statements_analyzer,
|
||||
);
|
||||
|
||||
if ($var_id) {
|
||||
if (isset($context->vars_in_scope[$var_id])) {
|
||||
$context->removeDescendents(
|
||||
$var_id,
|
||||
$context->vars_in_scope[$var_id],
|
||||
null,
|
||||
$statements_analyzer,
|
||||
);
|
||||
}
|
||||
|
||||
$context->vars_in_scope[$var_id] = Type::getMixed();
|
||||
}
|
||||
}
|
||||
|
||||
$config = $codebase->config;
|
||||
|
||||
if ($item_value_type = $statements_analyzer->node_data->getType($item->value)) {
|
||||
if ($item_key_value !== null
|
||||
&& count($array_creation_info->property_types) <= $config->max_shaped_array_size
|
||||
) {
|
||||
$array_creation_info->property_types[$item_key_value] = $item_value_type;
|
||||
} else {
|
||||
$array_creation_info->can_create_objectlike = false;
|
||||
$array_creation_info->item_key_atomic_types = array_merge(
|
||||
$array_creation_info->item_key_atomic_types,
|
||||
array_values($key_type->getAtomicTypes()),
|
||||
);
|
||||
$array_creation_info->item_value_atomic_types = array_merge(
|
||||
$array_creation_info->item_value_atomic_types,
|
||||
array_values($item_value_type->getAtomicTypes()),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if ($item_key_value !== null
|
||||
&& count($array_creation_info->property_types) <= $config->max_shaped_array_size
|
||||
) {
|
||||
$array_creation_info->property_types[$item_key_value] = Type::getMixed();
|
||||
} else {
|
||||
$array_creation_info->can_create_objectlike = false;
|
||||
$array_creation_info->item_key_atomic_types = array_merge(
|
||||
$array_creation_info->item_key_atomic_types,
|
||||
array_values($key_type->getAtomicTypes()),
|
||||
);
|
||||
$array_creation_info->item_value_atomic_types[] = new TMixed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function handleUnpackedArray(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
ArrayCreationInfo $array_creation_info,
|
||||
PhpParser\Node\Expr\ArrayItem $item,
|
||||
Union $unpacked_array_type,
|
||||
Codebase $codebase
|
||||
): void {
|
||||
$all_non_empty = true;
|
||||
|
||||
$has_possibly_undefined = false;
|
||||
foreach ($unpacked_array_type->getAtomicTypes() as $unpacked_atomic_type) {
|
||||
if ($unpacked_atomic_type instanceof TList) {
|
||||
$unpacked_atomic_type = $unpacked_atomic_type->getKeyedArray();
|
||||
}
|
||||
if ($unpacked_atomic_type instanceof TKeyedArray) {
|
||||
foreach ($unpacked_atomic_type->properties as $key => $property_value) {
|
||||
if ($property_value->possibly_undefined) {
|
||||
$has_possibly_undefined = true;
|
||||
continue;
|
||||
}
|
||||
if (is_string($key)) {
|
||||
if ($codebase->analysis_php_version_id <= 8_00_00) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new DuplicateArrayKey(
|
||||
'String keys are not supported in unpacked arrays',
|
||||
new CodeLocation($statements_analyzer->getSource(), $item->value),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
continue 2;
|
||||
}
|
||||
$new_offset = $key;
|
||||
$array_creation_info->item_key_atomic_types[] = new TLiteralString($new_offset);
|
||||
$array_creation_info->all_list = false;
|
||||
} else {
|
||||
$new_offset = $array_creation_info->int_offset++;
|
||||
$array_creation_info->item_key_atomic_types[] = new TLiteralInt($new_offset);
|
||||
}
|
||||
|
||||
$array_creation_info->array_keys[$new_offset] = true;
|
||||
$array_creation_info->property_types[$new_offset] = $property_value;
|
||||
}
|
||||
|
||||
if (!$unpacked_atomic_type->isNonEmpty()) {
|
||||
$all_non_empty = false;
|
||||
}
|
||||
|
||||
if ($has_possibly_undefined) {
|
||||
$unpacked_atomic_type = $unpacked_atomic_type->getGenericArrayType();
|
||||
} elseif (!$unpacked_atomic_type->fallback_params) {
|
||||
continue;
|
||||
}
|
||||
} elseif (!$unpacked_atomic_type instanceof TNonEmptyArray) {
|
||||
$all_non_empty = false;
|
||||
}
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
if (!$unpacked_atomic_type->isIterable($codebase)) {
|
||||
$array_creation_info->can_create_objectlike = false;
|
||||
$array_creation_info->item_key_atomic_types[] = new TArrayKey();
|
||||
$array_creation_info->item_value_atomic_types[] = new TMixed();
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidOperand(
|
||||
"Cannot use spread operator on non-iterable type {$unpacked_array_type->getId()}",
|
||||
new CodeLocation($statements_analyzer->getSource(), $item->value),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$iterable_type = $unpacked_atomic_type->getIterable($codebase);
|
||||
|
||||
if ($iterable_type->type_params[0]->isNever()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$array_creation_info->can_create_objectlike = false;
|
||||
|
||||
if (!UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$iterable_type->type_params[0],
|
||||
Type::getArrayKey(),
|
||||
)) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidOperand(
|
||||
"Cannot use spread operator on iterable with key type "
|
||||
. $iterable_type->type_params[0]->getId(),
|
||||
new CodeLocation($statements_analyzer->getSource(), $item->value),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($iterable_type->type_params[0]->hasString()) {
|
||||
if ($codebase->analysis_php_version_id <= 8_00_00) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new DuplicateArrayKey(
|
||||
'String keys are not supported in unpacked arrays',
|
||||
new CodeLocation($statements_analyzer->getSource(), $item->value),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
$array_creation_info->all_list = false;
|
||||
}
|
||||
|
||||
// Unpacked array might overwrite known properties, so values are merged when the keys intersect.
|
||||
foreach ($array_creation_info->property_types as $prop_key_val => $prop_val) {
|
||||
$prop_key = new Union([ConstantTypeResolver::getLiteralTypeFromScalarValue($prop_key_val)]);
|
||||
// Since $prop_key is a single literal type, the types intersect iff $prop_key is contained by the
|
||||
// template type (ie $prop_key cannot overlap with the template type without being contained by it).
|
||||
if (UnionTypeComparator::isContainedBy($codebase, $prop_key, $iterable_type->type_params[0])) {
|
||||
$new_prop_val = Type::combineUnionTypes($prop_val, $iterable_type->type_params[1]);
|
||||
$array_creation_info->property_types[$prop_key_val] = $new_prop_val;
|
||||
}
|
||||
}
|
||||
|
||||
$array_creation_info->item_key_atomic_types = array_merge(
|
||||
$array_creation_info->item_key_atomic_types,
|
||||
array_values($iterable_type->type_params[0]->getAtomicTypes()),
|
||||
);
|
||||
$array_creation_info->item_value_atomic_types = array_merge(
|
||||
$array_creation_info->item_value_atomic_types,
|
||||
array_values($iterable_type->type_params[1]->getAtomicTypes()),
|
||||
);
|
||||
}
|
||||
|
||||
if ($all_non_empty) {
|
||||
$array_creation_info->can_be_empty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayCreationInfo.php
vendored
Normal file
51
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayCreationInfo.php
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression;
|
||||
|
||||
use Psalm\Internal\DataFlow\DataFlowNode;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ArrayCreationInfo
|
||||
{
|
||||
/**
|
||||
* @var list<Atomic>
|
||||
*/
|
||||
public array $item_key_atomic_types = [];
|
||||
|
||||
/**
|
||||
* @var list<Atomic>
|
||||
*/
|
||||
public array $item_value_atomic_types = [];
|
||||
|
||||
/**
|
||||
* @var array<int|string, Union>
|
||||
*/
|
||||
public array $property_types = [];
|
||||
|
||||
/**
|
||||
* @var array<string, true>
|
||||
*/
|
||||
public array $class_strings = [];
|
||||
|
||||
public bool $can_create_objectlike = true;
|
||||
|
||||
/**
|
||||
* @var array<int|string, true>
|
||||
*/
|
||||
public array $array_keys = [];
|
||||
|
||||
public int $int_offset = 0;
|
||||
|
||||
public bool $all_list = true;
|
||||
|
||||
/**
|
||||
* @var array<string, DataFlowNode>
|
||||
*/
|
||||
public array $parent_taint_nodes = [];
|
||||
|
||||
public bool $can_be_empty = true;
|
||||
}
|
||||
4181
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php
vendored
Normal file
4181
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\Assignment;
|
||||
|
||||
use Psalm\Type\Union;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class AssignedProperty
|
||||
{
|
||||
public Union $property_type;
|
||||
|
||||
public string $id;
|
||||
|
||||
public Union $assignment_type;
|
||||
|
||||
public function __construct(
|
||||
Union $property_type,
|
||||
string $id,
|
||||
Union $assignment_type
|
||||
) {
|
||||
$this->property_type = $property_type;
|
||||
$this->id = $id;
|
||||
$this->assignment_type = $assignment_type;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,350 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\Assignment;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\FileManipulation;
|
||||
use Psalm\Internal\Analyzer\ClassAnalyzer;
|
||||
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\Type\TypeExpander;
|
||||
use Psalm\Issue\ImplicitToStringCast;
|
||||
use Psalm\Issue\InvalidPropertyAssignmentValue;
|
||||
use Psalm\Issue\MixedPropertyTypeCoercion;
|
||||
use Psalm\Issue\PossiblyInvalidPropertyAssignmentValue;
|
||||
use Psalm\Issue\PropertyTypeCoercion;
|
||||
use Psalm\Issue\UndefinedPropertyAssignment;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TClassString;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function explode;
|
||||
use function strtolower;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class StaticPropertyAssignmentAnalyzer
|
||||
{
|
||||
/**
|
||||
* @return false|null
|
||||
*/
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\StaticPropertyFetch $stmt,
|
||||
?PhpParser\Node\Expr $assignment_value,
|
||||
Union $assignment_value_type,
|
||||
Context $context
|
||||
): ?bool {
|
||||
$var_id = ExpressionIdentifier::getExtendedVarId(
|
||||
$stmt,
|
||||
$context->self,
|
||||
$statements_analyzer,
|
||||
);
|
||||
|
||||
$lhs_type = $statements_analyzer->node_data->getType($stmt->class);
|
||||
|
||||
if (!$lhs_type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$prop_name = $stmt->name;
|
||||
|
||||
foreach ($lhs_type->getAtomicTypes() as $lhs_atomic_type) {
|
||||
if ($lhs_atomic_type instanceof TClassString) {
|
||||
if (!$lhs_atomic_type->as_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$lhs_atomic_type = $lhs_atomic_type->as_type;
|
||||
}
|
||||
|
||||
if (!$lhs_atomic_type instanceof TNamedObject) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fq_class_name = $lhs_atomic_type->value;
|
||||
|
||||
if (!$prop_name instanceof PhpParser\Node\Identifier) {
|
||||
$was_inside_general_use = $context->inside_general_use;
|
||||
|
||||
$context->inside_general_use = true;
|
||||
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $prop_name, $context) === false) {
|
||||
$context->inside_general_use = $was_inside_general_use;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$context->inside_general_use = $was_inside_general_use;
|
||||
|
||||
if (!$context->ignore_variable_property) {
|
||||
$codebase->analyzer->addMixedMemberName(
|
||||
strtolower($fq_class_name) . '::$',
|
||||
$context->calling_method_id ?: $statements_analyzer->getFileName(),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$property_id = $fq_class_name . '::$' . $prop_name;
|
||||
|
||||
if ($codebase->store_node_types
|
||||
&& !$context->collect_initializations
|
||||
&& !$context->collect_mutations
|
||||
) {
|
||||
$codebase->analyzer->addNodeReference(
|
||||
$statements_analyzer->getFilePath(),
|
||||
$stmt->class,
|
||||
$fq_class_name,
|
||||
);
|
||||
|
||||
$codebase->analyzer->addNodeReference(
|
||||
$statements_analyzer->getFilePath(),
|
||||
$stmt->name,
|
||||
$property_id,
|
||||
);
|
||||
}
|
||||
|
||||
if (!$codebase->properties->propertyExists($property_id, false, $statements_analyzer, $context)) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new UndefinedPropertyAssignment(
|
||||
'Static property ' . $property_id . ' is not defined',
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt),
|
||||
$property_id,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ClassLikeAnalyzer::checkPropertyVisibility(
|
||||
$property_id,
|
||||
$context,
|
||||
$statements_analyzer,
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$declaring_property_class = (string) $codebase->properties->getDeclaringClassForProperty(
|
||||
$fq_class_name . '::$' . $prop_name->name,
|
||||
false,
|
||||
);
|
||||
|
||||
$declaring_property_id = strtolower($declaring_property_class) . '::$' . $prop_name;
|
||||
|
||||
if ($codebase->alter_code && $stmt->class instanceof PhpParser\Node\Name) {
|
||||
$moved_class = $codebase->classlikes->handleClassLikeReferenceInMigration(
|
||||
$codebase,
|
||||
$statements_analyzer,
|
||||
$stmt->class,
|
||||
$fq_class_name,
|
||||
$context->calling_method_id,
|
||||
);
|
||||
|
||||
if (!$moved_class) {
|
||||
foreach ($codebase->property_transforms as $original_pattern => $transformation) {
|
||||
if ($declaring_property_id === $original_pattern) {
|
||||
[$old_declaring_fq_class_name] = explode('::$', $declaring_property_id);
|
||||
[$new_fq_class_name, $new_property_name] = explode('::$', $transformation);
|
||||
|
||||
$file_manipulations = [];
|
||||
|
||||
if (strtolower($new_fq_class_name) !== $old_declaring_fq_class_name) {
|
||||
$file_manipulations[] = new FileManipulation(
|
||||
(int) $stmt->class->getAttribute('startFilePos'),
|
||||
(int) $stmt->class->getAttribute('endFilePos') + 1,
|
||||
Type::getStringFromFQCLN(
|
||||
$new_fq_class_name,
|
||||
$statements_analyzer->getNamespace(),
|
||||
$statements_analyzer->getAliasedClassesFlipped(),
|
||||
null,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$file_manipulations[] = new FileManipulation(
|
||||
(int) $stmt->name->getAttribute('startFilePos'),
|
||||
(int) $stmt->name->getAttribute('endFilePos') + 1,
|
||||
'$' . $new_property_name,
|
||||
);
|
||||
|
||||
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$class_storage = $codebase->classlike_storage_provider->get($declaring_property_class);
|
||||
|
||||
if ($var_id) {
|
||||
$context->vars_in_scope[$var_id] = $assignment_value_type;
|
||||
}
|
||||
|
||||
InstancePropertyAssignmentAnalyzer::taintUnspecializedProperty(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$property_id,
|
||||
$class_storage,
|
||||
$assignment_value_type,
|
||||
$context,
|
||||
null,
|
||||
);
|
||||
|
||||
$class_property_type = $codebase->properties->getPropertyType(
|
||||
$property_id,
|
||||
true,
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
);
|
||||
|
||||
if (!$class_property_type) {
|
||||
$class_property_type = Type::getMixed();
|
||||
|
||||
$source_analyzer = $statements_analyzer->getSource()->getSource();
|
||||
|
||||
$prop_name_name = $prop_name->name;
|
||||
|
||||
if ($source_analyzer instanceof ClassAnalyzer
|
||||
&& $fq_class_name === $source_analyzer->getFQCLN()
|
||||
) {
|
||||
$source_analyzer->inferred_property_types[$prop_name_name] = Type::combineUnionTypes(
|
||||
$assignment_value_type,
|
||||
$source_analyzer->inferred_property_types[$prop_name_name] ?? null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($assignment_value_type->hasMixed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($class_property_type->hasMixed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class_property_type = TypeExpander::expandUnion(
|
||||
$codebase,
|
||||
$class_property_type,
|
||||
$fq_class_name,
|
||||
$fq_class_name,
|
||||
$class_storage->parent_class,
|
||||
);
|
||||
|
||||
$union_comparison_results = new TypeComparisonResult();
|
||||
|
||||
$type_match_found = UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$assignment_value_type,
|
||||
$class_property_type,
|
||||
true,
|
||||
true,
|
||||
$union_comparison_results,
|
||||
);
|
||||
|
||||
if ($union_comparison_results->type_coerced) {
|
||||
if ($union_comparison_results->type_coerced_from_mixed) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new MixedPropertyTypeCoercion(
|
||||
$var_id . ' expects \'' . $class_property_type->getId() . '\', '
|
||||
. ' parent type `' . $assignment_value_type->getId() . '` provided',
|
||||
new CodeLocation(
|
||||
$statements_analyzer->getSource(),
|
||||
$assignment_value ?? $stmt,
|
||||
$context->include_location,
|
||||
),
|
||||
$property_id,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
new PropertyTypeCoercion(
|
||||
$var_id . ' expects \'' . $class_property_type->getId() . '\', '
|
||||
. ' parent type \'' . $assignment_value_type->getId() . '\' provided',
|
||||
new CodeLocation(
|
||||
$statements_analyzer->getSource(),
|
||||
$assignment_value ?? $stmt,
|
||||
$context->include_location,
|
||||
),
|
||||
$property_id,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($union_comparison_results->to_string_cast) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new ImplicitToStringCast(
|
||||
$var_id . ' expects \'' . $class_property_type . '\', '
|
||||
. '\'' . $assignment_value_type . '\' provided with a __toString method',
|
||||
new CodeLocation(
|
||||
$statements_analyzer->getSource(),
|
||||
$assignment_value ?? $stmt,
|
||||
$context->include_location,
|
||||
),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
|
||||
if (!$type_match_found && !$union_comparison_results->type_coerced) {
|
||||
if (UnionTypeComparator::canBeContainedBy($codebase, $assignment_value_type, $class_property_type)) {
|
||||
if (IssueBuffer::accepts(
|
||||
new PossiblyInvalidPropertyAssignmentValue(
|
||||
$var_id . ' with declared type \''
|
||||
. $class_property_type->getId() . '\' cannot be assigned type \''
|
||||
. $assignment_value_type->getId() . '\'',
|
||||
new CodeLocation(
|
||||
$statements_analyzer->getSource(),
|
||||
$assignment_value ?? $stmt,
|
||||
),
|
||||
$property_id,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidPropertyAssignmentValue(
|
||||
$var_id . ' with declared type \'' . $class_property_type->getId()
|
||||
. '\' cannot be assigned type \''
|
||||
. $assignment_value_type->getId() . '\'',
|
||||
new CodeLocation(
|
||||
$statements_analyzer->getSource(),
|
||||
$assignment_value ?? $stmt,
|
||||
),
|
||||
$property_id,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($var_id) {
|
||||
$context->vars_in_scope[$var_id] = $assignment_value_type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
1828
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php
vendored
Normal file
1828
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
225
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/AndAnalyzer.php
vendored
Normal file
225
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/AndAnalyzer.php
vendored
Normal file
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\BinaryOp;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Algebra\FormulaGenerator;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\IfConditionalAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\IfElseAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Clause;
|
||||
use Psalm\Node\Stmt\VirtualExpression;
|
||||
use Psalm\Node\Stmt\VirtualIf;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
use function array_diff_key;
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class AndAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\BinaryOp $stmt,
|
||||
Context $context,
|
||||
bool $from_stmt = false
|
||||
): bool {
|
||||
if ($from_stmt) {
|
||||
$fake_if_stmt = new VirtualIf(
|
||||
$stmt->left,
|
||||
[
|
||||
'stmts' => [
|
||||
new VirtualExpression(
|
||||
$stmt->right,
|
||||
),
|
||||
],
|
||||
],
|
||||
$stmt->getAttributes(),
|
||||
);
|
||||
|
||||
return IfElseAnalyzer::analyze($statements_analyzer, $fake_if_stmt, $context) !== false;
|
||||
}
|
||||
|
||||
$pre_referenced_var_ids = $context->cond_referenced_var_ids;
|
||||
|
||||
$pre_assigned_var_ids = $context->assigned_var_ids;
|
||||
|
||||
$left_context = clone $context;
|
||||
|
||||
$left_context->cond_referenced_var_ids = [];
|
||||
$left_context->assigned_var_ids = [];
|
||||
|
||||
/** @var list<string> $left_context->reconciled_expression_clauses */
|
||||
$left_context->reconciled_expression_clauses = [];
|
||||
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->left, $left_context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IfConditionalAnalyzer::handleParadoxicalCondition($statements_analyzer, $stmt->left);
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$left_cond_id = spl_object_id($stmt->left);
|
||||
|
||||
$left_clauses = FormulaGenerator::getFormula(
|
||||
$left_cond_id,
|
||||
$left_cond_id,
|
||||
$stmt->left,
|
||||
$context->self,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
foreach ($left_context->vars_in_scope as $var_id => $type) {
|
||||
if (isset($left_context->assigned_var_ids[$var_id])) {
|
||||
$context->vars_in_scope[$var_id] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<string, bool> */
|
||||
$left_referenced_var_ids = $left_context->cond_referenced_var_ids;
|
||||
$context->cond_referenced_var_ids = array_merge($pre_referenced_var_ids, $left_referenced_var_ids);
|
||||
|
||||
$left_assigned_var_ids = array_diff_key($left_context->assigned_var_ids, $pre_assigned_var_ids);
|
||||
|
||||
$left_referenced_var_ids = array_diff_key($left_referenced_var_ids, $left_assigned_var_ids);
|
||||
|
||||
$context_clauses = array_merge($left_context->clauses, $left_clauses);
|
||||
|
||||
if ($left_context->reconciled_expression_clauses) {
|
||||
$reconciled_expression_clauses = $left_context->reconciled_expression_clauses;
|
||||
|
||||
$context_clauses = array_values(
|
||||
array_filter(
|
||||
$context_clauses,
|
||||
static fn(Clause $c): bool => !in_array($c->hash, $reconciled_expression_clauses, true)
|
||||
),
|
||||
);
|
||||
|
||||
if (count($context_clauses) === 1
|
||||
&& $context_clauses[0]->wedge
|
||||
&& !$context_clauses[0]->possibilities
|
||||
) {
|
||||
$context_clauses = [];
|
||||
}
|
||||
}
|
||||
|
||||
$simplified_clauses = Algebra::simplifyCNF($context_clauses);
|
||||
|
||||
$active_left_assertions = [];
|
||||
|
||||
$left_type_assertions = Algebra::getTruthsFromFormula(
|
||||
$simplified_clauses,
|
||||
$left_cond_id,
|
||||
$left_referenced_var_ids,
|
||||
$active_left_assertions,
|
||||
);
|
||||
|
||||
$changed_var_ids = [];
|
||||
|
||||
if ($left_type_assertions) {
|
||||
$right_context = clone $context;
|
||||
// while in an and, we allow scope to boil over to support
|
||||
// statements of the form if ($x && $x->foo())
|
||||
[$right_context->vars_in_scope, $right_context->references_in_scope] = Reconciler::reconcileKeyedTypes(
|
||||
$left_type_assertions,
|
||||
$active_left_assertions,
|
||||
$right_context->vars_in_scope,
|
||||
$context->references_in_scope,
|
||||
$changed_var_ids,
|
||||
$left_referenced_var_ids,
|
||||
$statements_analyzer,
|
||||
$statements_analyzer->getTemplateTypeMap() ?: [],
|
||||
$context->inside_loop,
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt->left),
|
||||
$context->inside_negation,
|
||||
);
|
||||
} else {
|
||||
$right_context = clone $left_context;
|
||||
}
|
||||
|
||||
$partitioned_clauses = Context::removeReconciledClauses(
|
||||
[...$left_context->clauses, ...$left_clauses],
|
||||
$changed_var_ids,
|
||||
);
|
||||
|
||||
$right_context->clauses = $partitioned_clauses[0];
|
||||
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->right, $right_context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IfConditionalAnalyzer::handleParadoxicalCondition($statements_analyzer, $stmt->right);
|
||||
|
||||
$context->cond_referenced_var_ids = array_merge(
|
||||
$right_context->cond_referenced_var_ids,
|
||||
$left_context->cond_referenced_var_ids,
|
||||
);
|
||||
|
||||
if ($context->inside_conditional) {
|
||||
$context->updateChecks($right_context);
|
||||
|
||||
$context->vars_possibly_in_scope = array_merge(
|
||||
$right_context->vars_possibly_in_scope,
|
||||
$left_context->vars_possibly_in_scope,
|
||||
);
|
||||
|
||||
$context->assigned_var_ids = array_merge(
|
||||
$left_context->assigned_var_ids,
|
||||
$right_context->assigned_var_ids,
|
||||
);
|
||||
}
|
||||
|
||||
if ($context->if_body_context && !$context->inside_negation) {
|
||||
$if_body_context = $context->if_body_context;
|
||||
$context->vars_in_scope = $right_context->vars_in_scope;
|
||||
$if_body_context->vars_in_scope = array_merge(
|
||||
$if_body_context->vars_in_scope,
|
||||
$context->vars_in_scope,
|
||||
);
|
||||
|
||||
$if_body_context->cond_referenced_var_ids = array_merge(
|
||||
$if_body_context->cond_referenced_var_ids,
|
||||
$context->cond_referenced_var_ids,
|
||||
);
|
||||
|
||||
$if_body_context->assigned_var_ids = array_merge(
|
||||
$if_body_context->assigned_var_ids,
|
||||
$context->assigned_var_ids,
|
||||
);
|
||||
|
||||
$if_body_context->reconciled_expression_clauses = [
|
||||
...$if_body_context->reconciled_expression_clauses,
|
||||
...array_map(
|
||||
/** @return string|int */
|
||||
static fn(Clause $c) => $c->hash,
|
||||
$partitioned_clauses[1],
|
||||
),
|
||||
];
|
||||
|
||||
$if_body_context->vars_possibly_in_scope = array_merge(
|
||||
$if_body_context->vars_possibly_in_scope,
|
||||
$context->vars_possibly_in_scope,
|
||||
);
|
||||
|
||||
$if_body_context->updateChecks($context);
|
||||
} else {
|
||||
$context->vars_in_scope = $left_context->vars_in_scope;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
1418
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php
vendored
Normal file
1418
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\BinaryOp;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Node\Expr\VirtualIsset;
|
||||
use Psalm\Node\Expr\VirtualTernary;
|
||||
use Psalm\Node\Expr\VirtualVariable;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TMixed;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class CoalesceAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\BinaryOp\Coalesce $stmt,
|
||||
Context $context
|
||||
): bool {
|
||||
$left_expr = $stmt->left;
|
||||
|
||||
$root_expr = $left_expr;
|
||||
|
||||
while ($root_expr instanceof PhpParser\Node\Expr\ArrayDimFetch
|
||||
|| $root_expr instanceof PhpParser\Node\Expr\PropertyFetch
|
||||
) {
|
||||
$root_expr = $root_expr->var;
|
||||
}
|
||||
|
||||
if ($root_expr instanceof PhpParser\Node\Expr\FuncCall
|
||||
|| $root_expr instanceof PhpParser\Node\Expr\MethodCall
|
||||
|| $root_expr instanceof PhpParser\Node\Expr\StaticCall
|
||||
|| $root_expr instanceof PhpParser\Node\Expr\Cast
|
||||
|| $root_expr instanceof PhpParser\Node\Expr\NullsafePropertyFetch
|
||||
|| $root_expr instanceof PhpParser\Node\Expr\NullsafeMethodCall
|
||||
|| $root_expr instanceof PhpParser\Node\Expr\Ternary
|
||||
) {
|
||||
$left_var_id = '$<tmp coalesce var>' . (int) $left_expr->getAttribute('startFilePos');
|
||||
|
||||
$cloned = clone $context;
|
||||
$cloned->inside_isset = true;
|
||||
|
||||
ExpressionAnalyzer::analyze($statements_analyzer, $left_expr, $cloned);
|
||||
|
||||
if ($root_expr !== $left_expr) {
|
||||
$condition_type = $statements_analyzer->node_data->getType($left_expr);
|
||||
if ($condition_type) {
|
||||
$condition_type = $condition_type->setPossiblyUndefined(true);
|
||||
} else {
|
||||
$condition_type = new Union([new TMixed()], ['possibly_undefined' => true]);
|
||||
}
|
||||
} else {
|
||||
$condition_type = $statements_analyzer->node_data->getType($left_expr) ?? Type::getMixed();
|
||||
}
|
||||
|
||||
$context->vars_in_scope[$left_var_id] = $condition_type;
|
||||
|
||||
$left_expr = new VirtualVariable(
|
||||
substr($left_var_id, 1),
|
||||
$left_expr->getAttributes(),
|
||||
);
|
||||
}
|
||||
|
||||
$ternary = new VirtualTernary(
|
||||
new VirtualIsset(
|
||||
[$left_expr],
|
||||
$stmt->left->getAttributes(),
|
||||
),
|
||||
$left_expr,
|
||||
$stmt->right,
|
||||
$stmt->getAttributes(),
|
||||
);
|
||||
|
||||
$old_node_data = $statements_analyzer->node_data;
|
||||
|
||||
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
||||
|
||||
ExpressionAnalyzer::analyze($statements_analyzer, $ternary, $context);
|
||||
|
||||
$ternary_type = $statements_analyzer->node_data->getType($ternary) ?? Type::getMixed();
|
||||
|
||||
$statements_analyzer->node_data = $old_node_data;
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, $ternary_type);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
450
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php
vendored
Normal file
450
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php
vendored
Normal file
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\BinaryOp;
|
||||
|
||||
use AssertionError;
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Analyzer\TraitAnalyzer;
|
||||
use Psalm\Internal\Codebase\VariableUseGraph;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Internal\Type\Comparator\AtomicTypeComparator;
|
||||
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Issue\FalseOperand;
|
||||
use Psalm\Issue\ImplicitToStringCast;
|
||||
use Psalm\Issue\ImpureMethodCall;
|
||||
use Psalm\Issue\InvalidOperand;
|
||||
use Psalm\Issue\MixedOperand;
|
||||
use Psalm\Issue\NullOperand;
|
||||
use Psalm\Issue\PossiblyFalseOperand;
|
||||
use Psalm\Issue\PossiblyInvalidOperand;
|
||||
use Psalm\Issue\PossiblyNullOperand;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TFalse;
|
||||
use Psalm\Type\Atomic\TFloat;
|
||||
use Psalm\Type\Atomic\TInt;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Atomic\TLowercaseString;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Atomic\TNonEmptyNonspecificLiteralString;
|
||||
use Psalm\Type\Atomic\TNonEmptyString;
|
||||
use Psalm\Type\Atomic\TNonspecificLiteralString;
|
||||
use Psalm\Type\Atomic\TNull;
|
||||
use Psalm\Type\Atomic\TNumericString;
|
||||
use Psalm\Type\Atomic\TString;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Union;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function count;
|
||||
use function reset;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ConcatAnalyzer
|
||||
{
|
||||
private const MAX_LITERALS = 64;
|
||||
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr $left,
|
||||
PhpParser\Node\Expr $right,
|
||||
Context $context,
|
||||
Union &$result_type = null
|
||||
): void {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$left_type = $statements_analyzer->node_data->getType($left);
|
||||
$right_type = $statements_analyzer->node_data->getType($right);
|
||||
$config = Config::getInstance();
|
||||
|
||||
if ($left_type && $right_type) {
|
||||
$result_type = Type::getString();
|
||||
|
||||
if ($left_type->hasMixed() || $right_type->hasMixed()) {
|
||||
if (!$context->collect_initializations
|
||||
&& !$context->collect_mutations
|
||||
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
|
||||
&& (!(($parent_source = $statements_analyzer->getSource())
|
||||
instanceof FunctionLikeAnalyzer)
|
||||
|| !$parent_source->getSource() instanceof TraitAnalyzer)
|
||||
) {
|
||||
$codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath());
|
||||
}
|
||||
|
||||
if ($left_type->hasMixed()) {
|
||||
$arg_location = new CodeLocation($statements_analyzer->getSource(), $left);
|
||||
|
||||
$origin_locations = [];
|
||||
|
||||
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) {
|
||||
foreach ($left_type->parent_nodes as $parent_node) {
|
||||
$origin_locations = [
|
||||
...$origin_locations,
|
||||
...$statements_analyzer->data_flow_graph->getOriginLocations($parent_node),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$origin_location = count($origin_locations) === 1 ? reset($origin_locations) : null;
|
||||
|
||||
if ($origin_location && $origin_location->getHash() === $arg_location->getHash()) {
|
||||
$origin_location = null;
|
||||
}
|
||||
|
||||
IssueBuffer::maybeAdd(
|
||||
new MixedOperand(
|
||||
'Left operand cannot be mixed',
|
||||
$arg_location,
|
||||
$origin_location,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
$arg_location = new CodeLocation($statements_analyzer->getSource(), $right);
|
||||
$origin_locations = [];
|
||||
|
||||
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) {
|
||||
foreach ($right_type->parent_nodes as $parent_node) {
|
||||
$origin_locations = [
|
||||
...$origin_locations,
|
||||
...$statements_analyzer->data_flow_graph->getOriginLocations($parent_node),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$origin_location = count($origin_locations) === 1 ? reset($origin_locations) : null;
|
||||
|
||||
if ($origin_location && $origin_location->getHash() === $arg_location->getHash()) {
|
||||
$origin_location = null;
|
||||
}
|
||||
|
||||
IssueBuffer::maybeAdd(
|
||||
new MixedOperand(
|
||||
'Right operand cannot be mixed',
|
||||
$arg_location,
|
||||
$origin_location,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$context->collect_initializations
|
||||
&& !$context->collect_mutations
|
||||
&& $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath()
|
||||
&& (!(($parent_source = $statements_analyzer->getSource())
|
||||
instanceof FunctionLikeAnalyzer)
|
||||
|| !$parent_source->getSource() instanceof TraitAnalyzer)
|
||||
) {
|
||||
$codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath());
|
||||
}
|
||||
|
||||
self::analyzeOperand($statements_analyzer, $left, $left_type, 'Left', $context);
|
||||
self::analyzeOperand($statements_analyzer, $right, $right_type, 'Right', $context);
|
||||
|
||||
// If both types are specific literals, combine them into new literals
|
||||
$literal_concat = false;
|
||||
|
||||
if ($left_type->allSpecificLiterals() && $right_type->allSpecificLiterals()) {
|
||||
$left_type_parts = $left_type->getAtomicTypes();
|
||||
$right_type_parts = $right_type->getAtomicTypes();
|
||||
$combinations = count($left_type_parts) * count($right_type_parts);
|
||||
if ($combinations < self::MAX_LITERALS) {
|
||||
$literal_concat = true;
|
||||
$result_type_parts = [];
|
||||
|
||||
foreach ($left_type->getAtomicTypes() as $left_type_part) {
|
||||
foreach ($right_type->getAtomicTypes() as $right_type_part) {
|
||||
$literal = $left_type_part->value . $right_type_part->value;
|
||||
if (strlen($literal) >= $config->max_string_length) {
|
||||
// Literal too long, use non-literal type instead
|
||||
$literal_concat = false;
|
||||
break 2;
|
||||
}
|
||||
|
||||
$result_type_parts[] = new TLiteralString($literal);
|
||||
}
|
||||
}
|
||||
|
||||
if ($literal_concat) {
|
||||
// Bypass opcache bug: https://github.com/php/php-src/issues/10635
|
||||
(function (int $_): void {
|
||||
})($combinations);
|
||||
if (count($result_type_parts) === 0) {
|
||||
throw new AssertionError("The number of parts cannot be 0!");
|
||||
}
|
||||
if (count($result_type_parts) !== $combinations) {
|
||||
throw new AssertionError("The number of parts does not match!");
|
||||
}
|
||||
$result_type = new Union($result_type_parts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$literal_concat) {
|
||||
$numeric_type = new Union([
|
||||
new TNumericString,
|
||||
new TInt,
|
||||
new TFloat,
|
||||
]);
|
||||
$left_is_numeric = UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$left_type,
|
||||
$numeric_type,
|
||||
);
|
||||
|
||||
$right_is_numeric = UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$right_type,
|
||||
$numeric_type,
|
||||
);
|
||||
|
||||
$has_numeric_type = $left_is_numeric || $right_is_numeric;
|
||||
|
||||
if ($left_is_numeric) {
|
||||
$right_uint = Type::getListKey();
|
||||
$right_is_uint = UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$right_type,
|
||||
$right_uint,
|
||||
);
|
||||
|
||||
if ($right_is_uint) {
|
||||
$result_type = Type::getNumericString();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$lowercase_type = $numeric_type->getBuilder()->addType(new TLowercaseString())->freeze();
|
||||
|
||||
$all_lowercase = UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$left_type,
|
||||
$lowercase_type,
|
||||
) && UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$right_type,
|
||||
$lowercase_type,
|
||||
);
|
||||
|
||||
$non_empty_string = $numeric_type->getBuilder()->addType(new TNonEmptyString())->freeze();
|
||||
|
||||
$left_non_empty = UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$left_type,
|
||||
$non_empty_string,
|
||||
);
|
||||
|
||||
$right_non_empty = UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$right_type,
|
||||
$non_empty_string,
|
||||
);
|
||||
|
||||
$has_non_empty = $left_non_empty || $right_non_empty;
|
||||
$all_non_empty = $left_non_empty && $right_non_empty;
|
||||
|
||||
$has_numeric_and_non_empty = $has_numeric_type && $has_non_empty;
|
||||
|
||||
$all_literals = $left_type->allLiterals() && $right_type->allLiterals();
|
||||
|
||||
if ($has_non_empty) {
|
||||
if ($all_literals) {
|
||||
$result_type = new Union([new TNonEmptyNonspecificLiteralString]);
|
||||
} elseif ($all_lowercase) {
|
||||
$result_type = Type::getNonEmptyLowercaseString();
|
||||
} else {
|
||||
$result_type = $all_non_empty || $has_numeric_and_non_empty ?
|
||||
Type::getNonFalsyString() : Type::getNonEmptyString();
|
||||
}
|
||||
} else {
|
||||
if ($all_literals) {
|
||||
$result_type = new Union([new TNonspecificLiteralString]);
|
||||
} elseif ($all_lowercase) {
|
||||
$result_type = Type::getLowercaseString();
|
||||
} else {
|
||||
$result_type = Type::getString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function analyzeOperand(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr $operand,
|
||||
Union $operand_type,
|
||||
string $side,
|
||||
Context $context
|
||||
): void {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
$config = Config::getInstance();
|
||||
|
||||
if ($operand_type->isNull()) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new NullOperand(
|
||||
'Cannot concatenate with a ' . $operand_type,
|
||||
new CodeLocation($statements_analyzer->getSource(), $operand),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($operand_type->isFalse()) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new FalseOperand(
|
||||
'Cannot concatenate with a ' . $operand_type,
|
||||
new CodeLocation($statements_analyzer->getSource(), $operand),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($operand_type->isNullable() && !$operand_type->ignore_nullable_issues) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new PossiblyNullOperand(
|
||||
'Cannot concatenate with a possibly null ' . $operand_type,
|
||||
new CodeLocation($statements_analyzer->getSource(), $operand),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
|
||||
if ($operand_type->isFalsable() && !$operand_type->ignore_falsable_issues) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new PossiblyFalseOperand(
|
||||
'Cannot concatenate with a possibly false ' . $operand_type,
|
||||
new CodeLocation($statements_analyzer->getSource(), $operand),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
|
||||
$operand_type_match = true;
|
||||
$has_valid_operand = false;
|
||||
$comparison_result = new TypeComparisonResult();
|
||||
|
||||
foreach ($operand_type->getAtomicTypes() as $operand_type_part) {
|
||||
if ($operand_type_part instanceof TTemplateParam && !$operand_type_part->as->isString()) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new MixedOperand(
|
||||
"$side operand cannot be a non-string template param",
|
||||
new CodeLocation($statements_analyzer->getSource(), $operand),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($operand_type_part instanceof TNull || $operand_type_part instanceof TFalse) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$operand_type_part_match = AtomicTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$operand_type_part,
|
||||
new TString,
|
||||
false,
|
||||
false,
|
||||
$comparison_result,
|
||||
);
|
||||
|
||||
$operand_type_match = $operand_type_match && $operand_type_part_match;
|
||||
|
||||
$has_valid_operand = $has_valid_operand || $operand_type_part_match;
|
||||
|
||||
if ($comparison_result->to_string_cast && $config->strict_binary_operands) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new ImplicitToStringCast(
|
||||
"$side side of concat op expects string, '$operand_type' provided with a __toString method",
|
||||
new CodeLocation($statements_analyzer->getSource(), $operand),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($operand_type->getAtomicTypes() as $atomic_type) {
|
||||
if ($atomic_type instanceof TNamedObject) {
|
||||
$to_string_method_id = new MethodIdentifier(
|
||||
$atomic_type->value,
|
||||
'__tostring',
|
||||
);
|
||||
|
||||
if ($codebase->methods->methodExists(
|
||||
$to_string_method_id,
|
||||
$context->calling_method_id,
|
||||
$codebase->collect_locations
|
||||
? new CodeLocation($statements_analyzer->getSource(), $operand)
|
||||
: null,
|
||||
!$context->collect_initializations
|
||||
&& !$context->collect_mutations
|
||||
? $statements_analyzer
|
||||
: null,
|
||||
$statements_analyzer->getFilePath(),
|
||||
)) {
|
||||
try {
|
||||
$storage = $codebase->methods->getStorage($to_string_method_id);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($context->mutation_free && !$storage->mutation_free) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new ImpureMethodCall(
|
||||
'Cannot call a possibly-mutating method '
|
||||
. $atomic_type->value . '::__toString from a pure context',
|
||||
new CodeLocation($statements_analyzer, $operand),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} elseif ($statements_analyzer->getSource()
|
||||
instanceof FunctionLikeAnalyzer
|
||||
&& $statements_analyzer->getSource()->track_mutations
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$operand_type_match
|
||||
&& (!$comparison_result->scalar_type_match_found || $config->strict_binary_operands)
|
||||
) {
|
||||
if ($has_valid_operand) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new PossiblyInvalidOperand(
|
||||
'Cannot concatenate with a ' . $operand_type,
|
||||
new CodeLocation($statements_analyzer->getSource(), $operand),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidOperand(
|
||||
'Cannot concatenate with a ' . $operand_type,
|
||||
new CodeLocation($statements_analyzer->getSource(), $operand),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\BinaryOp;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOpAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TFloat;
|
||||
use Psalm\Type\Atomic\TInt;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class NonComparisonOpAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\BinaryOp $stmt,
|
||||
Context $context
|
||||
): void {
|
||||
$stmt_left_type = $statements_analyzer->node_data->getType($stmt->left);
|
||||
$stmt_right_type = $statements_analyzer->node_data->getType($stmt->right);
|
||||
|
||||
if (!$stmt_left_type || !$stmt_right_type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (($stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseOr
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseXor
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseAnd
|
||||
)
|
||||
&& $stmt_left_type->hasString()
|
||||
&& $stmt_right_type->hasString()
|
||||
) {
|
||||
$stmt_type = Type::getString();
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, $stmt_type);
|
||||
BinaryOpAnalyzer::addDataFlow(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$stmt->left,
|
||||
$stmt->right,
|
||||
'nondivop',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseOr
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseXor
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseAnd
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\ShiftLeft
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\ShiftRight
|
||||
) {
|
||||
ArithmeticOpAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$statements_analyzer->node_data,
|
||||
$stmt->left,
|
||||
$stmt->right,
|
||||
$stmt,
|
||||
$result_type,
|
||||
$context,
|
||||
);
|
||||
|
||||
if (!$result_type) {
|
||||
$result_type = new Union([new TInt(), new TFloat()]);
|
||||
}
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, $result_type);
|
||||
|
||||
BinaryOpAnalyzer::addDataFlow(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$stmt->left,
|
||||
$stmt->right,
|
||||
'nondivop',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalXor) {
|
||||
if ($stmt_left_type->hasBool() || $stmt_right_type->hasBool()) {
|
||||
$statements_analyzer->node_data->setType($stmt, Type::getBool());
|
||||
}
|
||||
|
||||
BinaryOpAnalyzer::addDataFlow(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$stmt->left,
|
||||
$stmt->right,
|
||||
'xor',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Div) {
|
||||
ArithmeticOpAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$statements_analyzer->node_data,
|
||||
$stmt->left,
|
||||
$stmt->right,
|
||||
$stmt,
|
||||
$result_type,
|
||||
$context,
|
||||
);
|
||||
|
||||
if (!$result_type) {
|
||||
$result_type = new Union([new TInt(), new TFloat()]);
|
||||
}
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, $result_type);
|
||||
|
||||
BinaryOpAnalyzer::addDataFlow(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$stmt->left,
|
||||
$stmt->right,
|
||||
'div',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
404
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php
vendored
Normal file
404
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/OrAnalyzer.php
vendored
Normal file
@@ -0,0 +1,404 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\BinaryOp;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Exception\ComplicatedExpressionException;
|
||||
use Psalm\Exception\ScopeAnalysisException;
|
||||
use Psalm\Internal\Algebra;
|
||||
use Psalm\Internal\Algebra\FormulaGenerator;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\IfConditionalAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\IfElse\IfAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\IfElseAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Clause;
|
||||
use Psalm\Internal\Scope\IfScope;
|
||||
use Psalm\Internal\Type\AssertionReconciler;
|
||||
use Psalm\Node\Expr\VirtualBooleanNot;
|
||||
use Psalm\Node\Stmt\VirtualExpression;
|
||||
use Psalm\Node\Stmt\VirtualIf;
|
||||
use Psalm\Storage\Assertion\Truthy;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
use function array_diff_key;
|
||||
use function array_filter;
|
||||
use function array_merge;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function in_array;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class OrAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\BinaryOp $stmt,
|
||||
Context $context,
|
||||
bool $from_stmt = false
|
||||
): bool {
|
||||
if ($from_stmt) {
|
||||
$fake_if_stmt = new VirtualIf(
|
||||
new VirtualBooleanNot($stmt->left, $stmt->left->getAttributes()),
|
||||
[
|
||||
'stmts' => [
|
||||
new VirtualExpression(
|
||||
$stmt->right,
|
||||
),
|
||||
],
|
||||
],
|
||||
$stmt->getAttributes(),
|
||||
);
|
||||
|
||||
return IfElseAnalyzer::analyze($statements_analyzer, $fake_if_stmt, $context) !== false;
|
||||
}
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$post_leaving_if_context = null;
|
||||
|
||||
// we cap this at max depth of 4 to prevent quadratic behaviour
|
||||
// when analysing <expr> || <expr> || <expr> || <expr> || <expr>
|
||||
if (!$stmt->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| !$stmt->left->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
|| !$stmt->left->left->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
) {
|
||||
$if_scope = new IfScope();
|
||||
|
||||
try {
|
||||
$if_conditional_scope = IfConditionalAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt->left,
|
||||
$context,
|
||||
$codebase,
|
||||
$if_scope,
|
||||
$context->branch_point ?: (int) $stmt->getAttribute('startFilePos'),
|
||||
);
|
||||
|
||||
$left_context = $if_conditional_scope->if_context;
|
||||
|
||||
$left_referenced_var_ids = $if_conditional_scope->cond_referenced_var_ids;
|
||||
$left_assigned_var_ids = $if_conditional_scope->assigned_in_conditional_var_ids;
|
||||
|
||||
if ($stmt->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
|
||||
$post_leaving_if_context = clone $context;
|
||||
}
|
||||
} catch (ScopeAnalysisException $e) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$pre_referenced_var_ids = $context->cond_referenced_var_ids;
|
||||
$context->cond_referenced_var_ids = [];
|
||||
|
||||
$pre_assigned_var_ids = $context->assigned_var_ids;
|
||||
|
||||
$post_leaving_if_context = clone $context;
|
||||
|
||||
$left_context = clone $context;
|
||||
$left_context->if_body_context = null;
|
||||
$left_context->assigned_var_ids = [];
|
||||
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->left, $left_context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($left_context->parent_remove_vars as $var_id => $_) {
|
||||
$context->removeVarFromConflictingClauses($var_id);
|
||||
}
|
||||
|
||||
IfConditionalAnalyzer::handleParadoxicalCondition($statements_analyzer, $stmt->left);
|
||||
|
||||
foreach ($left_context->vars_in_scope as $var_id => $type) {
|
||||
if (!isset($context->vars_in_scope[$var_id])) {
|
||||
if (isset($left_context->assigned_var_ids[$var_id])) {
|
||||
$context->vars_in_scope[$var_id] = $type;
|
||||
}
|
||||
} else {
|
||||
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$context->vars_in_scope[$var_id],
|
||||
$type,
|
||||
$codebase,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$left_referenced_var_ids = $left_context->cond_referenced_var_ids;
|
||||
$left_context->cond_referenced_var_ids = array_merge($pre_referenced_var_ids, $left_referenced_var_ids);
|
||||
|
||||
$left_assigned_var_ids = array_diff_key($left_context->assigned_var_ids, $pre_assigned_var_ids);
|
||||
$left_context->assigned_var_ids = array_merge($pre_assigned_var_ids, $left_context->assigned_var_ids);
|
||||
|
||||
$left_referenced_var_ids = array_diff_key($left_referenced_var_ids, $left_assigned_var_ids);
|
||||
}
|
||||
|
||||
$left_cond_id = spl_object_id($stmt->left);
|
||||
|
||||
$left_clauses = FormulaGenerator::getFormula(
|
||||
$left_cond_id,
|
||||
$left_cond_id,
|
||||
$stmt->left,
|
||||
$context->self,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
try {
|
||||
$negated_left_clauses = Algebra::negateFormula($left_clauses);
|
||||
} catch (ComplicatedExpressionException $e) {
|
||||
try {
|
||||
$negated_left_clauses = FormulaGenerator::getFormula(
|
||||
$left_cond_id,
|
||||
$left_cond_id,
|
||||
new VirtualBooleanNot($stmt->left),
|
||||
$context->self,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
false,
|
||||
);
|
||||
} catch (ComplicatedExpressionException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($left_context->reconciled_expression_clauses) {
|
||||
$reconciled_expression_clauses = $left_context->reconciled_expression_clauses;
|
||||
|
||||
$negated_left_clauses = array_values(
|
||||
array_filter(
|
||||
$negated_left_clauses,
|
||||
static fn(Clause $c): bool => !in_array($c->hash, $reconciled_expression_clauses)
|
||||
),
|
||||
);
|
||||
|
||||
if (count($negated_left_clauses) === 1
|
||||
&& $negated_left_clauses[0]->wedge
|
||||
&& !$negated_left_clauses[0]->possibilities
|
||||
) {
|
||||
$negated_left_clauses = [];
|
||||
}
|
||||
}
|
||||
|
||||
$clauses_for_right_analysis = Algebra::simplifyCNF(
|
||||
[...$context->clauses, ...$negated_left_clauses],
|
||||
);
|
||||
|
||||
$active_negated_type_assertions = [];
|
||||
|
||||
$negated_type_assertions = Algebra::getTruthsFromFormula(
|
||||
$clauses_for_right_analysis,
|
||||
$left_cond_id,
|
||||
$left_referenced_var_ids,
|
||||
$active_negated_type_assertions,
|
||||
);
|
||||
|
||||
$changed_var_ids = [];
|
||||
|
||||
$right_context = clone $context;
|
||||
|
||||
if ($stmt->left instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr
|
||||
&& $left_assigned_var_ids
|
||||
&& $post_leaving_if_context
|
||||
) {
|
||||
IfAnalyzer::addConditionallyAssignedVarsToContext(
|
||||
$statements_analyzer,
|
||||
$stmt->left,
|
||||
$post_leaving_if_context,
|
||||
$right_context,
|
||||
$left_assigned_var_ids,
|
||||
);
|
||||
}
|
||||
|
||||
if ($negated_type_assertions) {
|
||||
// while in an or, we allow scope to boil over to support
|
||||
// statements of the form if ($x === null || $x->foo())
|
||||
[$right_context->vars_in_scope, $right_context->references_in_scope] = Reconciler::reconcileKeyedTypes(
|
||||
$negated_type_assertions,
|
||||
$active_negated_type_assertions,
|
||||
$right_context->vars_in_scope,
|
||||
$right_context->references_in_scope,
|
||||
$changed_var_ids,
|
||||
$left_referenced_var_ids,
|
||||
$statements_analyzer,
|
||||
[],
|
||||
$left_context->inside_loop,
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt->left),
|
||||
!$context->inside_negation,
|
||||
);
|
||||
}
|
||||
|
||||
$right_context->clauses = $clauses_for_right_analysis;
|
||||
|
||||
if ($changed_var_ids) {
|
||||
$partitioned_clauses = Context::removeReconciledClauses($right_context->clauses, $changed_var_ids);
|
||||
$right_context->clauses = $partitioned_clauses[0];
|
||||
$right_context->reconciled_expression_clauses = $context->reconciled_expression_clauses;
|
||||
|
||||
foreach ($partitioned_clauses[1] as $clause) {
|
||||
$right_context->reconciled_expression_clauses[] = $clause->hash;
|
||||
}
|
||||
|
||||
$partitioned_clauses = Context::removeReconciledClauses($context->clauses, $changed_var_ids);
|
||||
$context->clauses = $partitioned_clauses[0];
|
||||
|
||||
foreach ($partitioned_clauses[1] as $clause) {
|
||||
$context->reconciled_expression_clauses[] = $clause->hash;
|
||||
}
|
||||
}
|
||||
|
||||
$right_context->if_body_context = null;
|
||||
|
||||
$pre_referenced_var_ids = $right_context->cond_referenced_var_ids;
|
||||
$right_context->cond_referenced_var_ids = [];
|
||||
|
||||
$pre_assigned_var_ids = $right_context->assigned_var_ids;
|
||||
$right_context->assigned_var_ids = [];
|
||||
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->right, $right_context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
IfConditionalAnalyzer::handleParadoxicalCondition($statements_analyzer, $stmt->right);
|
||||
|
||||
$right_referenced_var_ids = $right_context->cond_referenced_var_ids;
|
||||
$right_context->cond_referenced_var_ids = array_merge($pre_referenced_var_ids, $right_referenced_var_ids);
|
||||
|
||||
$right_assigned_var_ids = $right_context->assigned_var_ids;
|
||||
$right_context->assigned_var_ids = array_merge($pre_assigned_var_ids, $right_assigned_var_ids);
|
||||
|
||||
$right_cond_id = spl_object_id($stmt->right);
|
||||
|
||||
$right_clauses = FormulaGenerator::getFormula(
|
||||
$right_cond_id,
|
||||
$right_cond_id,
|
||||
$stmt->right,
|
||||
$context->self,
|
||||
$statements_analyzer,
|
||||
$codebase,
|
||||
);
|
||||
|
||||
$clauses_for_right_analysis = Context::removeReconciledClauses(
|
||||
$clauses_for_right_analysis,
|
||||
$right_assigned_var_ids,
|
||||
)[0];
|
||||
|
||||
$combined_right_clauses = Algebra::simplifyCNF(
|
||||
[...$clauses_for_right_analysis, ...$right_clauses],
|
||||
);
|
||||
|
||||
$active_right_type_assertions = [];
|
||||
|
||||
$right_type_assertions = Algebra::getTruthsFromFormula(
|
||||
$combined_right_clauses,
|
||||
$right_cond_id,
|
||||
$right_referenced_var_ids,
|
||||
$active_right_type_assertions,
|
||||
);
|
||||
|
||||
if ($right_type_assertions) {
|
||||
$right_changed_var_ids = [];
|
||||
|
||||
Reconciler::reconcileKeyedTypes(
|
||||
$right_type_assertions,
|
||||
$active_right_type_assertions,
|
||||
$right_context->vars_in_scope,
|
||||
$right_context->references_in_scope,
|
||||
$right_changed_var_ids,
|
||||
$right_referenced_var_ids,
|
||||
$statements_analyzer,
|
||||
[],
|
||||
$left_context->inside_loop,
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt->right),
|
||||
$context->inside_negation,
|
||||
);
|
||||
}
|
||||
|
||||
if (!($stmt->right instanceof PhpParser\Node\Expr\Exit_)) {
|
||||
foreach ($right_context->vars_in_scope as $var_id => $type) {
|
||||
if (isset($context->vars_in_scope[$var_id])) {
|
||||
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$context->vars_in_scope[$var_id],
|
||||
$type,
|
||||
$codebase,
|
||||
);
|
||||
}
|
||||
}
|
||||
} elseif ($stmt->left instanceof PhpParser\Node\Expr\Assign) {
|
||||
$var_id = ExpressionIdentifier::getVarId($stmt->left->var, $context->self);
|
||||
|
||||
if ($var_id && isset($left_context->vars_in_scope[$var_id])) {
|
||||
$left_inferred_reconciled = AssertionReconciler::reconcile(
|
||||
new Truthy(),
|
||||
$left_context->vars_in_scope[$var_id],
|
||||
'',
|
||||
$statements_analyzer,
|
||||
$context->inside_loop,
|
||||
[],
|
||||
new CodeLocation($statements_analyzer->getSource(), $stmt->left),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
$context->vars_in_scope[$var_id] = $left_inferred_reconciled;
|
||||
}
|
||||
}
|
||||
|
||||
if ($context->inside_conditional) {
|
||||
$context->updateChecks($right_context);
|
||||
}
|
||||
|
||||
$context->cond_referenced_var_ids = array_merge(
|
||||
$right_context->cond_referenced_var_ids,
|
||||
$context->cond_referenced_var_ids,
|
||||
);
|
||||
|
||||
$context->assigned_var_ids = array_merge(
|
||||
$context->assigned_var_ids,
|
||||
$right_context->assigned_var_ids,
|
||||
);
|
||||
|
||||
if ($context->if_body_context) {
|
||||
$if_body_context = $context->if_body_context;
|
||||
|
||||
foreach ($right_context->vars_in_scope as $var_id => $type) {
|
||||
if (isset($if_body_context->vars_in_scope[$var_id])) {
|
||||
$if_body_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$if_body_context->vars_in_scope[$var_id],
|
||||
$codebase,
|
||||
);
|
||||
} elseif (isset($left_context->vars_in_scope[$var_id])) {
|
||||
$if_body_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$left_context->vars_in_scope[$var_id],
|
||||
$codebase,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$if_body_context->cond_referenced_var_ids = array_merge(
|
||||
$context->cond_referenced_var_ids,
|
||||
$if_body_context->cond_referenced_var_ids,
|
||||
);
|
||||
|
||||
$if_body_context->assigned_var_ids = array_merge(
|
||||
$context->assigned_var_ids,
|
||||
$if_body_context->assigned_var_ids,
|
||||
);
|
||||
|
||||
$if_body_context->updateChecks($context);
|
||||
}
|
||||
|
||||
$context->vars_possibly_in_scope = array_merge(
|
||||
$right_context->vars_possibly_in_scope,
|
||||
$context->vars_possibly_in_scope,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
531
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php
vendored
Normal file
531
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php
vendored
Normal file
@@ -0,0 +1,531 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\AndAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\CoalesceAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\ConcatAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\NonComparisonOpAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\OrAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Codebase\TaintFlowGraph;
|
||||
use Psalm\Internal\Codebase\VariableUseGraph;
|
||||
use Psalm\Internal\DataFlow\DataFlowNode;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Issue\DocblockTypeContradiction;
|
||||
use Psalm\Issue\ImpureMethodCall;
|
||||
use Psalm\Issue\InvalidOperand;
|
||||
use Psalm\Issue\RedundantCondition;
|
||||
use Psalm\Issue\RedundantConditionGivenDocblockType;
|
||||
use Psalm\Issue\TypeDoesNotContainType;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TLiteralInt;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Union;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function in_array;
|
||||
use function strlen;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class BinaryOpAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\BinaryOp $stmt,
|
||||
Context $context,
|
||||
int $nesting = 0,
|
||||
bool $from_stmt = false
|
||||
): bool {
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat && $nesting > 100) {
|
||||
$statements_analyzer->node_data->setType($stmt, Type::getString());
|
||||
|
||||
// ignore deeply-nested string concatenation
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd ||
|
||||
$stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd
|
||||
) {
|
||||
$was_inside_general_use = $context->inside_general_use;
|
||||
$context->inside_general_use = true;
|
||||
|
||||
$expr_result = AndAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$context,
|
||||
$from_stmt,
|
||||
);
|
||||
|
||||
$context->inside_general_use = $was_inside_general_use;
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, Type::getBool());
|
||||
|
||||
return $expr_result;
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr ||
|
||||
$stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr
|
||||
) {
|
||||
$was_inside_general_use = $context->inside_general_use;
|
||||
$context->inside_general_use = true;
|
||||
|
||||
$expr_result = OrAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$context,
|
||||
$from_stmt,
|
||||
);
|
||||
|
||||
$context->inside_general_use = $was_inside_general_use;
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, Type::getBool());
|
||||
|
||||
return $expr_result;
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Coalesce) {
|
||||
$expr_result = CoalesceAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$context,
|
||||
);
|
||||
|
||||
self::addDataFlow(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$stmt->left,
|
||||
$stmt->right,
|
||||
'coalesce',
|
||||
);
|
||||
|
||||
return $expr_result;
|
||||
}
|
||||
|
||||
if ($stmt->left instanceof PhpParser\Node\Expr\BinaryOp) {
|
||||
if (self::analyze($statements_analyzer, $stmt->left, $context, $nesting + 1) === false) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->left, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt->right instanceof PhpParser\Node\Expr\BinaryOp) {
|
||||
if (self::analyze($statements_analyzer, $stmt->right, $context, $nesting + 1) === false) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->right, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat) {
|
||||
$stmt_type = Type::getString();
|
||||
|
||||
ConcatAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt->left,
|
||||
$stmt->right,
|
||||
$context,
|
||||
$result_type,
|
||||
);
|
||||
|
||||
if ($result_type) {
|
||||
$stmt_type = $result_type;
|
||||
}
|
||||
|
||||
if ($statements_analyzer->data_flow_graph
|
||||
&& ($statements_analyzer->data_flow_graph instanceof VariableUseGraph
|
||||
|| !in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()))
|
||||
) {
|
||||
$stmt_left_type = $statements_analyzer->node_data->getType($stmt->left);
|
||||
$stmt_right_type = $statements_analyzer->node_data->getType($stmt->right);
|
||||
|
||||
$var_location = new CodeLocation($statements_analyzer, $stmt);
|
||||
|
||||
$new_parent_node = DataFlowNode::getForAssignment('concat', $var_location);
|
||||
$statements_analyzer->data_flow_graph->addNode($new_parent_node);
|
||||
|
||||
$stmt_type = $stmt_type->setParentNodes([
|
||||
$new_parent_node->id => $new_parent_node,
|
||||
]);
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
$event = new AddRemoveTaintsEvent($stmt, $context, $statements_analyzer, $codebase);
|
||||
|
||||
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
|
||||
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
|
||||
|
||||
if ($stmt_left_type && $stmt_left_type->parent_nodes) {
|
||||
foreach ($stmt_left_type->parent_nodes as $parent_node) {
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$parent_node,
|
||||
$new_parent_node,
|
||||
'concat',
|
||||
$added_taints,
|
||||
$removed_taints,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt_right_type && $stmt_right_type->parent_nodes) {
|
||||
foreach ($stmt_right_type->parent_nodes as $parent_node) {
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$parent_node,
|
||||
$new_parent_node,
|
||||
'concat',
|
||||
$added_taints,
|
||||
$removed_taints,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, $stmt_type);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Spaceship) {
|
||||
$statements_analyzer->node_data->setType(
|
||||
$stmt,
|
||||
new Union(
|
||||
[
|
||||
new TLiteralInt(-1),
|
||||
new TLiteralInt(0),
|
||||
new TLiteralInt(1),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
self::addDataFlow(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$stmt->left,
|
||||
$stmt->right,
|
||||
'<=>',
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($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
|
||||
) {
|
||||
$statements_analyzer->node_data->setType($stmt, Type::getBool());
|
||||
|
||||
$stmt_left_type = $statements_analyzer->node_data->getType($stmt->left);
|
||||
$stmt_right_type = $statements_analyzer->node_data->getType($stmt->right);
|
||||
|
||||
if (($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)
|
||||
&& $statements_analyzer->getCodebase()->config->strict_binary_operands
|
||||
&& $stmt_left_type
|
||||
&& $stmt_right_type
|
||||
&& (($stmt_left_type->isSingle() && $stmt_left_type->hasBool())
|
||||
|| ($stmt_right_type->isSingle() && $stmt_right_type->hasBool()))
|
||||
) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidOperand(
|
||||
'Cannot compare ' . $stmt_left_type->getId() . ' to ' . $stmt_right_type->getId(),
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
|
||||
if (($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->left instanceof PhpParser\Node\Expr\FuncCall
|
||||
&& $stmt->left->name instanceof PhpParser\Node\Name
|
||||
&& $stmt->left->name->parts === ['substr']
|
||||
&& isset($stmt->left->getArgs()[1])
|
||||
&& $stmt_right_type
|
||||
&& $stmt_right_type->hasLiteralString()
|
||||
) {
|
||||
$from_type = $statements_analyzer->node_data->getType($stmt->left->getArgs()[1]->value);
|
||||
|
||||
$length_type = isset($stmt->left->getArgs()[2])
|
||||
? ($statements_analyzer->node_data->getType($stmt->left->getArgs()[2]->value) ?? Type::getMixed())
|
||||
: null;
|
||||
|
||||
$string_length = null;
|
||||
|
||||
if ($from_type && $from_type->isSingleIntLiteral() && $length_type === null) {
|
||||
$string_length = -$from_type->getSingleIntLiteral()->value;
|
||||
} elseif ($length_type && $length_type->isSingleIntLiteral()) {
|
||||
$string_length = $length_type->getSingleIntLiteral()->value;
|
||||
}
|
||||
|
||||
if ($string_length > 0) {
|
||||
foreach ($stmt_right_type->getAtomicTypes() as $atomic_right_type) {
|
||||
if ($atomic_right_type instanceof TLiteralString) {
|
||||
if (strlen($atomic_right_type->value) !== $string_length) {
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical
|
||||
) {
|
||||
if ($atomic_right_type->from_docblock) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new DocblockTypeContradiction(
|
||||
$atomic_right_type . ' string length is not ' . $string_length,
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
"strlen($atomic_right_type) !== $string_length",
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
new TypeDoesNotContainType(
|
||||
$atomic_right_type . ' string length is not ' . $string_length,
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
"strlen($atomic_right_type) !== $string_length",
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if ($atomic_right_type->from_docblock) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new RedundantConditionGivenDocblockType(
|
||||
$atomic_right_type . ' string length is never ' . $string_length,
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
"strlen($atomic_right_type) !== $string_length",
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
new RedundantCondition(
|
||||
$atomic_right_type . ' string length is never ' . $string_length,
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
"strlen($atomic_right_type) !== $string_length",
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal
|
||||
&& $stmt_left_type
|
||||
&& $stmt_right_type
|
||||
&& ($context->mutation_free || $codebase->alter_code)
|
||||
) {
|
||||
self::checkForImpureEqualityComparison(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$stmt_left_type,
|
||||
$stmt_right_type,
|
||||
);
|
||||
}
|
||||
|
||||
self::addDataFlow(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$stmt->left,
|
||||
$stmt->right,
|
||||
'comparison',
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
NonComparisonOpAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$stmt,
|
||||
$context,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function addDataFlow(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr $stmt,
|
||||
PhpParser\Node\Expr $left,
|
||||
PhpParser\Node\Expr $right,
|
||||
string $type = 'binaryop'
|
||||
): void {
|
||||
if ($stmt->getLine() === -1) {
|
||||
throw new UnexpectedValueException('bad');
|
||||
}
|
||||
$result_type = $statements_analyzer->node_data->getType($stmt);
|
||||
if (!$result_type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph
|
||||
&& $stmt instanceof PhpParser\Node\Expr\BinaryOp
|
||||
&& !$stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat
|
||||
&& !$stmt instanceof PhpParser\Node\Expr\BinaryOp\Coalesce
|
||||
&& (!$stmt instanceof PhpParser\Node\Expr\BinaryOp\Plus || !$result_type->hasArray())
|
||||
) {
|
||||
//among BinaryOp, only Concat and Coalesce can pass tainted value to the result. Also Plus on arrays only
|
||||
return;
|
||||
}
|
||||
|
||||
if ($statements_analyzer->data_flow_graph) {
|
||||
$stmt_left_type = $statements_analyzer->node_data->getType($left);
|
||||
$stmt_right_type = $statements_analyzer->node_data->getType($right);
|
||||
|
||||
$var_location = new CodeLocation($statements_analyzer, $stmt);
|
||||
|
||||
$new_parent_node = DataFlowNode::getForAssignment($type, $var_location);
|
||||
$statements_analyzer->data_flow_graph->addNode($new_parent_node);
|
||||
|
||||
$result_type = $result_type->setParentNodes([
|
||||
$new_parent_node->id => $new_parent_node,
|
||||
]);
|
||||
$statements_analyzer->node_data->setType($stmt, $result_type);
|
||||
|
||||
if ($stmt_left_type && $stmt_left_type->parent_nodes) {
|
||||
foreach ($stmt_left_type->parent_nodes as $parent_node) {
|
||||
$statements_analyzer->data_flow_graph->addPath($parent_node, $new_parent_node, $type);
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt_right_type && $stmt_right_type->parent_nodes) {
|
||||
foreach ($stmt_right_type->parent_nodes as $parent_node) {
|
||||
$statements_analyzer->data_flow_graph->addPath($parent_node, $new_parent_node, $type);
|
||||
}
|
||||
}
|
||||
|
||||
if ($stmt instanceof PhpParser\Node\Expr\AssignOp
|
||||
&& $statements_analyzer->data_flow_graph instanceof VariableUseGraph
|
||||
) {
|
||||
$root_expr = $left;
|
||||
|
||||
while ($root_expr instanceof PhpParser\Node\Expr\ArrayDimFetch) {
|
||||
$root_expr = $root_expr->var;
|
||||
}
|
||||
|
||||
if ($left instanceof PhpParser\Node\Expr\PropertyFetch) {
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$new_parent_node,
|
||||
new DataFlowNode('variable-use', 'variable use', null),
|
||||
'used-by-instance-property',
|
||||
);
|
||||
} if ($left instanceof PhpParser\Node\Expr\StaticPropertyFetch) {
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$new_parent_node,
|
||||
new DataFlowNode('variable-use', 'variable use', null),
|
||||
'use-in-static-property',
|
||||
);
|
||||
} elseif (!$left instanceof PhpParser\Node\Expr\Variable) {
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$new_parent_node,
|
||||
new DataFlowNode('variable-use', 'variable use', null),
|
||||
'variable-use',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function checkForImpureEqualityComparison(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\BinaryOp\Equal $stmt,
|
||||
Union $stmt_left_type,
|
||||
Union $stmt_right_type
|
||||
): void {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
if ($stmt_left_type->hasString() && $stmt_right_type->hasObjectType()) {
|
||||
foreach ($stmt_right_type->getAtomicTypes() as $atomic_type) {
|
||||
if ($atomic_type instanceof TNamedObject) {
|
||||
try {
|
||||
$storage = $codebase->methods->getStorage(
|
||||
new MethodIdentifier(
|
||||
$atomic_type->value,
|
||||
'__tostring',
|
||||
),
|
||||
);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$storage->mutation_free) {
|
||||
if ($statements_analyzer->getSource()
|
||||
instanceof FunctionLikeAnalyzer
|
||||
&& $statements_analyzer->getSource()->track_mutations
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
new ImpureMethodCall(
|
||||
'Cannot call a possibly-mutating method '
|
||||
. $atomic_type->value . '::__toString from a pure context',
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($stmt_right_type->hasString() && $stmt_left_type->hasObjectType()) {
|
||||
foreach ($stmt_left_type->getAtomicTypes() as $atomic_type) {
|
||||
if ($atomic_type instanceof TNamedObject) {
|
||||
try {
|
||||
$storage = $codebase->methods->getStorage(
|
||||
new MethodIdentifier(
|
||||
$atomic_type->value,
|
||||
'__tostring',
|
||||
),
|
||||
);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$storage->mutation_free) {
|
||||
if ($statements_analyzer->getSource() instanceof FunctionLikeAnalyzer
|
||||
&& $statements_analyzer->getSource()->track_mutations
|
||||
) {
|
||||
$statements_analyzer->getSource()->inferred_has_mutation = true;
|
||||
$statements_analyzer->getSource()->inferred_impure = true;
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
new ImpureMethodCall(
|
||||
'Cannot call a possibly-mutating method '
|
||||
. $atomic_type->value . '::__toString from a pure context',
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
129
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php
vendored
Normal file
129
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php
vendored
Normal file
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Codebase\VariableUseGraph;
|
||||
use Psalm\Internal\DataFlow\DataFlowNode;
|
||||
use Psalm\Issue\InvalidOperand;
|
||||
use Psalm\Issue\PossiblyInvalidOperand;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TFloat;
|
||||
use Psalm\Type\Atomic\TInt;
|
||||
use Psalm\Type\Atomic\TLiteralFloat;
|
||||
use Psalm\Type\Atomic\TLiteralInt;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Atomic\TString;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class BitwiseNotAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\BitwiseNot $stmt,
|
||||
Context $context
|
||||
): bool {
|
||||
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!($stmt_expr_type = $statements_analyzer->node_data->getType($stmt->expr))) {
|
||||
$statements_analyzer->node_data->setType($stmt, new Union([new TInt(), new TString()]));
|
||||
} elseif ($stmt_expr_type->isMixed()) {
|
||||
$statements_analyzer->node_data->setType($stmt, Type::getMixed());
|
||||
} else {
|
||||
$acceptable_types = [];
|
||||
$unacceptable_type = null;
|
||||
$has_valid_operand = false;
|
||||
|
||||
$stmt_expr_type = $stmt_expr_type->getBuilder();
|
||||
foreach ($stmt_expr_type->getAtomicTypes() as $type_string => $type_part) {
|
||||
if ($type_part instanceof TInt || $type_part instanceof TString) {
|
||||
if ($type_part instanceof TLiteralInt) {
|
||||
$type_part = new TLiteralInt(~$type_part->value);
|
||||
} elseif ($type_part instanceof TLiteralString) {
|
||||
$type_part = new TLiteralString(~$type_part->value);
|
||||
}
|
||||
|
||||
$acceptable_types[] = $type_part;
|
||||
$has_valid_operand = true;
|
||||
} elseif ($type_part instanceof TFloat) {
|
||||
$type_part = ($type_part instanceof TLiteralFloat) ?
|
||||
new TLiteralInt(~$type_part->value) :
|
||||
new TInt;
|
||||
|
||||
$stmt_expr_type->removeType($type_string);
|
||||
$stmt_expr_type->addType($type_part);
|
||||
|
||||
$acceptable_types[] = $type_part;
|
||||
$has_valid_operand = true;
|
||||
} elseif (!$unacceptable_type) {
|
||||
$unacceptable_type = $type_part;
|
||||
}
|
||||
}
|
||||
|
||||
if ($unacceptable_type || !$acceptable_types) {
|
||||
$message = 'Cannot negate a non-numeric non-string type ' . $unacceptable_type;
|
||||
if ($has_valid_operand) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new PossiblyInvalidOperand(
|
||||
$message,
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidOperand(
|
||||
$message,
|
||||
new CodeLocation($statements_analyzer, $stmt),
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, Type::getMixed());
|
||||
} else {
|
||||
$statements_analyzer->node_data->setType($stmt, new Union($acceptable_types));
|
||||
}
|
||||
}
|
||||
|
||||
self::addDataFlow($statements_analyzer, $stmt, $stmt->expr);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function addDataFlow(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr $stmt,
|
||||
PhpParser\Node\Expr $value
|
||||
): void {
|
||||
$result_type = $statements_analyzer->node_data->getType($stmt);
|
||||
if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph && $result_type) {
|
||||
$var_location = new CodeLocation($statements_analyzer, $stmt);
|
||||
|
||||
$stmt_value_type = $statements_analyzer->node_data->getType($value);
|
||||
|
||||
$new_parent_node = DataFlowNode::getForAssignment('bitwisenot', $var_location);
|
||||
$statements_analyzer->data_flow_graph->addNode($new_parent_node);
|
||||
$result_type = $result_type->setParentNodes([
|
||||
$new_parent_node->id => $new_parent_node,
|
||||
]);
|
||||
$statements_analyzer->node_data->setType($stmt, $result_type);
|
||||
|
||||
if ($stmt_value_type && $stmt_value_type->parent_nodes) {
|
||||
foreach ($stmt_value_type->parent_nodes as $parent_node) {
|
||||
$statements_analyzer->data_flow_graph->addPath($parent_node, $new_parent_node, 'bitwisenot');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php
vendored
Normal file
57
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TBool;
|
||||
use Psalm\Type\Atomic\TFalse;
|
||||
use Psalm\Type\Atomic\TTrue;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class BooleanNotAnalyzer
|
||||
{
|
||||
public static function analyze(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\BooleanNot $stmt,
|
||||
Context $context
|
||||
): bool {
|
||||
|
||||
|
||||
$inside_negation = $context->inside_negation;
|
||||
|
||||
$context->inside_negation = !$inside_negation;
|
||||
|
||||
$result = ExpressionAnalyzer::analyze($statements_analyzer, $stmt->expr, $context);
|
||||
|
||||
$context->inside_negation = $inside_negation;
|
||||
|
||||
$expr_type = $statements_analyzer->node_data->getType($stmt->expr);
|
||||
|
||||
if ($expr_type) {
|
||||
if ($expr_type->isAlwaysTruthy()) {
|
||||
$stmt_type = new TFalse($expr_type->from_docblock);
|
||||
} elseif ($expr_type->isAlwaysFalsy()) {
|
||||
$stmt_type = new TTrue($expr_type->from_docblock);
|
||||
} else {
|
||||
$stmt_type = new TBool();
|
||||
}
|
||||
|
||||
$stmt_type = new Union([$stmt_type], [
|
||||
'parent_nodes' => $expr_type->parent_nodes,
|
||||
]);
|
||||
} else {
|
||||
$stmt_type = Type::getBool();
|
||||
}
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, $stmt_type);
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
1617
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php
vendored
Normal file
1617
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
145
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentMapPopulator.php
vendored
Normal file
145
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentMapPopulator.php
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
|
||||
|
||||
use PhpParser\Node\Expr;
|
||||
use PhpParser\Node\Expr\FuncCall;
|
||||
use PhpParser\Node\Expr\MethodCall;
|
||||
use PhpParser\Node\Expr\New_;
|
||||
use PhpParser\Node\Expr\StaticCall;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
|
||||
use function array_reverse;
|
||||
use function array_shift;
|
||||
use function is_string;
|
||||
use function reset;
|
||||
use function strlen;
|
||||
use function substr;
|
||||
use function token_get_all;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ArgumentMapPopulator
|
||||
{
|
||||
/**
|
||||
* @param MethodCall|StaticCall|FuncCall|New_ $stmt
|
||||
*/
|
||||
public static function recordArgumentPositions(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
Expr $stmt,
|
||||
Codebase $codebase,
|
||||
string $function_reference
|
||||
): void {
|
||||
$file_content = $codebase->file_provider->getContents($statements_analyzer->getFilePath());
|
||||
|
||||
// Find opening paren
|
||||
$first_argument = $stmt->getArgs()[0] ?? null;
|
||||
$first_argument_character = $first_argument !== null
|
||||
? $first_argument->getStartFilePos()
|
||||
: $stmt->getEndFilePos();
|
||||
$method_name_and_first_paren_source_code_length = $first_argument_character - $stmt->getStartFilePos();
|
||||
// FIXME: There are weird ::__construct calls in the AST for `extends`
|
||||
if ($method_name_and_first_paren_source_code_length <= 0) {
|
||||
return;
|
||||
}
|
||||
$method_name_and_first_paren_source_code = substr(
|
||||
$file_content,
|
||||
$stmt->getStartFilePos(),
|
||||
$method_name_and_first_paren_source_code_length,
|
||||
);
|
||||
$method_name_and_first_paren_tokens = token_get_all('<?php ' . $method_name_and_first_paren_source_code);
|
||||
$opening_paren_position = $first_argument_character;
|
||||
foreach (array_reverse($method_name_and_first_paren_tokens) as $token) {
|
||||
$token = is_string($token) ? $token : $token[1];
|
||||
$opening_paren_position -= strlen($token);
|
||||
|
||||
if ($token === '(') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// New instances can be created without parens
|
||||
if ($opening_paren_position < $stmt->getStartFilePos()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Record ranges of the source code that need to be tokenized to find commas
|
||||
/** @var array{0: int, 1: int}[] $ranges */
|
||||
$ranges = [];
|
||||
|
||||
// Add range between opening paren and first argument
|
||||
$first_argument = $stmt->getArgs()[0] ?? null;
|
||||
$first_argument_starting_position = $first_argument !== null
|
||||
? $first_argument->getStartFilePos()
|
||||
: $stmt->getEndFilePos();
|
||||
$first_range_starting_position = $opening_paren_position + 1;
|
||||
if ($first_range_starting_position !== $first_argument_starting_position) {
|
||||
$ranges[] = [$first_range_starting_position, $first_argument_starting_position];
|
||||
}
|
||||
|
||||
// Add range between arguments
|
||||
foreach ($stmt->getArgs() as $i => $argument) {
|
||||
$range_start = $argument->getEndFilePos() + 1;
|
||||
$next_argument = $stmt->getArgs()[$i + 1] ?? null;
|
||||
$range_end = $next_argument !== null
|
||||
? $next_argument->getStartFilePos()
|
||||
: $stmt->getEndFilePos();
|
||||
|
||||
if ($range_start !== $range_end) {
|
||||
$ranges[] = [$range_start, $range_end];
|
||||
}
|
||||
}
|
||||
|
||||
$commas = [];
|
||||
foreach ($ranges as $range) {
|
||||
$position = $range[0];
|
||||
$length = $range[1] - $position;
|
||||
|
||||
if ($length > 0) {
|
||||
$range_source_code = substr($file_content, $position, $length);
|
||||
$range_tokens = token_get_all('<?php ' . $range_source_code);
|
||||
|
||||
array_shift($range_tokens);
|
||||
|
||||
$current_position = $position;
|
||||
foreach ($range_tokens as $token) {
|
||||
$token = is_string($token) ? $token : $token[1];
|
||||
|
||||
if ($token === ',') {
|
||||
$commas[] = $current_position;
|
||||
}
|
||||
|
||||
$current_position += strlen($token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$argument_start_position = $opening_paren_position + 1;
|
||||
$argument_number = 0;
|
||||
while (!empty($commas)) {
|
||||
$comma = reset($commas);
|
||||
array_shift($commas);
|
||||
|
||||
$codebase->analyzer->addNodeArgument(
|
||||
$statements_analyzer->getFilePath(),
|
||||
$argument_start_position,
|
||||
$comma,
|
||||
$function_reference,
|
||||
$argument_number,
|
||||
);
|
||||
|
||||
++$argument_number;
|
||||
$argument_start_position = $comma + 1;
|
||||
}
|
||||
|
||||
$codebase->analyzer->addNodeArgument(
|
||||
$statements_analyzer->getFilePath(),
|
||||
$argument_start_position,
|
||||
$stmt->getEndFilePos(),
|
||||
$function_reference,
|
||||
$argument_number,
|
||||
);
|
||||
}
|
||||
}
|
||||
1805
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php
vendored
Normal file
1805
vendor/vimeo/psalm/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,965 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
|
||||
|
||||
use AssertionError;
|
||||
use PhpParser;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\ArrayAssignmentAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\ExpressionIdentifier;
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Codebase\InternalCallMapHandler;
|
||||
use Psalm\Internal\MethodIdentifier;
|
||||
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
use Psalm\Internal\Type\TemplateStandinTypeReplacer;
|
||||
use Psalm\Internal\Type\TypeCombiner;
|
||||
use Psalm\Internal\Type\TypeExpander;
|
||||
use Psalm\Issue\ArgumentTypeCoercion;
|
||||
use Psalm\Issue\InvalidArgument;
|
||||
use Psalm\Issue\InvalidScalarArgument;
|
||||
use Psalm\Issue\MixedArgumentTypeCoercion;
|
||||
use Psalm\Issue\PossiblyInvalidArgument;
|
||||
use Psalm\Issue\TooFewArguments;
|
||||
use Psalm\Issue\TooManyArguments;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Node\Expr\VirtualArrayDimFetch;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TCallable;
|
||||
use Psalm\Type\Atomic\TClosure;
|
||||
use Psalm\Type\Atomic\TKeyedArray;
|
||||
use Psalm\Type\Atomic\TList;
|
||||
use Psalm\Type\Atomic\TNonEmptyArray;
|
||||
use Psalm\Type\Union;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_filter;
|
||||
use function array_pop;
|
||||
use function array_shift;
|
||||
use function array_unshift;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function explode;
|
||||
use function strpos;
|
||||
use function strtolower;
|
||||
use function substr;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ArrayFunctionArgumentsAnalyzer
|
||||
{
|
||||
/**
|
||||
* @param array<int, PhpParser\Node\Arg> $args
|
||||
*/
|
||||
public static function checkArgumentsMatch(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
Context $context,
|
||||
array $args,
|
||||
string $method_id,
|
||||
bool $check_functions
|
||||
): void {
|
||||
$closure_index = $method_id === 'array_map' ? 0 : 1;
|
||||
|
||||
$array_arg_types = [];
|
||||
|
||||
foreach ($args as $i => $arg) {
|
||||
if ($i === 0 && $method_id === 'array_map') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($i === 1 && $method_id === 'array_filter') {
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var TKeyedArray|TArray|null
|
||||
*/
|
||||
$array_arg_type = ($arg_value_type = $statements_analyzer->node_data->getType($arg->value))
|
||||
&& $arg_value_type->hasArray()
|
||||
? $arg_value_type->getArray()
|
||||
: null;
|
||||
|
||||
if ($array_arg_type instanceof TKeyedArray) {
|
||||
$array_arg_type = $array_arg_type->getGenericArrayType();
|
||||
}
|
||||
|
||||
$array_arg_types[] = $array_arg_type;
|
||||
}
|
||||
|
||||
$closure_arg = $args[$closure_index] ?? null;
|
||||
|
||||
$closure_arg_type = null;
|
||||
|
||||
if ($closure_arg) {
|
||||
$closure_arg_type = $statements_analyzer->node_data->getType($closure_arg->value);
|
||||
}
|
||||
|
||||
if ($closure_arg && $closure_arg_type) {
|
||||
$min_closure_param_count = $max_closure_param_count = count($array_arg_types);
|
||||
|
||||
if ($method_id === 'array_filter') {
|
||||
$max_closure_param_count = count($args) > 2 ? 2 : 1;
|
||||
}
|
||||
|
||||
$new = [];
|
||||
foreach ($closure_arg_type->getAtomicTypes() as $closure_type) {
|
||||
self::checkClosureType(
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
$method_id,
|
||||
$closure_type,
|
||||
$closure_arg,
|
||||
$min_closure_param_count,
|
||||
$max_closure_param_count,
|
||||
$array_arg_types,
|
||||
$check_functions,
|
||||
);
|
||||
$new []= $closure_type;
|
||||
}
|
||||
|
||||
$statements_analyzer->node_data->setType(
|
||||
$closure_arg->value,
|
||||
$closure_arg_type->getBuilder()->setTypes($new)->freeze(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<PhpParser\Node\Arg> $args
|
||||
* @return false|null
|
||||
*/
|
||||
public static function handleAddition(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
array $args,
|
||||
Context $context,
|
||||
string $method_id
|
||||
): ?bool {
|
||||
$array_arg = $args[0]->value;
|
||||
$nb_args = count($args);
|
||||
|
||||
$unpacked_args = array_filter(
|
||||
$args,
|
||||
static fn(PhpParser\Node\Arg $arg): bool => $arg->unpack,
|
||||
);
|
||||
|
||||
if ($method_id === 'array_push' && !$unpacked_args) {
|
||||
for ($i = 1; $i < $nb_args; $i++) {
|
||||
$was_inside_assignment = $context->inside_assignment;
|
||||
|
||||
$context->inside_assignment = true;
|
||||
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$args[$i]->value,
|
||||
$context,
|
||||
) === false) {
|
||||
$context->inside_assignment = $was_inside_assignment;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$context->inside_assignment = $was_inside_assignment;
|
||||
|
||||
$old_node_data = $statements_analyzer->node_data;
|
||||
|
||||
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
||||
|
||||
ArrayAssignmentAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
new VirtualArrayDimFetch(
|
||||
$args[0]->value,
|
||||
null,
|
||||
$args[$i]->value->getAttributes(),
|
||||
),
|
||||
$context,
|
||||
$args[$i]->value,
|
||||
$statements_analyzer->node_data->getType($args[$i]->value) ?? Type::getMixed(),
|
||||
);
|
||||
|
||||
$statements_analyzer->node_data = $old_node_data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$context->inside_call = true;
|
||||
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
$context,
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for ($i = 1; $i < $nb_args; $i++) {
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$args[$i]->value,
|
||||
$context,
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (($array_arg_type = $statements_analyzer->node_data->getType($array_arg))
|
||||
&& $array_arg_type->hasArray()
|
||||
) {
|
||||
$array_type = $array_arg_type->getArray();
|
||||
|
||||
$objectlike_list = null;
|
||||
|
||||
if ($array_type instanceof TKeyedArray) {
|
||||
if ($array_type->is_list) {
|
||||
$objectlike_list = $array_type;
|
||||
}
|
||||
}
|
||||
|
||||
$by_ref_type = new Union([$array_type]);
|
||||
|
||||
foreach ($args as $argument_offset => $arg) {
|
||||
if ($argument_offset === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$arg->value,
|
||||
$context,
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($method_id === 'array_unshift' && $nb_args === 2 && !$unpacked_args) {
|
||||
$new_offset_type = Type::getInt(false, 0);
|
||||
} else {
|
||||
$new_offset_type = Type::getInt();
|
||||
}
|
||||
|
||||
if (!($arg_value_type = $statements_analyzer->node_data->getType($arg->value))
|
||||
|| $arg_value_type->hasMixed()
|
||||
) {
|
||||
$by_ref_type = Type::combineUnionTypes(
|
||||
$by_ref_type,
|
||||
new Union([new TArray([$new_offset_type, Type::getMixed()])]),
|
||||
);
|
||||
} elseif ($arg->unpack) {
|
||||
$arg_value_type = $arg_value_type->getBuilder();
|
||||
|
||||
foreach ($arg_value_type->getAtomicTypes() as $arg_value_atomic_type) {
|
||||
if ($arg_value_atomic_type instanceof TKeyedArray) {
|
||||
$was_list = $arg_value_atomic_type->is_list;
|
||||
|
||||
$arg_value_atomic_type = $arg_value_atomic_type->getGenericArrayType();
|
||||
|
||||
if ($was_list) {
|
||||
if ($arg_value_atomic_type instanceof TNonEmptyArray) {
|
||||
$arg_value_atomic_type = Type::getNonEmptyListAtomic(
|
||||
$arg_value_atomic_type->type_params[1],
|
||||
);
|
||||
} else {
|
||||
$arg_value_atomic_type = Type::getListAtomic(
|
||||
$arg_value_atomic_type->type_params[1],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$arg_value_type->addType($arg_value_atomic_type);
|
||||
}
|
||||
}
|
||||
$arg_value_type = $arg_value_type->freeze();
|
||||
|
||||
$by_ref_type = Type::combineUnionTypes(
|
||||
$by_ref_type,
|
||||
$arg_value_type,
|
||||
);
|
||||
} else {
|
||||
if ($objectlike_list) {
|
||||
$properties = $objectlike_list->properties;
|
||||
array_unshift($properties, $arg_value_type);
|
||||
|
||||
$by_ref_type = new Union([$objectlike_list->setProperties($properties)]);
|
||||
} elseif ($array_type instanceof TArray && $array_type->isEmptyArray()) {
|
||||
$by_ref_type = new Union([new TKeyedArray([
|
||||
$arg_value_type,
|
||||
], null, null, true)]);
|
||||
} else {
|
||||
$by_ref_type = Type::combineUnionTypes(
|
||||
$by_ref_type,
|
||||
new Union(
|
||||
[
|
||||
new TNonEmptyArray(
|
||||
[
|
||||
$new_offset_type,
|
||||
$arg_value_type,
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
null,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AssignmentAnalyzer::assignByRefParam(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
$by_ref_type,
|
||||
$by_ref_type,
|
||||
$context,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
$context->inside_call = false;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<PhpParser\Node\Arg> $args
|
||||
* @return false|null
|
||||
*/
|
||||
public static function handleSplice(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
array $args,
|
||||
Context $context
|
||||
): ?bool {
|
||||
$context->inside_call = true;
|
||||
$array_arg = $args[0]->value;
|
||||
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
$context,
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$offset_arg = $args[1]->value;
|
||||
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$offset_arg,
|
||||
$context,
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($args[2])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$length_arg = $args[2]->value;
|
||||
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$length_arg,
|
||||
$context,
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isset($args[3])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$replacement_arg = $args[3]->value;
|
||||
|
||||
if (ExpressionAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$replacement_arg,
|
||||
$context,
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$context->inside_call = false;
|
||||
|
||||
$replacement_arg_type = $statements_analyzer->node_data->getType($replacement_arg);
|
||||
|
||||
if ($replacement_arg_type
|
||||
&& !$replacement_arg_type->hasArray()
|
||||
&& $replacement_arg_type->hasString()
|
||||
&& $replacement_arg_type->isSingle()
|
||||
) {
|
||||
$replacement_arg_type = new Union([
|
||||
new TArray([Type::getInt(), $replacement_arg_type]),
|
||||
]);
|
||||
|
||||
$statements_analyzer->node_data->setType($replacement_arg, $replacement_arg_type);
|
||||
}
|
||||
|
||||
if (($array_arg_type = $statements_analyzer->node_data->getType($array_arg))
|
||||
&& $array_arg_type->hasArray()
|
||||
&& $replacement_arg_type
|
||||
&& $replacement_arg_type->hasArray()
|
||||
) {
|
||||
/**
|
||||
* @var TArray|TKeyedArray
|
||||
*/
|
||||
$array_type = $array_arg_type->getArray();
|
||||
|
||||
if ($array_type instanceof TKeyedArray) {
|
||||
if ($array_type->is_list) {
|
||||
$array_type = Type::getNonEmptyListAtomic($array_type->getGenericValueType());
|
||||
} else {
|
||||
$array_type = $array_type->getGenericArrayType();
|
||||
}
|
||||
}
|
||||
|
||||
if ($array_type instanceof TArray
|
||||
&& $array_type->type_params[0]->hasInt()
|
||||
&& !$array_type->type_params[0]->hasString()
|
||||
) {
|
||||
if ($array_type instanceof TNonEmptyArray) {
|
||||
$array_type = Type::getNonEmptyListAtomic($array_type->type_params[1]);
|
||||
} else {
|
||||
$array_type = Type::getListAtomic($array_type->type_params[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @var TArray|TKeyedArray
|
||||
*/
|
||||
$replacement_array_type = $replacement_arg_type->getArray();
|
||||
|
||||
if ($replacement_array_type instanceof TKeyedArray) {
|
||||
$was_list = $replacement_array_type->is_list;
|
||||
|
||||
$replacement_array_type = $replacement_array_type->getGenericArrayType();
|
||||
|
||||
if ($was_list) {
|
||||
if ($replacement_array_type instanceof TNonEmptyArray) {
|
||||
$replacement_array_type = Type::getNonEmptyListAtomic($replacement_array_type->type_params[1]);
|
||||
} else {
|
||||
$replacement_array_type = Type::getListAtomic($replacement_array_type->type_params[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$by_ref_type = TypeCombiner::combine([$array_type, $replacement_array_type]);
|
||||
|
||||
AssignmentAnalyzer::assignByRefParam(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
$by_ref_type,
|
||||
$by_ref_type,
|
||||
$context,
|
||||
false,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$array_type = Type::getArray();
|
||||
|
||||
AssignmentAnalyzer::assignByRefParam(
|
||||
$statements_analyzer,
|
||||
$array_arg,
|
||||
$array_type,
|
||||
$array_type,
|
||||
$context,
|
||||
false,
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function handleByRefArrayAdjustment(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Arg $arg,
|
||||
Context $context,
|
||||
bool $is_array_shift
|
||||
): void {
|
||||
$var_id = ExpressionIdentifier::getVarId(
|
||||
$arg->value,
|
||||
$statements_analyzer->getFQCLN(),
|
||||
$statements_analyzer,
|
||||
);
|
||||
|
||||
if ($var_id) {
|
||||
$context->removeVarFromConflictingClauses($var_id, null, $statements_analyzer);
|
||||
|
||||
if (isset($context->vars_in_scope[$var_id])) {
|
||||
$array_atomic_types = [];
|
||||
|
||||
foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $array_atomic_type) {
|
||||
if ($array_atomic_type instanceof TList) {
|
||||
$array_atomic_type = $array_atomic_type->getKeyedArray();
|
||||
}
|
||||
|
||||
if ($array_atomic_type instanceof TKeyedArray) {
|
||||
if ($is_array_shift && $array_atomic_type->is_list
|
||||
&& !$context->inside_loop
|
||||
) {
|
||||
$array_properties = $array_atomic_type->properties;
|
||||
|
||||
array_shift($array_properties);
|
||||
|
||||
if (!$array_properties) {
|
||||
$array_atomic_types []= $array_atomic_type->fallback_params
|
||||
? Type::getListAtomic($array_atomic_type->fallback_params[1])
|
||||
: Type::getEmptyArrayAtomic();
|
||||
} else {
|
||||
$array_atomic_types []= $array_atomic_type->setProperties($array_properties);
|
||||
}
|
||||
continue;
|
||||
} elseif (!$is_array_shift && $array_atomic_type->is_list
|
||||
&& !$array_atomic_type->fallback_params
|
||||
&& !$context->inside_loop
|
||||
) {
|
||||
$array_properties = $array_atomic_type->properties;
|
||||
|
||||
array_pop($array_properties);
|
||||
|
||||
if (!$array_properties) {
|
||||
$array_atomic_types []= Type::getEmptyArrayAtomic();
|
||||
} else {
|
||||
$array_atomic_types []= $array_atomic_type->setProperties($array_properties);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
$array_atomic_type = $array_atomic_type->is_list
|
||||
? Type::getListAtomic($array_atomic_type->getGenericValueType())
|
||||
: $array_atomic_type->getGenericArrayType();
|
||||
}
|
||||
|
||||
if ($array_atomic_type instanceof TNonEmptyArray) {
|
||||
if (!$context->inside_loop && $array_atomic_type->count !== null) {
|
||||
if ($array_atomic_type->count === 1) {
|
||||
$array_atomic_type = new TArray(
|
||||
[
|
||||
Type::getNever(),
|
||||
Type::getNever(),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
$array_atomic_type = $array_atomic_type->setCount($array_atomic_type->count-1);
|
||||
}
|
||||
} else {
|
||||
$array_atomic_type = new TArray($array_atomic_type->type_params);
|
||||
}
|
||||
|
||||
$array_atomic_types[] = $array_atomic_type;
|
||||
} elseif ($array_atomic_type instanceof TKeyedArray && $array_atomic_type->is_list) {
|
||||
if (!$context->inside_loop
|
||||
&& ($prop_count = $array_atomic_type->getMaxCount())
|
||||
&& $prop_count === $array_atomic_type->getMinCount()
|
||||
) {
|
||||
if ($prop_count === 1) {
|
||||
$array_atomic_type = new TArray(
|
||||
[
|
||||
Type::getNever(),
|
||||
Type::getNever(),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
$properties = $array_atomic_type->properties;
|
||||
unset($properties[$prop_count-1]);
|
||||
assert($properties !== []);
|
||||
$array_atomic_type = $array_atomic_type->setProperties($properties);
|
||||
}
|
||||
} else {
|
||||
$array_atomic_type = Type::getListAtomic($array_atomic_type->getGenericValueType());
|
||||
}
|
||||
|
||||
$array_atomic_types[] = $array_atomic_type;
|
||||
} else {
|
||||
$array_atomic_types[] = $array_atomic_type;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$array_atomic_types) {
|
||||
throw new AssertionError("We must have some types here!");
|
||||
}
|
||||
$array_type = new Union($array_atomic_types);
|
||||
$context->removeDescendents($var_id, $array_type);
|
||||
$context->vars_in_scope[$var_id] = $array_type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param (TArray|null)[] $array_arg_types
|
||||
*/
|
||||
private static function checkClosureType(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
Context $context,
|
||||
string $method_id,
|
||||
Atomic &$closure_type,
|
||||
PhpParser\Node\Arg $closure_arg,
|
||||
int $min_closure_param_count,
|
||||
int $max_closure_param_count,
|
||||
array $array_arg_types,
|
||||
bool $check_functions
|
||||
): void {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
if (!$closure_type instanceof TClosure) {
|
||||
if ($method_id === 'array_map') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$closure_arg->value instanceof PhpParser\Node\Scalar\String_
|
||||
&& !$closure_arg->value instanceof PhpParser\Node\Expr\Array_
|
||||
&& !$closure_arg->value instanceof PhpParser\Node\Expr\BinaryOp\Concat
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$function_ids = CallAnalyzer::getFunctionIdsFromCallableArg(
|
||||
$statements_analyzer,
|
||||
$closure_arg->value,
|
||||
);
|
||||
|
||||
$closure_types = [];
|
||||
|
||||
foreach ($function_ids as $function_id) {
|
||||
$function_id = strtolower($function_id);
|
||||
|
||||
if (strpos($function_id, '::') !== false) {
|
||||
if ($function_id[0] === '$') {
|
||||
$function_id = substr($function_id, 1);
|
||||
}
|
||||
|
||||
$function_id_parts = explode('&', $function_id);
|
||||
|
||||
foreach ($function_id_parts as $function_id_part) {
|
||||
[$callable_fq_class_name, $method_name] = explode('::', $function_id_part);
|
||||
|
||||
switch ($callable_fq_class_name) {
|
||||
case 'self':
|
||||
case 'static':
|
||||
case 'parent':
|
||||
$container_class = $statements_analyzer->getFQCLN();
|
||||
|
||||
if ($callable_fq_class_name === 'parent') {
|
||||
$container_class = $statements_analyzer->getParentFQCLN();
|
||||
}
|
||||
|
||||
if (!$container_class) {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$callable_fq_class_name = $container_class;
|
||||
}
|
||||
|
||||
if (!$codebase->classOrInterfaceExists($callable_fq_class_name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$function_id_part = new MethodIdentifier(
|
||||
$callable_fq_class_name,
|
||||
strtolower($method_name),
|
||||
);
|
||||
|
||||
try {
|
||||
$method_storage = $codebase->methods->getStorage($function_id_part);
|
||||
} catch (UnexpectedValueException $e) {
|
||||
// the method may not exist, but we're suppressing that issue
|
||||
continue;
|
||||
}
|
||||
|
||||
$closure_types[] = new TClosure(
|
||||
'Closure',
|
||||
$method_storage->params,
|
||||
$method_storage->return_type ?: Type::getMixed(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!$check_functions) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$codebase->functions->functionExists($statements_analyzer, $function_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$function_storage = $codebase->functions->getStorage(
|
||||
$statements_analyzer,
|
||||
$function_id,
|
||||
);
|
||||
|
||||
if (InternalCallMapHandler::inCallMap($function_id)) {
|
||||
$callmap_callables = InternalCallMapHandler::getCallablesFromCallMap($function_id);
|
||||
|
||||
if ($callmap_callables === null) {
|
||||
throw new UnexpectedValueException('This should not happen');
|
||||
}
|
||||
|
||||
$passing_callmap_callables = [];
|
||||
|
||||
foreach ($callmap_callables as $callmap_callable) {
|
||||
$required_param_count = 0;
|
||||
|
||||
assert($callmap_callable->params !== null);
|
||||
|
||||
foreach ($callmap_callable->params as $i => $param) {
|
||||
if (!$param->is_optional && !$param->is_variadic) {
|
||||
$required_param_count = $i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if ($required_param_count <= $max_closure_param_count) {
|
||||
$passing_callmap_callables[] = $callmap_callable;
|
||||
}
|
||||
}
|
||||
|
||||
if ($passing_callmap_callables) {
|
||||
foreach ($passing_callmap_callables as $passing_callmap_callable) {
|
||||
$closure_types[] = $passing_callmap_callable;
|
||||
}
|
||||
} else {
|
||||
$closure_types[] = $callmap_callables[0];
|
||||
}
|
||||
} else {
|
||||
$closure_types[] = new TClosure(
|
||||
'Closure',
|
||||
$function_storage->params,
|
||||
$function_storage->return_type ?: Type::getMixed(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$closure_types = [&$closure_type];
|
||||
}
|
||||
|
||||
foreach ($closure_types as &$closure_type) {
|
||||
if ($closure_type->params === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
self::checkClosureTypeArgs(
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
$method_id,
|
||||
$closure_type,
|
||||
$closure_arg,
|
||||
$min_closure_param_count,
|
||||
$max_closure_param_count,
|
||||
$array_arg_types,
|
||||
);
|
||||
}
|
||||
unset($closure_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TClosure|TCallable $closure_type
|
||||
* @param (TArray|null)[] $array_arg_types
|
||||
*/
|
||||
private static function checkClosureTypeArgs(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
Context $context,
|
||||
string $method_id,
|
||||
Atomic &$closure_type,
|
||||
PhpParser\Node\Arg $closure_arg,
|
||||
int $min_closure_param_count,
|
||||
int $max_closure_param_count,
|
||||
array $array_arg_types
|
||||
): void {
|
||||
$codebase = $statements_analyzer->getCodebase();
|
||||
|
||||
$closure_params = $closure_type->params;
|
||||
|
||||
if ($closure_params === null) {
|
||||
throw new UnexpectedValueException('Closure params should not be null here');
|
||||
}
|
||||
|
||||
$required_param_count = 0;
|
||||
|
||||
foreach ($closure_params as $i => $param) {
|
||||
if (!$param->is_optional && !$param->is_variadic) {
|
||||
$required_param_count = $i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($closure_params) < $min_closure_param_count) {
|
||||
$argument_text = $min_closure_param_count === 1 ? 'one argument' : $min_closure_param_count . ' arguments';
|
||||
|
||||
IssueBuffer::maybeAdd(
|
||||
new TooManyArguments(
|
||||
'The callable passed to ' . $method_id . ' will be called with ' . $argument_text . ', expecting '
|
||||
. $required_param_count,
|
||||
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
||||
$method_id,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($required_param_count > $max_closure_param_count) {
|
||||
$argument_text = $max_closure_param_count === 1 ? 'one argument' : $max_closure_param_count . ' arguments';
|
||||
|
||||
IssueBuffer::maybeAdd(
|
||||
new TooFewArguments(
|
||||
'The callable passed to ' . $method_id . ' will be called with ' . $argument_text . ', expecting '
|
||||
. $required_param_count,
|
||||
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
||||
$method_id,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// abandon attempt to validate closure params if we have an extra arg for ARRAY_FILTER
|
||||
if ($method_id === 'array_filter' && $max_closure_param_count > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($closure_params as $i => $closure_param) {
|
||||
if (!isset($array_arg_types[$i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$array_arg_type = $array_arg_types[$i];
|
||||
|
||||
$input_type = $array_arg_type->type_params[1];
|
||||
|
||||
if ($input_type->hasMixed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$closure_param_type = $closure_param->type;
|
||||
|
||||
if (!$closure_param_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($method_id === 'array_map'
|
||||
&& $i === 0
|
||||
&& $closure_type->return_type
|
||||
&& $closure_param_type->hasTemplate()
|
||||
) {
|
||||
$template_result = new TemplateResult(
|
||||
[],
|
||||
[],
|
||||
);
|
||||
|
||||
foreach ($closure_param_type->getTemplateTypes() as $template_type) {
|
||||
$template_result->template_types[$template_type->param_name] = [
|
||||
($template_type->defining_class) => $template_type->as,
|
||||
];
|
||||
}
|
||||
|
||||
$closure_param_type = TemplateStandinTypeReplacer::replace(
|
||||
$closure_param_type,
|
||||
$template_result,
|
||||
$codebase,
|
||||
$statements_analyzer,
|
||||
$input_type,
|
||||
$i,
|
||||
$context->self,
|
||||
$context->calling_method_id ?: $context->calling_function_id,
|
||||
);
|
||||
|
||||
$closure_type = $closure_type->replaceTemplateTypesWithArgTypes(
|
||||
$template_result,
|
||||
$codebase,
|
||||
);
|
||||
}
|
||||
|
||||
$closure_param_type = TypeExpander::expandUnion(
|
||||
$codebase,
|
||||
$closure_param_type,
|
||||
$context->self,
|
||||
null,
|
||||
$statements_analyzer->getParentFQCLN(),
|
||||
);
|
||||
|
||||
$union_comparison_results = new TypeComparisonResult();
|
||||
|
||||
$type_match_found = UnionTypeComparator::isContainedBy(
|
||||
$codebase,
|
||||
$input_type,
|
||||
$closure_param_type,
|
||||
$input_type->ignore_nullable_issues,
|
||||
$input_type->ignore_falsable_issues,
|
||||
$union_comparison_results,
|
||||
);
|
||||
|
||||
if ($union_comparison_results->type_coerced) {
|
||||
if ($union_comparison_results->type_coerced_from_mixed) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new MixedArgumentTypeCoercion(
|
||||
'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' .
|
||||
$closure_param_type->getId() .
|
||||
', but parent type ' . $input_type->getId() . ' provided',
|
||||
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
||||
$method_id,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
new ArgumentTypeCoercion(
|
||||
'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' .
|
||||
$closure_param_type->getId() .
|
||||
', but parent type ' . $input_type->getId() . ' provided',
|
||||
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
||||
$method_id,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$union_comparison_results->type_coerced && !$type_match_found) {
|
||||
$types_can_be_identical = UnionTypeComparator::canExpressionTypesBeIdentical(
|
||||
$codebase,
|
||||
$input_type,
|
||||
$closure_param_type,
|
||||
);
|
||||
|
||||
if ($union_comparison_results->scalar_type_match_found) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidScalarArgument(
|
||||
'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' .
|
||||
$closure_param_type->getId() . ', but ' . $input_type->getId() . ' provided',
|
||||
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
||||
$method_id,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} elseif ($types_can_be_identical) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new PossiblyInvalidArgument(
|
||||
'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects '
|
||||
. $closure_param_type->getId() . ', but possibly different type '
|
||||
. $input_type->getId() . ' provided',
|
||||
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
||||
$method_id,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
} else {
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidArgument(
|
||||
'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' .
|
||||
$closure_param_type->getId() . ', but ' . $input_type->getId() . ' provided',
|
||||
new CodeLocation($statements_analyzer->getSource(), $closure_arg),
|
||||
$method_id,
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
|
||||
|
||||
use AssertionError;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Internal\Type\TemplateResult;
|
||||
use Psalm\Internal\Type\TypeExpander;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TClassConstant;
|
||||
use Psalm\Type\Atomic\TGenericObject;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Union;
|
||||
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_search;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class ClassTemplateParamCollector
|
||||
{
|
||||
/**
|
||||
* @param lowercase-string $method_name
|
||||
* @return array<string, non-empty-array<string, Union>>|null
|
||||
* @psalm-suppress MoreSpecificReturnType
|
||||
* @psalm-suppress LessSpecificReturnStatement
|
||||
*/
|
||||
public static function collect(
|
||||
Codebase $codebase,
|
||||
ClassLikeStorage $class_storage,
|
||||
ClassLikeStorage $static_class_storage,
|
||||
?string $method_name = null,
|
||||
?Atomic $lhs_type_part = null,
|
||||
bool $self_call = false
|
||||
): ?array {
|
||||
$non_trait_class_storage = $class_storage->is_trait
|
||||
? $static_class_storage
|
||||
: $class_storage;
|
||||
|
||||
$template_types = $class_storage->template_types;
|
||||
|
||||
$candidate_class_storages = [$class_storage];
|
||||
|
||||
if ($static_class_storage->template_extended_params
|
||||
&& $method_name
|
||||
&& !empty($non_trait_class_storage->overridden_method_ids[$method_name])
|
||||
&& isset($class_storage->methods[$method_name])
|
||||
&& (!isset($non_trait_class_storage->methods[$method_name]->return_type)
|
||||
|| $class_storage->methods[$method_name]->inherited_return_type)
|
||||
) {
|
||||
foreach ($non_trait_class_storage->overridden_method_ids[$method_name] as $overridden_method_id) {
|
||||
$overridden_storage = $codebase->methods->getStorage($overridden_method_id);
|
||||
|
||||
if (!$overridden_storage->return_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($overridden_storage->return_type->isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fq_overridden_class = $overridden_method_id->fq_class_name;
|
||||
|
||||
$overridden_class_storage = $codebase->classlike_storage_provider->get($fq_overridden_class);
|
||||
|
||||
$overridden_template_types = $overridden_class_storage->template_types;
|
||||
|
||||
if (!$template_types) {
|
||||
$template_types = $overridden_template_types;
|
||||
} elseif ($overridden_template_types) {
|
||||
foreach ($overridden_template_types as $template_name => $template_map) {
|
||||
if (isset($template_types[$template_name])) {
|
||||
$template_types[$template_name] = array_merge(
|
||||
$template_types[$template_name],
|
||||
$template_map,
|
||||
);
|
||||
} else {
|
||||
$template_types[$template_name] = $template_map;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$candidate_class_storages[] = $overridden_class_storage;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$template_types) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class_template_params = [];
|
||||
$e = $static_class_storage->template_extended_params;
|
||||
|
||||
if ($lhs_type_part instanceof TGenericObject) {
|
||||
if ($class_storage === $static_class_storage && $class_storage->template_types) {
|
||||
$i = 0;
|
||||
|
||||
foreach ($class_storage->template_types as $type_name => $_) {
|
||||
if (isset($lhs_type_part->type_params[$i])) {
|
||||
$class_template_params[$type_name][$class_storage->name]
|
||||
= $lhs_type_part->type_params[$i];
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
$template_result = null;
|
||||
if ($class_storage !== $static_class_storage && $static_class_storage->template_types) {
|
||||
$templates = self::collect(
|
||||
$codebase,
|
||||
$static_class_storage,
|
||||
$static_class_storage,
|
||||
null,
|
||||
$lhs_type_part,
|
||||
);
|
||||
if ($templates === null) {
|
||||
throw new AssertionError("Could not collect templates!");
|
||||
}
|
||||
$template_result = new TemplateResult(
|
||||
$static_class_storage->template_types,
|
||||
$templates,
|
||||
);
|
||||
}
|
||||
foreach ($template_types as $type_name => $_) {
|
||||
if (isset($class_template_params[$type_name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($class_storage !== $static_class_storage
|
||||
&& isset($e[$class_storage->name][$type_name])
|
||||
) {
|
||||
$input_type_extends = $e[$class_storage->name][$type_name];
|
||||
|
||||
$output_type_extends = self::resolveTemplateParam(
|
||||
$codebase,
|
||||
$input_type_extends,
|
||||
$static_class_storage,
|
||||
$lhs_type_part,
|
||||
$template_result,
|
||||
);
|
||||
|
||||
$class_template_params[$type_name] = [
|
||||
$class_storage->name => $output_type_extends ?? Type::getMixed(),
|
||||
];
|
||||
} else {
|
||||
$class_template_params[$type_name] = [$class_storage->name => Type::getMixed()];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($template_types as $type_name => $type_map) {
|
||||
foreach ($type_map as $type) {
|
||||
foreach ($candidate_class_storages as $candidate_class_storage) {
|
||||
if ($candidate_class_storage !== $static_class_storage
|
||||
&& isset($e[$candidate_class_storage->name][$type_name])
|
||||
&& !isset($class_template_params[$type_name][$candidate_class_storage->name])
|
||||
) {
|
||||
$class_template_params[$type_name][$candidate_class_storage->name] = new Union(
|
||||
self::expandType(
|
||||
$codebase,
|
||||
$e[$candidate_class_storage->name][$type_name],
|
||||
$e,
|
||||
$static_class_storage->name,
|
||||
$static_class_storage->template_types,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$self_call) {
|
||||
if (!isset($class_template_params[$type_name])) {
|
||||
$class_template_params[$type_name][$class_storage->name] = $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $class_template_params;
|
||||
}
|
||||
|
||||
private static function resolveTemplateParam(
|
||||
Codebase $codebase,
|
||||
Union $input_type_extends,
|
||||
ClassLikeStorage $static_class_storage,
|
||||
TGenericObject $lhs_type_part,
|
||||
?TemplateResult $template_result = null
|
||||
): ?Union {
|
||||
$output_type_extends = null;
|
||||
foreach ($input_type_extends->getAtomicTypes() as $type_extends_atomic) {
|
||||
if ($type_extends_atomic instanceof TTemplateParam) {
|
||||
if (isset(
|
||||
$static_class_storage
|
||||
->template_types
|
||||
[$type_extends_atomic->param_name]
|
||||
[$type_extends_atomic->defining_class],
|
||||
)
|
||||
) {
|
||||
$mapped_offset = array_search(
|
||||
$type_extends_atomic->param_name,
|
||||
array_keys($static_class_storage->template_types),
|
||||
true,
|
||||
);
|
||||
|
||||
if ($mapped_offset !== false
|
||||
&& isset($lhs_type_part->type_params[$mapped_offset])
|
||||
) {
|
||||
$output_type_extends = Type::combineUnionTypes(
|
||||
$lhs_type_part->type_params[$mapped_offset],
|
||||
$output_type_extends,
|
||||
);
|
||||
}
|
||||
} elseif (isset(
|
||||
$static_class_storage
|
||||
->template_extended_params
|
||||
[$type_extends_atomic->defining_class]
|
||||
[$type_extends_atomic->param_name],
|
||||
)) {
|
||||
$nested_output_type = self::resolveTemplateParam(
|
||||
$codebase,
|
||||
$static_class_storage
|
||||
->template_extended_params
|
||||
[$type_extends_atomic->defining_class]
|
||||
[$type_extends_atomic->param_name],
|
||||
$static_class_storage,
|
||||
$lhs_type_part,
|
||||
$template_result,
|
||||
);
|
||||
if ($nested_output_type !== null) {
|
||||
$output_type_extends = Type::combineUnionTypes(
|
||||
$nested_output_type,
|
||||
$output_type_extends,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($template_result !== null) {
|
||||
$type_extends_atomic = $type_extends_atomic->replaceTemplateTypesWithArgTypes(
|
||||
$template_result,
|
||||
$codebase,
|
||||
);
|
||||
}
|
||||
$output_type_extends = Type::combineUnionTypes(
|
||||
new Union([$type_extends_atomic]),
|
||||
$output_type_extends,
|
||||
);
|
||||
}
|
||||
}
|
||||
return $output_type_extends;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, Union>> $e
|
||||
* @return non-empty-list<Atomic>
|
||||
*/
|
||||
private static function expandType(
|
||||
Codebase $codebase,
|
||||
Union $input_type_extends,
|
||||
array $e,
|
||||
string $static_fq_class_name,
|
||||
?array $static_template_types
|
||||
): array {
|
||||
$output_type_extends = [];
|
||||
|
||||
foreach ($input_type_extends->getAtomicTypes() as $type_extends_atomic) {
|
||||
if ($type_extends_atomic instanceof TTemplateParam
|
||||
&& ($static_fq_class_name !== $type_extends_atomic->defining_class
|
||||
|| !isset($static_template_types[$type_extends_atomic->param_name]))
|
||||
&& isset($e[$type_extends_atomic->defining_class][$type_extends_atomic->param_name])
|
||||
) {
|
||||
$output_type_extends = [...$output_type_extends, ...self::expandType(
|
||||
$codebase,
|
||||
$e[$type_extends_atomic->defining_class][$type_extends_atomic->param_name],
|
||||
$e,
|
||||
$static_fq_class_name,
|
||||
$static_template_types,
|
||||
)];
|
||||
} elseif ($type_extends_atomic instanceof TClassConstant) {
|
||||
$expanded = TypeExpander::expandAtomic(
|
||||
$codebase,
|
||||
$type_extends_atomic,
|
||||
$type_extends_atomic->fq_classlike_name,
|
||||
$type_extends_atomic->fq_classlike_name,
|
||||
null,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
foreach ($expanded as $type) {
|
||||
$output_type_extends[] = $type;
|
||||
}
|
||||
} else {
|
||||
$output_type_extends[] = $type_extends_atomic;
|
||||
}
|
||||
}
|
||||
|
||||
return $output_type_extends;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user