mirror of
https://github.com/danog/psalm.git
synced 2025-01-05 12:38:35 +01:00
339 lines
12 KiB
PHP
339 lines
12 KiB
PHP
<?php
|
|
namespace Psalm\Internal\Analyzer\Statements\Expression\Call;
|
|
|
|
use PhpParser;
|
|
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
|
|
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
|
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
|
|
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
|
use Psalm\CodeLocation;
|
|
use Psalm\Context;
|
|
use Psalm\Internal\MethodIdentifier;
|
|
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
|
|
use Psalm\Issue\NonStaticSelfCall;
|
|
use Psalm\Issue\ParentNotFound;
|
|
use Psalm\IssueBuffer;
|
|
use Psalm\Type;
|
|
use Psalm\Type\Atomic\TNamedObject;
|
|
use function count;
|
|
use function in_array;
|
|
use function strtolower;
|
|
use Psalm\Internal\DataFlow\TaintSource;
|
|
use Psalm\Internal\DataFlow\DataFlowNode;
|
|
use Psalm\Internal\Codebase\TaintFlowGraph;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
class StaticCallAnalyzer extends CallAnalyzer
|
|
{
|
|
public static function analyze(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
PhpParser\Node\Expr\StaticCall $stmt,
|
|
Context $context
|
|
) : bool {
|
|
$method_id = null;
|
|
|
|
$lhs_type = null;
|
|
|
|
$codebase = $statements_analyzer->getCodebase();
|
|
$source = $statements_analyzer->getSource();
|
|
|
|
$config = $codebase->config;
|
|
|
|
if ($stmt->class instanceof PhpParser\Node\Name) {
|
|
$fq_class_name = null;
|
|
|
|
if (count($stmt->class->parts) === 1
|
|
&& in_array(strtolower($stmt->class->parts[0]), ['self', 'static', 'parent'], true)
|
|
) {
|
|
if ($stmt->class->parts[0] === 'parent') {
|
|
$child_fq_class_name = $context->self;
|
|
|
|
$class_storage = $child_fq_class_name
|
|
? $codebase->classlike_storage_provider->get($child_fq_class_name)
|
|
: null;
|
|
|
|
if (!$class_storage || !$class_storage->parent_class) {
|
|
if (IssueBuffer::accepts(
|
|
new ParentNotFound(
|
|
'Cannot call method on parent as this class does not extend another',
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt)
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
$fq_class_name = $class_storage->parent_class;
|
|
|
|
$fq_class_name = $codebase->classlikes->getUnAliasedName($fq_class_name);
|
|
|
|
$class_storage = $codebase->classlike_storage_provider->get($fq_class_name);
|
|
|
|
$fq_class_name = $class_storage->name;
|
|
} elseif ($context->self) {
|
|
if ($stmt->class->parts[0] === 'static' && isset($context->vars_in_scope['$this'])) {
|
|
$fq_class_name = (string) $context->vars_in_scope['$this'];
|
|
$lhs_type = clone $context->vars_in_scope['$this'];
|
|
} else {
|
|
$fq_class_name = $context->self;
|
|
}
|
|
} else {
|
|
if (IssueBuffer::accepts(
|
|
new NonStaticSelfCall(
|
|
'Cannot use ' . $stmt->class->parts[0] . ' outside class context',
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt)
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if ($context->isPhantomClass($fq_class_name)) {
|
|
return true;
|
|
}
|
|
} elseif ($context->check_classes) {
|
|
$aliases = $statements_analyzer->getAliases();
|
|
|
|
if ($context->calling_method_id
|
|
&& !$stmt->class instanceof PhpParser\Node\Name\FullyQualified
|
|
) {
|
|
$codebase->file_reference_provider->addMethodReferenceToClassMember(
|
|
$context->calling_method_id,
|
|
'use:' . $stmt->class->parts[0] . ':' . \md5($statements_analyzer->getFilePath())
|
|
);
|
|
}
|
|
|
|
$fq_class_name = ClassLikeAnalyzer::getFQCLNFromNameObject(
|
|
$stmt->class,
|
|
$aliases
|
|
);
|
|
|
|
if ($context->isPhantomClass($fq_class_name)) {
|
|
return true;
|
|
}
|
|
|
|
$does_class_exist = false;
|
|
|
|
if ($context->self) {
|
|
$self_storage = $codebase->classlike_storage_provider->get($context->self);
|
|
|
|
if (isset($self_storage->used_traits[strtolower($fq_class_name)])) {
|
|
$fq_class_name = $context->self;
|
|
$does_class_exist = true;
|
|
}
|
|
}
|
|
|
|
if (!isset($context->phantom_classes[strtolower($fq_class_name)])
|
|
&& !$does_class_exist
|
|
) {
|
|
$does_class_exist = ClassLikeAnalyzer::checkFullyQualifiedClassLikeName(
|
|
$statements_analyzer,
|
|
$fq_class_name,
|
|
new CodeLocation($source, $stmt->class),
|
|
!$context->collect_initializations
|
|
&& !$context->collect_mutations
|
|
? $context->self
|
|
: null,
|
|
!$context->collect_initializations
|
|
&& !$context->collect_mutations
|
|
? $context->calling_method_id
|
|
: null,
|
|
$statements_analyzer->getSuppressedIssues(),
|
|
false,
|
|
false,
|
|
false
|
|
);
|
|
}
|
|
|
|
if (!$does_class_exist) {
|
|
return $does_class_exist !== false;
|
|
}
|
|
}
|
|
|
|
if ($codebase->store_node_types
|
|
&& $fq_class_name
|
|
&& !$context->collect_initializations
|
|
&& !$context->collect_mutations
|
|
) {
|
|
$codebase->analyzer->addNodeReference(
|
|
$statements_analyzer->getFilePath(),
|
|
$stmt->class,
|
|
$fq_class_name
|
|
);
|
|
}
|
|
|
|
if ($fq_class_name && !$lhs_type) {
|
|
$lhs_type = new Type\Union([new TNamedObject($fq_class_name)]);
|
|
}
|
|
} else {
|
|
$was_inside_use = $context->inside_use;
|
|
$context->inside_use = true;
|
|
ExpressionAnalyzer::analyze($statements_analyzer, $stmt->class, $context);
|
|
$context->inside_use = $was_inside_use;
|
|
$lhs_type = $statements_analyzer->node_data->getType($stmt->class) ?: Type::getMixed();
|
|
}
|
|
|
|
if (!$lhs_type) {
|
|
if (ArgumentsAnalyzer::analyze(
|
|
$statements_analyzer,
|
|
$stmt->args,
|
|
null,
|
|
null,
|
|
true,
|
|
$context
|
|
) === false) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
$has_mock = false;
|
|
$moved_call = false;
|
|
$has_existing_method = false;
|
|
|
|
foreach ($lhs_type->getAtomicTypes() as $lhs_type_part) {
|
|
StaticMethod\AtomicStaticCallAnalyzer::analyze(
|
|
$statements_analyzer,
|
|
$stmt,
|
|
$context,
|
|
$lhs_type_part,
|
|
$lhs_type->ignore_nullable_issues,
|
|
$moved_call,
|
|
$has_mock,
|
|
$has_existing_method
|
|
);
|
|
}
|
|
|
|
if (!$has_existing_method) {
|
|
return self::checkMethodArgs(
|
|
$method_id,
|
|
$stmt->args,
|
|
null,
|
|
$context,
|
|
new CodeLocation($statements_analyzer->getSource(), $stmt),
|
|
$statements_analyzer
|
|
);
|
|
}
|
|
|
|
if (!$config->remember_property_assignments_after_call && !$context->collect_initializations) {
|
|
$context->removeAllObjectVars();
|
|
}
|
|
|
|
if (!$statements_analyzer->node_data->getType($stmt)) {
|
|
$statements_analyzer->node_data->setType($stmt, Type::getMixed());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static function taintReturnType(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
PhpParser\Node\Expr\StaticCall $stmt,
|
|
MethodIdentifier $method_id,
|
|
string $cased_method_id,
|
|
Type\Union $return_type_candidate,
|
|
?\Psalm\Storage\MethodStorage $method_storage,
|
|
?\Psalm\Internal\Type\TemplateResult $template_result
|
|
) : void {
|
|
if (!$statements_analyzer->data_flow_graph instanceof TaintFlowGraph
|
|
|| \in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())
|
|
) {
|
|
return;
|
|
}
|
|
|
|
$code_location = new CodeLocation($statements_analyzer->getSource(), $stmt);
|
|
|
|
$method_location = $method_storage
|
|
? ($method_storage->signature_return_type_location ?: $method_storage->location)
|
|
: null;
|
|
|
|
if ($method_storage && $method_storage->specialize_call) {
|
|
$method_source = DataFlowNode::getForMethodReturn(
|
|
(string) $method_id,
|
|
$cased_method_id,
|
|
$method_location,
|
|
$code_location
|
|
);
|
|
} else {
|
|
$method_source = DataFlowNode::getForMethodReturn(
|
|
(string) $method_id,
|
|
$cased_method_id,
|
|
$method_location
|
|
);
|
|
}
|
|
|
|
$statements_analyzer->data_flow_graph->addNode($method_source);
|
|
|
|
$codebase = $statements_analyzer->getCodebase();
|
|
|
|
$conditionally_removed_taints = [];
|
|
|
|
if ($method_storage && $template_result) {
|
|
foreach ($method_storage->conditionally_removed_taints as $conditionally_removed_taint) {
|
|
$conditionally_removed_taint = clone $conditionally_removed_taint;
|
|
|
|
TemplateInferredTypeReplacer::replace(
|
|
$conditionally_removed_taint,
|
|
$template_result,
|
|
$codebase
|
|
);
|
|
|
|
$expanded_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
|
|
$statements_analyzer->getCodebase(),
|
|
$conditionally_removed_taint,
|
|
null,
|
|
null,
|
|
null,
|
|
true,
|
|
true
|
|
);
|
|
|
|
foreach ($expanded_type->getLiteralStrings() as $literal_string) {
|
|
$conditionally_removed_taints[] = $literal_string->value;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($conditionally_removed_taints && $method_location) {
|
|
$assignment_node = DataFlowNode::getForAssignment(
|
|
$method_id . '-escaped',
|
|
$method_location,
|
|
$method_source->specialization_key
|
|
);
|
|
|
|
$statements_analyzer->data_flow_graph->addPath(
|
|
$method_source,
|
|
$assignment_node,
|
|
'conditionally-escaped',
|
|
[],
|
|
$conditionally_removed_taints
|
|
);
|
|
|
|
$return_type_candidate->parent_nodes[$assignment_node->id] = $assignment_node;
|
|
} else {
|
|
$return_type_candidate->parent_nodes = [$method_source->id => $method_source];
|
|
}
|
|
|
|
if ($method_storage && $method_storage->taint_source_types) {
|
|
$method_node = TaintSource::getForMethodReturn(
|
|
(string) $method_id,
|
|
$cased_method_id,
|
|
$method_storage->signature_return_type_location ?: $method_storage->location
|
|
);
|
|
|
|
$method_node->taints = $method_storage->taint_source_types;
|
|
|
|
$statements_analyzer->data_flow_graph->addSource($method_node);
|
|
}
|
|
}
|
|
}
|