Find this useful? Enter your email to receive occasional updates for securing PHP code.
Signing you up...
Thank you for signing up!
PHP Decode
<?php namespace Psalm\Internal\Analyzer; use InvalidArgumentException; use PhpParser; us..
Decoded Output download
<?php
namespace Psalm\Internal\Analyzer;
use InvalidArgumentException;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\DocComment;
use Psalm\Exception\DocblockParseException;
use Psalm\Exception\IncorrectDocblockException;
use Psalm\Exception\TypeParseTreeException;
use Psalm\FileManipulation;
use Psalm\Internal\Analyzer\Statements\Block\DoAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\ForAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\IfElseAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\SwitchAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\TryAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\WhileAnalyzer;
use Psalm\Internal\Analyzer\Statements\BreakAnalyzer;
use Psalm\Internal\Analyzer\Statements\ContinueAnalyzer;
use Psalm\Internal\Analyzer\Statements\DeclareAnalyzer;
use Psalm\Internal\Analyzer\Statements\EchoAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\ClassConstAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\VariableFetchAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\SimpleTypeInferer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\Statements\GlobalAnalyzer;
use Psalm\Internal\Analyzer\Statements\ReturnAnalyzer;
use Psalm\Internal\Analyzer\Statements\StaticAnalyzer;
use Psalm\Internal\Analyzer\Statements\ThrowAnalyzer;
use Psalm\Internal\Analyzer\Statements\UnsetAnalyzer;
use Psalm\Internal\Analyzer\Statements\UnusedAssignmentRemover;
use Psalm\Internal\Codebase\DataFlowGraph;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\Internal\ReferenceConstraint;
use Psalm\Internal\Scanner\ParsedDocblock;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Internal\Type\TypeParser;
use Psalm\Internal\Type\TypeTokenizer;
use Psalm\Issue\CheckType;
use Psalm\Issue\ComplexFunction;
use Psalm\Issue\ComplexMethod;
use Psalm\Issue\InvalidDocblock;
use Psalm\Issue\MissingDocblockType;
use Psalm\Issue\Trace;
use Psalm\Issue\UndefinedDocblockClass;
use Psalm\Issue\UndefinedTrace;
use Psalm\Issue\UnevaluatedCode;
use Psalm\Issue\UnrecognizedStatement;
use Psalm\Issue\UnusedForeachValue;
use Psalm\Issue\UnusedVariable;
use Psalm\IssueBuffer;
use Psalm\NodeTypeProvider;
use Psalm\Plugin\EventHandler\Event\AfterStatementAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\BeforeStatementAnalysisEvent;
use Psalm\Type;
use UnexpectedValueException;
use function array_change_key_case;
use function array_column;
use function array_combine;
use function array_keys;
use function array_map;
use function array_search;
use function assert;
use function count;
use function explode;
use function fwrite;
use function get_class;
use function in_array;
use function is_string;
use function preg_split;
use function reset;
use function round;
use function strlen;
use function strpos;
use function strrpos;
use function strtolower;
use function substr;
use function trim;
use const PREG_SPLIT_NO_EMPTY;
use const STDERR;
/**
* @internal
*/
final class StatementsAnalyzer extends SourceAnalyzer
{
protected SourceAnalyzer $source;
protected FileAnalyzer $file_analyzer;
protected Codebase $codebase;
/**
* @var array<string, CodeLocation>
*/
private array $all_vars = [];
/**
* @var array<string, int>
*/
private array $var_branch_points = [];
/**
* Possibly undefined variables should be initialised if we're altering code
*
* @var array<string, int>|null
*/
private ?array $vars_to_initialize = null;
/**
* @var array<string, FunctionAnalyzer>
*/
private array $function_analyzers = [];
/**
* @var array<string, array{0: string, 1: CodeLocation}>
*/
private array $unused_var_locations = [];
/**
* @var array<string, true>
*/
public array $byref_uses = [];
private ?ParsedDocblock $parsed_docblock = null;
private ?string $fake_this_class = null;
public NodeDataProvider $node_data;
public ?DataFlowGraph $data_flow_graph = null;
/**
* Locations of foreach values
*
* Used to discern ordinary UnusedVariables from UnusedForeachValues
*
* @var array<string, list<CodeLocation>>
* @psalm-internal Psalm\Internal\Analyzer
*/
public array $foreach_var_locations = [];
public function __construct(SourceAnalyzer $source, NodeDataProvider $node_data)
{
$this->source = $source;
$this->file_analyzer = $source->getFileAnalyzer();
$this->codebase = $source->getCodebase();
$this->node_data = $node_data;
if ($this->codebase->taint_flow_graph) {
$this->data_flow_graph = new TaintFlowGraph();
} elseif ($this->codebase->find_unused_variables) {
$this->data_flow_graph = new VariableUseGraph();
}
}
/**
* Checks an array of statements for validity
*
* @param array<PhpParser\Node\Stmt> $stmts
* @return null|false
*/
public function analyze(
array $stmts,
Context $context,
?Context $global_context = null,
bool $root_scope = false
): ?bool {
if (!$stmts) {
return null;
}
// hoist functions to the top
$this->hoistFunctions($stmts, $context);
$project_analyzer = $this->getFileAnalyzer()->project_analyzer;
$codebase = $project_analyzer->getCodebase();
if ($codebase->config->hoist_constants) {
self::hoistConstants($this, $stmts, $context);
}
foreach ($stmts as $stmt) {
if (self::analyzeStatement($this, $stmt, $context, $global_context) === false) {
return false;
}
}
if ($root_scope
&& !$context->collect_initializations
&& !$context->collect_mutations
&& $codebase->find_unused_variables
&& $context->check_variables
) {
$this->checkUnreferencedVars($stmts, $context);
}
if ($codebase->alter_code && $root_scope && $this->vars_to_initialize) {
$file_contents = $codebase->getFileContents($this->getFilePath());
foreach ($this->vars_to_initialize as $var_id => $branch_point) {
$newline_pos = (int)strrpos($file_contents, "\n", $branch_point - strlen($file_contents)) + 1;
$indentation = substr($file_contents, $newline_pos, $branch_point - $newline_pos);
FileManipulationBuffer::add($this->getFilePath(), [
new FileManipulation($branch_point, $branch_point, $var_id . ' = null;' . "\n" . $indentation),
]);
}
}
if ($root_scope
&& $this->data_flow_graph instanceof TaintFlowGraph
&& $this->codebase->taint_flow_graph
&& $codebase->config->trackTaintsInPath($this->getFilePath())
) {
$this->codebase->taint_flow_graph->addGraph($this->data_flow_graph);
}
return null;
}
/**
* @param array<PhpParser\Node\Stmt> $stmts
*/
private function hoistFunctions(array $stmts, Context $context): void
{
foreach ($stmts as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\Function_) {
$function_name = strtolower($stmt->name->name);
if ($ns = $this->getNamespace()) {
$fq_function_name = strtolower($ns) . '\\' . $function_name;
} else {
$fq_function_name = $function_name;
}
if ($this->data_flow_graph
&& $this->codebase->find_unused_variables
) {
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
|| !is_string($var->name)
) {
continue;
}
$var_id = '$' . $var->name;
if ($var_id !== '$argv' && $var_id !== '$argc') {
$context->byref_constraints[$var_id] = new ReferenceConstraint();
}
}
}
}
}
try {
$function_analyzer = new FunctionAnalyzer($stmt, $this->source);
$this->function_analyzers[$fq_function_name] = $function_analyzer;
} catch (UnexpectedValueException $e) {
// do nothing
}
}
}
}
/**
* @param array<PhpParser\Node\Stmt> $stmts
*/
private static function hoistConstants(
StatementsAnalyzer $statements_analyzer,
array $stmts,
Context $context
): void {
$codebase = $statements_analyzer->getCodebase();
foreach ($stmts as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\Const_) {
foreach ($stmt->consts as $const) {
ConstFetchAnalyzer::setConstType(
$statements_analyzer,
$const->name->name,
SimpleTypeInferer::infer(
$codebase,
$statements_analyzer->node_data,
$const->value,
$statements_analyzer->getAliases(),
$statements_analyzer,
) ?? Type::getMixed(),
$context,
);
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Expression
&& $stmt->expr instanceof PhpParser\Node\Expr\FuncCall
&& $stmt->expr->name instanceof PhpParser\Node\Name
&& $stmt->expr->name->getParts() === ['define']
&& isset($stmt->expr->getArgs()[1])
) {
$const_name = ConstFetchAnalyzer::getConstName(
$stmt->expr->getArgs()[0]->value,
$statements_analyzer->node_data,
$codebase,
$statements_analyzer->getAliases(),
);
if ($const_name !== null) {
ConstFetchAnalyzer::setConstType(
$statements_analyzer,
$const_name,
SimpleTypeInferer::infer(
$codebase,
$statements_analyzer->node_data,
$stmt->expr->getArgs()[1]->value,
$statements_analyzer->getAliases(),
$statements_analyzer,
) ?? Type::getMixed(),
$context,
);
}
}
}
}
/**
* @return false|null
*/
private static function analyzeStatement(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Stmt $stmt,
Context $context,
?Context $global_context
): ?bool {
if (self::dispatchBeforeStatementAnalysis($stmt, $context, $statements_analyzer) === false) {
return false;
}
$ignore_variable_property = false;
$ignore_variable_method = false;
$codebase = $statements_analyzer->getCodebase();
if ($statements_analyzer->getProjectAnalyzer()->debug_lines) {
fwrite(STDERR, $statements_analyzer->getFilePath() . ':' . $stmt->getLine() . "\n");
}
$new_issues = null;
$traced_variables = [];
$checked_types = [];
if ($docblock = $stmt->getDocComment()) {
$statements_analyzer->parseStatementDocblock($docblock, $stmt, $context);
if (isset($statements_analyzer->parsed_docblock->tags['psalm-trace'])) {
foreach ($statements_analyzer->parsed_docblock->tags['psalm-trace'] as $traced_variable_line) {
$possible_traced_variable_names = preg_split(
'/(?:\s*,\s*|\s+)/',
$traced_variable_line,
-1,
PREG_SPLIT_NO_EMPTY,
);
if ($possible_traced_variable_names) {
$traced_variables = [...$traced_variables, ...$possible_traced_variable_names];
}
}
}
foreach ($statements_analyzer->parsed_docblock->tags['psalm-check-type'] ?? [] as $inexact_check) {
$checked_types[] = [$inexact_check, false];
}
foreach ($statements_analyzer->parsed_docblock->tags['psalm-check-type-exact'] ?? [] as $exact_check) {
$checked_types[] = [$exact_check, true];
}
if (isset($statements_analyzer->parsed_docblock->tags['psalm-ignore-variable-method'])) {
$context->ignore_variable_method = $ignore_variable_method = true;
}
if (isset($statements_analyzer->parsed_docblock->tags['psalm-ignore-variable-property'])) {
$context->ignore_variable_property = $ignore_variable_property = true;
}
if (isset($statements_analyzer->parsed_docblock->tags['psalm-suppress'])) {
$suppressed = $statements_analyzer->parsed_docblock->tags['psalm-suppress'];
if ($suppressed) {
$new_issues = [];
foreach ($suppressed as $offset => $suppress_entry) {
foreach (DocComment::parseSuppressList($suppress_entry) as $issue_offset => $issue_type) {
$new_issues[$issue_offset + $offset] = $issue_type;
}
}
if ($codebase->track_unused_suppressions
&& (
(count($new_issues) === 1) // UnusedPsalmSuppress by itself should be marked as unused
|| !in_array("UnusedPsalmSuppress", $new_issues)
)
) {
foreach ($new_issues as $offset => $issue_type) {
if ($issue_type === 'InaccessibleMethod') {
continue;
}
IssueBuffer::addUnusedSuppression(
$statements_analyzer->getFilePath(),
$offset,
$issue_type,
);
}
}
$statements_analyzer->addSuppressedIssues($new_issues);
}
}
if (isset($statements_analyzer->parsed_docblock->combined_tags['var'])
&& !($stmt instanceof PhpParser\Node\Stmt\Expression
&& $stmt->expr instanceof PhpParser\Node\Expr\Assign)
&& !$stmt instanceof PhpParser\Node\Stmt\Foreach_
&& !$stmt instanceof PhpParser\Node\Stmt\Return_
) {
$file_path = $statements_analyzer->getRootFilePath();
$file_storage_provider = $codebase->file_storage_provider;
$file_storage = $file_storage_provider->get($file_path);
$template_type_map = $statements_analyzer->getTemplateTypeMap();
$var_comments = [];
try {
$var_comments = $codebase->config->disable_var_parsing
? []
: CommentAnalyzer::arrayToDocblocks(
$docblock,
$statements_analyzer->parsed_docblock,
$statements_analyzer->getSource(),
$statements_analyzer->getAliases(),
$template_type_map,
$file_storage->type_aliases,
);
} catch (IncorrectDocblockException $e) {
IssueBuffer::maybeAdd(
new MissingDocblockType(
$e->getMessage(),
new CodeLocation($statements_analyzer->getSource(), $stmt),
),
);
} catch (DocblockParseException $e) {
IssueBuffer::maybeAdd(
new InvalidDocblock(
$e->getMessage(),
new CodeLocation($statements_analyzer->getSource(), $stmt),
),
);
}
foreach ($var_comments as $var_comment) {
AssignmentAnalyzer::assignTypeFromVarDocblock(
$statements_analyzer,
$stmt,
$var_comment,
$context,
);
if ($var_comment->var_id === '$this'
&& $var_comment->type
&& $codebase->classExists((string)$var_comment->type)
) {
$statements_analyzer->setFQCLN((string)$var_comment->type);
}
}
}
} else {
$statements_analyzer->parsed_docblock = null;
}
if ($context->has_returned
&& !$context->collect_initializations
&& !$context->collect_mutations
&& !($stmt instanceof PhpParser\Node\Stmt\Nop)
&& !($stmt instanceof PhpParser\Node\Stmt\Function_)
&& !($stmt instanceof PhpParser\Node\Stmt\Class_)
&& !($stmt instanceof PhpParser\Node\Stmt\Interface_)
&& !($stmt instanceof PhpParser\Node\Stmt\Trait_)
&& !($stmt instanceof PhpParser\Node\Stmt\HaltCompiler)
&& !($stmt instanceof PhpParser\Node\Stmt\Declare_)
) {
if ($codebase->find_unused_variables) {
IssueBuffer::maybeAdd(
new UnevaluatedCode(
'Expressions after return/throw/continue',
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->source->getSuppressedIssues(),
);
}
return null;
}
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
if (IfElseAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
if (TryAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\For_) {
if (ForAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Foreach_) {
if (ForeachAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\While_) {
if (WhileAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Do_) {
if (DoAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Const_) {
ConstFetchAnalyzer::analyzeConstAssignment($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Unset_) {
UnsetAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Return_) {
ReturnAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Throw_) {
ThrowAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
SwitchAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Break_) {
BreakAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
ContinueAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Static_) {
StaticAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Echo_) {
if (EchoAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) {
FunctionAnalyzer::analyzeStatement($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Expression) {
if (ExpressionAnalyzer::analyze(
$statements_analyzer,
$stmt->expr,
$context,
false,
$global_context,
true,
) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\InlineHTML) {
// do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Global_) {
GlobalAnalyzer::analyze($statements_analyzer, $stmt, $context, $global_context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Property) {
InstancePropertyAssignmentAnalyzer::analyzeStatement($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\ClassConst) {
ClassConstAnalyzer::analyzeAssignment($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Class_) {
try {
$class_analyzer = new ClassAnalyzer(
$stmt,
$statements_analyzer->source,
$stmt->name->name ?? null,
);
$class_analyzer->analyze(null, $global_context);
} catch (InvalidArgumentException $e) {
// disregard this exception, we'll likely see it elsewhere in the form
// of an issue
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Trait_) {
TraitAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Nop) {
// do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Goto_) {
// do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Label) {
// do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Declare_) {
DeclareAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\HaltCompiler) {
$context->has_returned = true;
} else {
if (IssueBuffer::accepts(
new UnrecognizedStatement(
'Psalm does not understand ' . get_class($stmt),
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
)) {
return false;
}
}
if (self::dispatchAfterStatementAnalysis($stmt, $context, $statements_analyzer) === false) {
return false;
}
if ($new_issues) {
$statements_analyzer->removeSuppressedIssues($new_issues);
}
if ($ignore_variable_property) {
$context->ignore_variable_property = false;
}
if ($ignore_variable_method) {
$context->ignore_variable_method = false;
}
foreach ($traced_variables as $traced_variable) {
if (isset($context->vars_in_scope[$traced_variable])) {
IssueBuffer::maybeAdd(
new Trace(
$traced_variable . ': ' . $context->vars_in_scope[$traced_variable]->getId(),
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
} else {
IssueBuffer::maybeAdd(
new UndefinedTrace(
'Attempt to trace undefined variable ' . $traced_variable,
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
}
}
foreach ($checked_types as [$check_type_line, $is_exact]) {
[$checked_var, $check_type_string] = array_map('trim', explode('=', $check_type_line, 2)) + ['', ''];
if ($check_type_string === '' || $checked_var === '') {
IssueBuffer::maybeAdd(
new InvalidDocblock(
"Invalid format for @psalm-check-type" . ($is_exact ? "-exact" : ""),
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
} else {
$checked_var_id = $checked_var;
$possibly_undefined = strrpos($checked_var_id, "?") === strlen($checked_var_id) - 1;
if ($possibly_undefined) {
$checked_var_id = substr($checked_var_id, 0, strlen($checked_var_id) - 1);
}
if (!isset($context->vars_in_scope[$checked_var_id])) {
IssueBuffer::maybeAdd(
new InvalidDocblock(
"Attempt to check undefined variable $checked_var_id",
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
} else {
try {
$checked_type = $context->vars_in_scope[$checked_var_id];
$path = $statements_analyzer->getRootFilePath();
$file_storage = $codebase->file_storage_provider->get($path);
$check_tokens = TypeTokenizer::getFullyQualifiedTokens(
$check_type_string,
$statements_analyzer->getAliases(),
$statements_analyzer->getTemplateTypeMap(),
$file_storage->type_aliases,
);
$check_type = TypeParser::parseTokens(
$check_tokens,
null,
$statements_analyzer->getTemplateTypeMap() ?? [],
$file_storage->type_aliases,
true,
);
/** @psalm-suppress InaccessibleProperty We just created this type */
$check_type->possibly_undefined = $possibly_undefined;
if ($check_type->possibly_undefined !== $checked_type->possibly_undefined
|| !UnionTypeComparator::isContainedBy($codebase, $checked_type, $check_type)
|| ($is_exact && !UnionTypeComparator::isContainedBy($codebase, $check_type, $checked_type))
) {
$check_var = $checked_var_id . ($checked_type->possibly_undefined ? "?" : "");
IssueBuffer::maybeAdd(
new CheckType(
"Checked variable $checked_var = {$check_type->getId()} does not match "
. "$check_var = {$checked_type->getId()}",
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
}
} catch (TypeParseTreeException $e) {
IssueBuffer::maybeAdd(
new InvalidDocblock(
$e->getMessage(),
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
}
}
}
}
return null;
}
private static function dispatchAfterStatementAnalysis(
PhpParser\Node\Stmt $stmt,
Context $context,
StatementsAnalyzer $statements_analyzer
): ?bool {
$codebase = $statements_analyzer->getCodebase();
$event = new AfterStatementAnalysisEvent(
$stmt,
$context,
$statements_analyzer,
$codebase,
[],
);
if ($codebase->config->eventDispatcher->dispatchAfterStatementAnalysis($event) === false) {
return false;
}
$file_manipulations = $event->getFileReplacements();
if ($file_manipulations) {
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
return null;
}
private static function dispatchBeforeStatementAnalysis(
PhpParser\Node\Stmt $stmt,
Context $context,
StatementsAnalyzer $statements_analyzer
): ?bool {
$codebase = $statements_analyzer->getCodebase();
$event = new BeforeStatementAnalysisEvent(
$stmt,
$context,
$statements_analyzer,
$codebase,
[],
);
if ($codebase->config->eventDispatcher->dispatchBeforeStatementAnalysis($event) === false) {
return false;
}
$file_manipulations = $event->getFileReplacements();
if ($file_manipulations) {
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
return null;
}
private function parseStatementDocblock(
PhpParser\Comment\Doc $docblock,
PhpParser\Node\Stmt $stmt,
Context $context
): void {
$codebase = $this->getCodebase();
try {
$this->parsed_docblock = DocComment::parsePreservingLength($docblock);
} catch (DocblockParseException $e) {
IssueBuffer::maybeAdd(
new InvalidDocblock(
$e->getMessage(),
new CodeLocation($this->getSource(), $stmt, null, true),
),
);
$this->parsed_docblock = null;
}
if ($this->parsed_docblock === null) {
try {
$this->parsed_docblock = DocComment::parsePreservingLength($docblock, true);
} catch (DocblockParseException $e) {
// already reported above
}
}
$comments = $this->parsed_docblock;
if (isset($comments->tags['psalm-scope-this'])) {
$trimmed = trim(reset($comments->tags['psalm-scope-this']));
$scope_fqcn = Type::getFQCLNFromString($trimmed, $this->getAliases());
if (!$codebase->classExists($scope_fqcn)) {
IssueBuffer::maybeAdd(
new UndefinedDocblockClass(
'Scope class ' . $scope_fqcn . ' does not exist',
new CodeLocation($this->getSource(), $stmt, null, true),
$scope_fqcn,
),
);
} else {
$this_type = Type::parseString($scope_fqcn);
$context->self = $scope_fqcn;
$context->vars_in_scope['$this'] = $this_type;
$this->setFQCLN($scope_fqcn);
}
}
}
/**
* @param array<PhpParser\Node\Stmt> $stmts
*/
public function checkUnreferencedVars(array $stmts, Context $context): void
{
$source = $this->getSource();
$codebase = $source->getCodebase();
$function_storage = $source instanceof FunctionLikeAnalyzer ? $source->getFunctionLikeStorage($this) : null;
$var_list = array_column($this->unused_var_locations, 0);
$loc_list = array_column($this->unused_var_locations, 1);
$project_analyzer = $this->getProjectAnalyzer();
$unused_var_remover = new UnusedAssignmentRemover();
if ($this->data_flow_graph instanceof VariableUseGraph
&& $codebase->config->limit_method_complexity
&& $source instanceof FunctionLikeAnalyzer
&& !$source instanceof ClosureAnalyzer
&& $function_storage
&& $function_storage->location
) {
[$count, , $unique_destinations, $mean] = $this->data_flow_graph->getEdgeStats();
$average_destination_branches_converging = $unique_destinations > 0 ? $count / $unique_destinations : 0;
if ($count > $codebase->config->max_graph_size
&& $mean > $codebase->config->max_avg_path_length
&& $average_destination_branches_converging > 1.1
) {
if ($source instanceof FunctionAnalyzer) {
IssueBuffer::maybeAdd(
new ComplexFunction(
'This functions complexity is greater than the project limit'
. ' (method graph size = ' . $count .', average path length = ' . round($mean). ')',
$function_storage->location,
),
$this->getSuppressedIssues(),
);
} elseif ($source instanceof MethodAnalyzer) {
IssueBuffer::maybeAdd(
new ComplexMethod(
'This methods complexity is greater than the project limit'
. ' (method graph size = ' . $count .', average path length = ' . round($mean) . ')',
$function_storage->location,
),
$this->getSuppressedIssues(),
);
}
}
}
foreach ($this->unused_var_locations as [$var_id, $original_location]) {
if (strpos($var_id, '$_') === 0) {
continue;
}
if ($function_storage) {
$param_index = array_search(substr($var_id, 1), array_keys($function_storage->param_lookup), true);
if ($param_index !== false) {
$param = $function_storage->params[$param_index];
if ($param->location
&& ($original_location->raw_file_end === $param->location->raw_file_end
|| $param->by_ref)
) {
continue;
}
}
}
$assignment_node = DataFlowNode::getForAssignment($var_id, $original_location);
if (!isset($this->byref_uses[$var_id])
&& !isset($context->referenced_globals[$var_id])
&& !VariableFetchAnalyzer::isSuperGlobal($var_id)
&& $this->data_flow_graph instanceof VariableUseGraph
&& !$this->data_flow_graph->isVariableUsed($assignment_node)
) {
$is_foreach_var = false;
if (isset($this->foreach_var_locations[$var_id])) {
foreach ($this->foreach_var_locations[$var_id] as $location) {
if ($location->raw_file_start === $original_location->raw_file_start) {
$is_foreach_var = true;
break;
}
}
}
if ($is_foreach_var) {
$issue = new UnusedForeachValue(
$var_id . ' is never referenced or the value is not used',
$original_location,
);
} else {
$issue = new UnusedVariable(
$var_id . ' is never referenced or the value is not used',
$original_location,
);
}
if ($codebase->alter_code
&& $issue instanceof UnusedVariable
&& !$unused_var_remover->checkIfVarRemoved($var_id, $original_location)
&& isset($project_analyzer->getIssuesToFix()['UnusedVariable'])
&& !IssueBuffer::isSuppressed($issue, $this->getSuppressedIssues())
) {
$unused_var_remover->findUnusedAssignment(
$this->getCodebase(),
$stmts,
array_combine($var_list, $loc_list),
$var_id,
$original_location,
);
}
IssueBuffer::maybeAdd(
$issue,
$this->getSuppressedIssues(),
$issue instanceof UnusedVariable,
);
}
}
}
public function hasVariable(string $var_name): bool
{
return isset($this->all_vars[$var_name]);
}
public function registerVariable(string $var_id, CodeLocation $location, ?int $branch_point): void
{
$this->all_vars[$var_id] = $location;
if ($branch_point) {
$this->var_branch_points[$var_id] = $branch_point;
}
$this->registerVariableAssignment($var_id, $location);
}
public function registerVariableAssignment(string $var_id, CodeLocation $location): void
{
$this->unused_var_locations[$location->getHash()] = [$var_id, $location];
}
/**
* @return array<string, array{0: string, 1: CodeLocation}>
*/
public function getUnusedVarLocations(): array
{
return $this->unused_var_locations;
}
public function registerPossiblyUndefinedVariable(
string $undefined_var_id,
PhpParser\Node\Expr\Variable $stmt
): void {
if (!$this->data_flow_graph) {
return;
}
$use_location = new CodeLocation($this->getSource(), $stmt);
$use_node = DataFlowNode::getForAssignment($undefined_var_id, $use_location);
$stmt_type = $this->node_data->getType($stmt);
if ($stmt_type) {
$stmt_type = $stmt_type->addParentNodes([$use_node->id => $use_node]);
$this->node_data->setType($stmt, $stmt_type);
}
foreach ($this->unused_var_locations as [$var_id, $original_location]) {
if ($var_id === $undefined_var_id) {
$parent_node = DataFlowNode::getForAssignment($var_id, $original_location);
$this->data_flow_graph->addPath($parent_node, $use_node, '=');
}
}
}
/**
* @return array<string, DataFlowNode>
*/
public function getParentNodesForPossiblyUndefinedVariable(string $undefined_var_id): array
{
if (!$this->data_flow_graph) {
return [];
}
$parent_nodes = [];
foreach ($this->unused_var_locations as [$var_id, $original_location]) {
if ($var_id === $undefined_var_id) {
$assignment_node = DataFlowNode::getForAssignment($var_id, $original_location);
$parent_nodes[$assignment_node->id] = $assignment_node;
}
}
return $parent_nodes;
}
/**
* The first appearance of the variable in this set of statements being evaluated
*/
public function getFirstAppearance(string $var_id): ?CodeLocation
{
return $this->all_vars[$var_id] ?? null;
}
public function getBranchPoint(string $var_id): ?int
{
return $this->var_branch_points[$var_id] ?? null;
}
public function addVariableInitialization(string $var_id, int $branch_point): void
{
$this->vars_to_initialize[$var_id] = $branch_point;
}
public function getFileAnalyzer(): FileAnalyzer
{
return $this->file_analyzer;
}
public function getCodebase(): Codebase
{
return $this->codebase;
}
/**
* @return array<string, FunctionAnalyzer>
*/
public function getFunctionAnalyzers(): array
{
return $this->function_analyzers;
}
/**
* @param array<string, true> $byref_uses
*/
public function setByRefUses(array $byref_uses): void
{
$this->byref_uses = $byref_uses;
}
/**
* @return array<string, array<array-key, CodeLocation>>
*/
public function getUncaughtThrows(Context $context): array
{
$uncaught_throws = [];
if ($context->collect_exceptions) {
if ($context->possibly_thrown_exceptions) {
$config = $this->codebase->config;
$ignored_exceptions = array_change_key_case(
$context->is_global ?
$config->ignored_exceptions_in_global_scope :
$config->ignored_exceptions,
);
$ignored_exceptions_and_descendants = array_change_key_case(
$context->is_global ?
$config->ignored_exceptions_and_descendants_in_global_scope :
$config->ignored_exceptions_and_descendants,
);
foreach ($context->possibly_thrown_exceptions as $possibly_thrown_exception => $codelocations) {
if (isset($ignored_exceptions[strtolower($possibly_thrown_exception)])) {
continue;
}
$is_expected = false;
foreach ($ignored_exceptions_and_descendants as $expected_exception => $_) {
try {
if ($expected_exception === strtolower($possibly_thrown_exception)
|| $this->codebase->classExtends($possibly_thrown_exception, $expected_exception)
|| $this->codebase->interfaceExtends($possibly_thrown_exception, $expected_exception)
) {
$is_expected = true;
break;
}
} catch (InvalidArgumentException $e) {
$is_expected = true;
break;
}
}
if (!$is_expected) {
$uncaught_throws[$possibly_thrown_exception] = $codelocations;
}
}
}
}
return $uncaught_throws;
}
public function getFunctionAnalyzer(string $function_id): ?FunctionAnalyzer
{
return $this->function_analyzers[$function_id] ?? null;
}
public function getParsedDocblock(): ?ParsedDocblock
{
return $this->parsed_docblock;
}
/** @psalm-mutation-free */
public function getFQCLN(): ?string
{
if ($this->fake_this_class) {
return $this->fake_this_class;
}
return parent::getFQCLN();
}
public function setFQCLN(string $fake_this_class): void
{
$this->fake_this_class = $fake_this_class;
}
/**
* @return NodeDataProvider
*/
public function getNodeTypeProvider(): NodeTypeProvider
{
return $this->node_data;
}
public function getFullyQualifiedFunctionMethodOrNamespaceName(): ?string
{
if ($this->source instanceof MethodAnalyzer) {
$fqcn = $this->getFQCLN();
$method_name = $this->source->getFunctionLikeStorage($this)->cased_name;
assert($fqcn !== null && $method_name !== null);
return "$fqcn::$method_name";
}
if ($this->source instanceof FunctionAnalyzer) {
$namespace = $this->getNamespace();
$namespace = $namespace === "" ? "" : "$namespace\\";
$function_name = $this->source->getFunctionLikeStorage($this)->cased_name;
assert($function_name !== null);
return "{$namespace}{$function_name}";
}
return $this->getNamespace();
}
}
?>
Did this file decode correctly?
Original Code
<?php
namespace Psalm\Internal\Analyzer;
use InvalidArgumentException;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\DocComment;
use Psalm\Exception\DocblockParseException;
use Psalm\Exception\IncorrectDocblockException;
use Psalm\Exception\TypeParseTreeException;
use Psalm\FileManipulation;
use Psalm\Internal\Analyzer\Statements\Block\DoAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\ForAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\IfElseAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\SwitchAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\TryAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\WhileAnalyzer;
use Psalm\Internal\Analyzer\Statements\BreakAnalyzer;
use Psalm\Internal\Analyzer\Statements\ContinueAnalyzer;
use Psalm\Internal\Analyzer\Statements\DeclareAnalyzer;
use Psalm\Internal\Analyzer\Statements\EchoAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\ClassConstAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\VariableFetchAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\SimpleTypeInferer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\Statements\GlobalAnalyzer;
use Psalm\Internal\Analyzer\Statements\ReturnAnalyzer;
use Psalm\Internal\Analyzer\Statements\StaticAnalyzer;
use Psalm\Internal\Analyzer\Statements\ThrowAnalyzer;
use Psalm\Internal\Analyzer\Statements\UnsetAnalyzer;
use Psalm\Internal\Analyzer\Statements\UnusedAssignmentRemover;
use Psalm\Internal\Codebase\DataFlowGraph;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\Internal\ReferenceConstraint;
use Psalm\Internal\Scanner\ParsedDocblock;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Internal\Type\TypeParser;
use Psalm\Internal\Type\TypeTokenizer;
use Psalm\Issue\CheckType;
use Psalm\Issue\ComplexFunction;
use Psalm\Issue\ComplexMethod;
use Psalm\Issue\InvalidDocblock;
use Psalm\Issue\MissingDocblockType;
use Psalm\Issue\Trace;
use Psalm\Issue\UndefinedDocblockClass;
use Psalm\Issue\UndefinedTrace;
use Psalm\Issue\UnevaluatedCode;
use Psalm\Issue\UnrecognizedStatement;
use Psalm\Issue\UnusedForeachValue;
use Psalm\Issue\UnusedVariable;
use Psalm\IssueBuffer;
use Psalm\NodeTypeProvider;
use Psalm\Plugin\EventHandler\Event\AfterStatementAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\BeforeStatementAnalysisEvent;
use Psalm\Type;
use UnexpectedValueException;
use function array_change_key_case;
use function array_column;
use function array_combine;
use function array_keys;
use function array_map;
use function array_search;
use function assert;
use function count;
use function explode;
use function fwrite;
use function get_class;
use function in_array;
use function is_string;
use function preg_split;
use function reset;
use function round;
use function strlen;
use function strpos;
use function strrpos;
use function strtolower;
use function substr;
use function trim;
use const PREG_SPLIT_NO_EMPTY;
use const STDERR;
/**
* @internal
*/
final class StatementsAnalyzer extends SourceAnalyzer
{
protected SourceAnalyzer $source;
protected FileAnalyzer $file_analyzer;
protected Codebase $codebase;
/**
* @var array<string, CodeLocation>
*/
private array $all_vars = [];
/**
* @var array<string, int>
*/
private array $var_branch_points = [];
/**
* Possibly undefined variables should be initialised if we're altering code
*
* @var array<string, int>|null
*/
private ?array $vars_to_initialize = null;
/**
* @var array<string, FunctionAnalyzer>
*/
private array $function_analyzers = [];
/**
* @var array<string, array{0: string, 1: CodeLocation}>
*/
private array $unused_var_locations = [];
/**
* @var array<string, true>
*/
public array $byref_uses = [];
private ?ParsedDocblock $parsed_docblock = null;
private ?string $fake_this_class = null;
public NodeDataProvider $node_data;
public ?DataFlowGraph $data_flow_graph = null;
/**
* Locations of foreach values
*
* Used to discern ordinary UnusedVariables from UnusedForeachValues
*
* @var array<string, list<CodeLocation>>
* @psalm-internal Psalm\Internal\Analyzer
*/
public array $foreach_var_locations = [];
public function __construct(SourceAnalyzer $source, NodeDataProvider $node_data)
{
$this->source = $source;
$this->file_analyzer = $source->getFileAnalyzer();
$this->codebase = $source->getCodebase();
$this->node_data = $node_data;
if ($this->codebase->taint_flow_graph) {
$this->data_flow_graph = new TaintFlowGraph();
} elseif ($this->codebase->find_unused_variables) {
$this->data_flow_graph = new VariableUseGraph();
}
}
/**
* Checks an array of statements for validity
*
* @param array<PhpParser\Node\Stmt> $stmts
* @return null|false
*/
public function analyze(
array $stmts,
Context $context,
?Context $global_context = null,
bool $root_scope = false
): ?bool {
if (!$stmts) {
return null;
}
// hoist functions to the top
$this->hoistFunctions($stmts, $context);
$project_analyzer = $this->getFileAnalyzer()->project_analyzer;
$codebase = $project_analyzer->getCodebase();
if ($codebase->config->hoist_constants) {
self::hoistConstants($this, $stmts, $context);
}
foreach ($stmts as $stmt) {
if (self::analyzeStatement($this, $stmt, $context, $global_context) === false) {
return false;
}
}
if ($root_scope
&& !$context->collect_initializations
&& !$context->collect_mutations
&& $codebase->find_unused_variables
&& $context->check_variables
) {
$this->checkUnreferencedVars($stmts, $context);
}
if ($codebase->alter_code && $root_scope && $this->vars_to_initialize) {
$file_contents = $codebase->getFileContents($this->getFilePath());
foreach ($this->vars_to_initialize as $var_id => $branch_point) {
$newline_pos = (int)strrpos($file_contents, "\n", $branch_point - strlen($file_contents)) + 1;
$indentation = substr($file_contents, $newline_pos, $branch_point - $newline_pos);
FileManipulationBuffer::add($this->getFilePath(), [
new FileManipulation($branch_point, $branch_point, $var_id . ' = null;' . "\n" . $indentation),
]);
}
}
if ($root_scope
&& $this->data_flow_graph instanceof TaintFlowGraph
&& $this->codebase->taint_flow_graph
&& $codebase->config->trackTaintsInPath($this->getFilePath())
) {
$this->codebase->taint_flow_graph->addGraph($this->data_flow_graph);
}
return null;
}
/**
* @param array<PhpParser\Node\Stmt> $stmts
*/
private function hoistFunctions(array $stmts, Context $context): void
{
foreach ($stmts as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\Function_) {
$function_name = strtolower($stmt->name->name);
if ($ns = $this->getNamespace()) {
$fq_function_name = strtolower($ns) . '\\' . $function_name;
} else {
$fq_function_name = $function_name;
}
if ($this->data_flow_graph
&& $this->codebase->find_unused_variables
) {
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
|| !is_string($var->name)
) {
continue;
}
$var_id = '$' . $var->name;
if ($var_id !== '$argv' && $var_id !== '$argc') {
$context->byref_constraints[$var_id] = new ReferenceConstraint();
}
}
}
}
}
try {
$function_analyzer = new FunctionAnalyzer($stmt, $this->source);
$this->function_analyzers[$fq_function_name] = $function_analyzer;
} catch (UnexpectedValueException $e) {
// do nothing
}
}
}
}
/**
* @param array<PhpParser\Node\Stmt> $stmts
*/
private static function hoistConstants(
StatementsAnalyzer $statements_analyzer,
array $stmts,
Context $context
): void {
$codebase = $statements_analyzer->getCodebase();
foreach ($stmts as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\Const_) {
foreach ($stmt->consts as $const) {
ConstFetchAnalyzer::setConstType(
$statements_analyzer,
$const->name->name,
SimpleTypeInferer::infer(
$codebase,
$statements_analyzer->node_data,
$const->value,
$statements_analyzer->getAliases(),
$statements_analyzer,
) ?? Type::getMixed(),
$context,
);
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Expression
&& $stmt->expr instanceof PhpParser\Node\Expr\FuncCall
&& $stmt->expr->name instanceof PhpParser\Node\Name
&& $stmt->expr->name->getParts() === ['define']
&& isset($stmt->expr->getArgs()[1])
) {
$const_name = ConstFetchAnalyzer::getConstName(
$stmt->expr->getArgs()[0]->value,
$statements_analyzer->node_data,
$codebase,
$statements_analyzer->getAliases(),
);
if ($const_name !== null) {
ConstFetchAnalyzer::setConstType(
$statements_analyzer,
$const_name,
SimpleTypeInferer::infer(
$codebase,
$statements_analyzer->node_data,
$stmt->expr->getArgs()[1]->value,
$statements_analyzer->getAliases(),
$statements_analyzer,
) ?? Type::getMixed(),
$context,
);
}
}
}
}
/**
* @return false|null
*/
private static function analyzeStatement(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Stmt $stmt,
Context $context,
?Context $global_context
): ?bool {
if (self::dispatchBeforeStatementAnalysis($stmt, $context, $statements_analyzer) === false) {
return false;
}
$ignore_variable_property = false;
$ignore_variable_method = false;
$codebase = $statements_analyzer->getCodebase();
if ($statements_analyzer->getProjectAnalyzer()->debug_lines) {
fwrite(STDERR, $statements_analyzer->getFilePath() . ':' . $stmt->getLine() . "\n");
}
$new_issues = null;
$traced_variables = [];
$checked_types = [];
if ($docblock = $stmt->getDocComment()) {
$statements_analyzer->parseStatementDocblock($docblock, $stmt, $context);
if (isset($statements_analyzer->parsed_docblock->tags['psalm-trace'])) {
foreach ($statements_analyzer->parsed_docblock->tags['psalm-trace'] as $traced_variable_line) {
$possible_traced_variable_names = preg_split(
'/(?:\s*,\s*|\s+)/',
$traced_variable_line,
-1,
PREG_SPLIT_NO_EMPTY,
);
if ($possible_traced_variable_names) {
$traced_variables = [...$traced_variables, ...$possible_traced_variable_names];
}
}
}
foreach ($statements_analyzer->parsed_docblock->tags['psalm-check-type'] ?? [] as $inexact_check) {
$checked_types[] = [$inexact_check, false];
}
foreach ($statements_analyzer->parsed_docblock->tags['psalm-check-type-exact'] ?? [] as $exact_check) {
$checked_types[] = [$exact_check, true];
}
if (isset($statements_analyzer->parsed_docblock->tags['psalm-ignore-variable-method'])) {
$context->ignore_variable_method = $ignore_variable_method = true;
}
if (isset($statements_analyzer->parsed_docblock->tags['psalm-ignore-variable-property'])) {
$context->ignore_variable_property = $ignore_variable_property = true;
}
if (isset($statements_analyzer->parsed_docblock->tags['psalm-suppress'])) {
$suppressed = $statements_analyzer->parsed_docblock->tags['psalm-suppress'];
if ($suppressed) {
$new_issues = [];
foreach ($suppressed as $offset => $suppress_entry) {
foreach (DocComment::parseSuppressList($suppress_entry) as $issue_offset => $issue_type) {
$new_issues[$issue_offset + $offset] = $issue_type;
}
}
if ($codebase->track_unused_suppressions
&& (
(count($new_issues) === 1) // UnusedPsalmSuppress by itself should be marked as unused
|| !in_array("UnusedPsalmSuppress", $new_issues)
)
) {
foreach ($new_issues as $offset => $issue_type) {
if ($issue_type === 'InaccessibleMethod') {
continue;
}
IssueBuffer::addUnusedSuppression(
$statements_analyzer->getFilePath(),
$offset,
$issue_type,
);
}
}
$statements_analyzer->addSuppressedIssues($new_issues);
}
}
if (isset($statements_analyzer->parsed_docblock->combined_tags['var'])
&& !($stmt instanceof PhpParser\Node\Stmt\Expression
&& $stmt->expr instanceof PhpParser\Node\Expr\Assign)
&& !$stmt instanceof PhpParser\Node\Stmt\Foreach_
&& !$stmt instanceof PhpParser\Node\Stmt\Return_
) {
$file_path = $statements_analyzer->getRootFilePath();
$file_storage_provider = $codebase->file_storage_provider;
$file_storage = $file_storage_provider->get($file_path);
$template_type_map = $statements_analyzer->getTemplateTypeMap();
$var_comments = [];
try {
$var_comments = $codebase->config->disable_var_parsing
? []
: CommentAnalyzer::arrayToDocblocks(
$docblock,
$statements_analyzer->parsed_docblock,
$statements_analyzer->getSource(),
$statements_analyzer->getAliases(),
$template_type_map,
$file_storage->type_aliases,
);
} catch (IncorrectDocblockException $e) {
IssueBuffer::maybeAdd(
new MissingDocblockType(
$e->getMessage(),
new CodeLocation($statements_analyzer->getSource(), $stmt),
),
);
} catch (DocblockParseException $e) {
IssueBuffer::maybeAdd(
new InvalidDocblock(
$e->getMessage(),
new CodeLocation($statements_analyzer->getSource(), $stmt),
),
);
}
foreach ($var_comments as $var_comment) {
AssignmentAnalyzer::assignTypeFromVarDocblock(
$statements_analyzer,
$stmt,
$var_comment,
$context,
);
if ($var_comment->var_id === '$this'
&& $var_comment->type
&& $codebase->classExists((string)$var_comment->type)
) {
$statements_analyzer->setFQCLN((string)$var_comment->type);
}
}
}
} else {
$statements_analyzer->parsed_docblock = null;
}
if ($context->has_returned
&& !$context->collect_initializations
&& !$context->collect_mutations
&& !($stmt instanceof PhpParser\Node\Stmt\Nop)
&& !($stmt instanceof PhpParser\Node\Stmt\Function_)
&& !($stmt instanceof PhpParser\Node\Stmt\Class_)
&& !($stmt instanceof PhpParser\Node\Stmt\Interface_)
&& !($stmt instanceof PhpParser\Node\Stmt\Trait_)
&& !($stmt instanceof PhpParser\Node\Stmt\HaltCompiler)
&& !($stmt instanceof PhpParser\Node\Stmt\Declare_)
) {
if ($codebase->find_unused_variables) {
IssueBuffer::maybeAdd(
new UnevaluatedCode(
'Expressions after return/throw/continue',
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->source->getSuppressedIssues(),
);
}
return null;
}
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
if (IfElseAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
if (TryAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\For_) {
if (ForAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Foreach_) {
if (ForeachAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\While_) {
if (WhileAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Do_) {
if (DoAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Const_) {
ConstFetchAnalyzer::analyzeConstAssignment($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Unset_) {
UnsetAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Return_) {
ReturnAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Throw_) {
ThrowAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
SwitchAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Break_) {
BreakAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
ContinueAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Static_) {
StaticAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Echo_) {
if (EchoAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) {
FunctionAnalyzer::analyzeStatement($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Expression) {
if (ExpressionAnalyzer::analyze(
$statements_analyzer,
$stmt->expr,
$context,
false,
$global_context,
true,
) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\InlineHTML) {
// do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Global_) {
GlobalAnalyzer::analyze($statements_analyzer, $stmt, $context, $global_context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Property) {
InstancePropertyAssignmentAnalyzer::analyzeStatement($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\ClassConst) {
ClassConstAnalyzer::analyzeAssignment($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Class_) {
try {
$class_analyzer = new ClassAnalyzer(
$stmt,
$statements_analyzer->source,
$stmt->name->name ?? null,
);
$class_analyzer->analyze(null, $global_context);
} catch (InvalidArgumentException $e) {
// disregard this exception, we'll likely see it elsewhere in the form
// of an issue
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Trait_) {
TraitAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Nop) {
// do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Goto_) {
// do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Label) {
// do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Declare_) {
DeclareAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\HaltCompiler) {
$context->has_returned = true;
} else {
if (IssueBuffer::accepts(
new UnrecognizedStatement(
'Psalm does not understand ' . get_class($stmt),
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
)) {
return false;
}
}
if (self::dispatchAfterStatementAnalysis($stmt, $context, $statements_analyzer) === false) {
return false;
}
if ($new_issues) {
$statements_analyzer->removeSuppressedIssues($new_issues);
}
if ($ignore_variable_property) {
$context->ignore_variable_property = false;
}
if ($ignore_variable_method) {
$context->ignore_variable_method = false;
}
foreach ($traced_variables as $traced_variable) {
if (isset($context->vars_in_scope[$traced_variable])) {
IssueBuffer::maybeAdd(
new Trace(
$traced_variable . ': ' . $context->vars_in_scope[$traced_variable]->getId(),
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
} else {
IssueBuffer::maybeAdd(
new UndefinedTrace(
'Attempt to trace undefined variable ' . $traced_variable,
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
}
}
foreach ($checked_types as [$check_type_line, $is_exact]) {
[$checked_var, $check_type_string] = array_map('trim', explode('=', $check_type_line, 2)) + ['', ''];
if ($check_type_string === '' || $checked_var === '') {
IssueBuffer::maybeAdd(
new InvalidDocblock(
"Invalid format for @psalm-check-type" . ($is_exact ? "-exact" : ""),
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
} else {
$checked_var_id = $checked_var;
$possibly_undefined = strrpos($checked_var_id, "?") === strlen($checked_var_id) - 1;
if ($possibly_undefined) {
$checked_var_id = substr($checked_var_id, 0, strlen($checked_var_id) - 1);
}
if (!isset($context->vars_in_scope[$checked_var_id])) {
IssueBuffer::maybeAdd(
new InvalidDocblock(
"Attempt to check undefined variable $checked_var_id",
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
} else {
try {
$checked_type = $context->vars_in_scope[$checked_var_id];
$path = $statements_analyzer->getRootFilePath();
$file_storage = $codebase->file_storage_provider->get($path);
$check_tokens = TypeTokenizer::getFullyQualifiedTokens(
$check_type_string,
$statements_analyzer->getAliases(),
$statements_analyzer->getTemplateTypeMap(),
$file_storage->type_aliases,
);
$check_type = TypeParser::parseTokens(
$check_tokens,
null,
$statements_analyzer->getTemplateTypeMap() ?? [],
$file_storage->type_aliases,
true,
);
/** @psalm-suppress InaccessibleProperty We just created this type */
$check_type->possibly_undefined = $possibly_undefined;
if ($check_type->possibly_undefined !== $checked_type->possibly_undefined
|| !UnionTypeComparator::isContainedBy($codebase, $checked_type, $check_type)
|| ($is_exact && !UnionTypeComparator::isContainedBy($codebase, $check_type, $checked_type))
) {
$check_var = $checked_var_id . ($checked_type->possibly_undefined ? "?" : "");
IssueBuffer::maybeAdd(
new CheckType(
"Checked variable $checked_var = {$check_type->getId()} does not match "
. "$check_var = {$checked_type->getId()}",
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
}
} catch (TypeParseTreeException $e) {
IssueBuffer::maybeAdd(
new InvalidDocblock(
$e->getMessage(),
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
}
}
}
}
return null;
}
private static function dispatchAfterStatementAnalysis(
PhpParser\Node\Stmt $stmt,
Context $context,
StatementsAnalyzer $statements_analyzer
): ?bool {
$codebase = $statements_analyzer->getCodebase();
$event = new AfterStatementAnalysisEvent(
$stmt,
$context,
$statements_analyzer,
$codebase,
[],
);
if ($codebase->config->eventDispatcher->dispatchAfterStatementAnalysis($event) === false) {
return false;
}
$file_manipulations = $event->getFileReplacements();
if ($file_manipulations) {
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
return null;
}
private static function dispatchBeforeStatementAnalysis(
PhpParser\Node\Stmt $stmt,
Context $context,
StatementsAnalyzer $statements_analyzer
): ?bool {
$codebase = $statements_analyzer->getCodebase();
$event = new BeforeStatementAnalysisEvent(
$stmt,
$context,
$statements_analyzer,
$codebase,
[],
);
if ($codebase->config->eventDispatcher->dispatchBeforeStatementAnalysis($event) === false) {
return false;
}
$file_manipulations = $event->getFileReplacements();
if ($file_manipulations) {
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
return null;
}
private function parseStatementDocblock(
PhpParser\Comment\Doc $docblock,
PhpParser\Node\Stmt $stmt,
Context $context
): void {
$codebase = $this->getCodebase();
try {
$this->parsed_docblock = DocComment::parsePreservingLength($docblock);
} catch (DocblockParseException $e) {
IssueBuffer::maybeAdd(
new InvalidDocblock(
$e->getMessage(),
new CodeLocation($this->getSource(), $stmt, null, true),
),
);
$this->parsed_docblock = null;
}
if ($this->parsed_docblock === null) {
try {
$this->parsed_docblock = DocComment::parsePreservingLength($docblock, true);
} catch (DocblockParseException $e) {
// already reported above
}
}
$comments = $this->parsed_docblock;
if (isset($comments->tags['psalm-scope-this'])) {
$trimmed = trim(reset($comments->tags['psalm-scope-this']));
$scope_fqcn = Type::getFQCLNFromString($trimmed, $this->getAliases());
if (!$codebase->classExists($scope_fqcn)) {
IssueBuffer::maybeAdd(
new UndefinedDocblockClass(
'Scope class ' . $scope_fqcn . ' does not exist',
new CodeLocation($this->getSource(), $stmt, null, true),
$scope_fqcn,
),
);
} else {
$this_type = Type::parseString($scope_fqcn);
$context->self = $scope_fqcn;
$context->vars_in_scope['$this'] = $this_type;
$this->setFQCLN($scope_fqcn);
}
}
}
/**
* @param array<PhpParser\Node\Stmt> $stmts
*/
public function checkUnreferencedVars(array $stmts, Context $context): void
{
$source = $this->getSource();
$codebase = $source->getCodebase();
$function_storage = $source instanceof FunctionLikeAnalyzer ? $source->getFunctionLikeStorage($this) : null;
$var_list = array_column($this->unused_var_locations, 0);
$loc_list = array_column($this->unused_var_locations, 1);
$project_analyzer = $this->getProjectAnalyzer();
$unused_var_remover = new UnusedAssignmentRemover();
if ($this->data_flow_graph instanceof VariableUseGraph
&& $codebase->config->limit_method_complexity
&& $source instanceof FunctionLikeAnalyzer
&& !$source instanceof ClosureAnalyzer
&& $function_storage
&& $function_storage->location
) {
[$count, , $unique_destinations, $mean] = $this->data_flow_graph->getEdgeStats();
$average_destination_branches_converging = $unique_destinations > 0 ? $count / $unique_destinations : 0;
if ($count > $codebase->config->max_graph_size
&& $mean > $codebase->config->max_avg_path_length
&& $average_destination_branches_converging > 1.1
) {
if ($source instanceof FunctionAnalyzer) {
IssueBuffer::maybeAdd(
new ComplexFunction(
'This functions complexity is greater than the project limit'
. ' (method graph size = ' . $count .', average path length = ' . round($mean). ')',
$function_storage->location,
),
$this->getSuppressedIssues(),
);
} elseif ($source instanceof MethodAnalyzer) {
IssueBuffer::maybeAdd(
new ComplexMethod(
'This methods complexity is greater than the project limit'
. ' (method graph size = ' . $count .', average path length = ' . round($mean) . ')',
$function_storage->location,
),
$this->getSuppressedIssues(),
);
}
}
}
foreach ($this->unused_var_locations as [$var_id, $original_location]) {
if (strpos($var_id, '$_') === 0) {
continue;
}
if ($function_storage) {
$param_index = array_search(substr($var_id, 1), array_keys($function_storage->param_lookup), true);
if ($param_index !== false) {
$param = $function_storage->params[$param_index];
if ($param->location
&& ($original_location->raw_file_end === $param->location->raw_file_end
|| $param->by_ref)
) {
continue;
}
}
}
$assignment_node = DataFlowNode::getForAssignment($var_id, $original_location);
if (!isset($this->byref_uses[$var_id])
&& !isset($context->referenced_globals[$var_id])
&& !VariableFetchAnalyzer::isSuperGlobal($var_id)
&& $this->data_flow_graph instanceof VariableUseGraph
&& !$this->data_flow_graph->isVariableUsed($assignment_node)
) {
$is_foreach_var = false;
if (isset($this->foreach_var_locations[$var_id])) {
foreach ($this->foreach_var_locations[$var_id] as $location) {
if ($location->raw_file_start === $original_location->raw_file_start) {
$is_foreach_var = true;
break;
}
}
}
if ($is_foreach_var) {
$issue = new UnusedForeachValue(
$var_id . ' is never referenced or the value is not used',
$original_location,
);
} else {
$issue = new UnusedVariable(
$var_id . ' is never referenced or the value is not used',
$original_location,
);
}
if ($codebase->alter_code
&& $issue instanceof UnusedVariable
&& !$unused_var_remover->checkIfVarRemoved($var_id, $original_location)
&& isset($project_analyzer->getIssuesToFix()['UnusedVariable'])
&& !IssueBuffer::isSuppressed($issue, $this->getSuppressedIssues())
) {
$unused_var_remover->findUnusedAssignment(
$this->getCodebase(),
$stmts,
array_combine($var_list, $loc_list),
$var_id,
$original_location,
);
}
IssueBuffer::maybeAdd(
$issue,
$this->getSuppressedIssues(),
$issue instanceof UnusedVariable,
);
}
}
}
public function hasVariable(string $var_name): bool
{
return isset($this->all_vars[$var_name]);
}
public function registerVariable(string $var_id, CodeLocation $location, ?int $branch_point): void
{
$this->all_vars[$var_id] = $location;
if ($branch_point) {
$this->var_branch_points[$var_id] = $branch_point;
}
$this->registerVariableAssignment($var_id, $location);
}
public function registerVariableAssignment(string $var_id, CodeLocation $location): void
{
$this->unused_var_locations[$location->getHash()] = [$var_id, $location];
}
/**
* @return array<string, array{0: string, 1: CodeLocation}>
*/
public function getUnusedVarLocations(): array
{
return $this->unused_var_locations;
}
public function registerPossiblyUndefinedVariable(
string $undefined_var_id,
PhpParser\Node\Expr\Variable $stmt
): void {
if (!$this->data_flow_graph) {
return;
}
$use_location = new CodeLocation($this->getSource(), $stmt);
$use_node = DataFlowNode::getForAssignment($undefined_var_id, $use_location);
$stmt_type = $this->node_data->getType($stmt);
if ($stmt_type) {
$stmt_type = $stmt_type->addParentNodes([$use_node->id => $use_node]);
$this->node_data->setType($stmt, $stmt_type);
}
foreach ($this->unused_var_locations as [$var_id, $original_location]) {
if ($var_id === $undefined_var_id) {
$parent_node = DataFlowNode::getForAssignment($var_id, $original_location);
$this->data_flow_graph->addPath($parent_node, $use_node, '=');
}
}
}
/**
* @return array<string, DataFlowNode>
*/
public function getParentNodesForPossiblyUndefinedVariable(string $undefined_var_id): array
{
if (!$this->data_flow_graph) {
return [];
}
$parent_nodes = [];
foreach ($this->unused_var_locations as [$var_id, $original_location]) {
if ($var_id === $undefined_var_id) {
$assignment_node = DataFlowNode::getForAssignment($var_id, $original_location);
$parent_nodes[$assignment_node->id] = $assignment_node;
}
}
return $parent_nodes;
}
/**
* The first appearance of the variable in this set of statements being evaluated
*/
public function getFirstAppearance(string $var_id): ?CodeLocation
{
return $this->all_vars[$var_id] ?? null;
}
public function getBranchPoint(string $var_id): ?int
{
return $this->var_branch_points[$var_id] ?? null;
}
public function addVariableInitialization(string $var_id, int $branch_point): void
{
$this->vars_to_initialize[$var_id] = $branch_point;
}
public function getFileAnalyzer(): FileAnalyzer
{
return $this->file_analyzer;
}
public function getCodebase(): Codebase
{
return $this->codebase;
}
/**
* @return array<string, FunctionAnalyzer>
*/
public function getFunctionAnalyzers(): array
{
return $this->function_analyzers;
}
/**
* @param array<string, true> $byref_uses
*/
public function setByRefUses(array $byref_uses): void
{
$this->byref_uses = $byref_uses;
}
/**
* @return array<string, array<array-key, CodeLocation>>
*/
public function getUncaughtThrows(Context $context): array
{
$uncaught_throws = [];
if ($context->collect_exceptions) {
if ($context->possibly_thrown_exceptions) {
$config = $this->codebase->config;
$ignored_exceptions = array_change_key_case(
$context->is_global ?
$config->ignored_exceptions_in_global_scope :
$config->ignored_exceptions,
);
$ignored_exceptions_and_descendants = array_change_key_case(
$context->is_global ?
$config->ignored_exceptions_and_descendants_in_global_scope :
$config->ignored_exceptions_and_descendants,
);
foreach ($context->possibly_thrown_exceptions as $possibly_thrown_exception => $codelocations) {
if (isset($ignored_exceptions[strtolower($possibly_thrown_exception)])) {
continue;
}
$is_expected = false;
foreach ($ignored_exceptions_and_descendants as $expected_exception => $_) {
try {
if ($expected_exception === strtolower($possibly_thrown_exception)
|| $this->codebase->classExtends($possibly_thrown_exception, $expected_exception)
|| $this->codebase->interfaceExtends($possibly_thrown_exception, $expected_exception)
) {
$is_expected = true;
break;
}
} catch (InvalidArgumentException $e) {
$is_expected = true;
break;
}
}
if (!$is_expected) {
$uncaught_throws[$possibly_thrown_exception] = $codelocations;
}
}
}
}
return $uncaught_throws;
}
public function getFunctionAnalyzer(string $function_id): ?FunctionAnalyzer
{
return $this->function_analyzers[$function_id] ?? null;
}
public function getParsedDocblock(): ?ParsedDocblock
{
return $this->parsed_docblock;
}
/** @psalm-mutation-free */
public function getFQCLN(): ?string
{
if ($this->fake_this_class) {
return $this->fake_this_class;
}
return parent::getFQCLN();
}
public function setFQCLN(string $fake_this_class): void
{
$this->fake_this_class = $fake_this_class;
}
/**
* @return NodeDataProvider
*/
public function getNodeTypeProvider(): NodeTypeProvider
{
return $this->node_data;
}
public function getFullyQualifiedFunctionMethodOrNamespaceName(): ?string
{
if ($this->source instanceof MethodAnalyzer) {
$fqcn = $this->getFQCLN();
$method_name = $this->source->getFunctionLikeStorage($this)->cased_name;
assert($fqcn !== null && $method_name !== null);
return "$fqcn::$method_name";
}
if ($this->source instanceof FunctionAnalyzer) {
$namespace = $this->getNamespace();
$namespace = $namespace === "" ? "" : "$namespace\\";
$function_name = $this->source->getFunctionLikeStorage($this)->cased_name;
assert($function_name !== null);
return "{$namespace}{$function_name}";
}
return $this->getNamespace();
}
}
Function Calls
None |
Stats
MD5 | 40dbaa2893afa6f00b07953d98209e7c |
Eval Count | 0 |
Decode Time | 112 ms |