mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 05:41:20 +01:00
Allow immutable classes to be specialised through calls
This commit is contained in:
parent
106ab936f9
commit
005f394d8e
@ -242,6 +242,16 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer
|
||||
|
||||
$context->vars_in_scope['$this'] = new Type\Union([$this_object_type]);
|
||||
|
||||
if ($codebase->taint_flow_graph
|
||||
&& $storage->specialize_call
|
||||
&& $storage->location
|
||||
) {
|
||||
$new_parent_node = DataFlowNode::getForAssignment('$this in ' . $method_id, $storage->location);
|
||||
|
||||
$codebase->taint_flow_graph->addNode($new_parent_node);
|
||||
$context->vars_in_scope['$this']->parent_nodes += [$new_parent_node->id => $new_parent_node];
|
||||
}
|
||||
|
||||
if ($storage->external_mutation_free
|
||||
&& !$storage->mutation_free_inferred
|
||||
) {
|
||||
|
@ -901,24 +901,14 @@ class AssignmentAnalyzer
|
||||
) {
|
||||
$context->vars_in_scope[$list_var_id]->parent_nodes = [];
|
||||
} else {
|
||||
$new_parent_node = DataFlowNode::getForAssignment($list_var_id, $var_location);
|
||||
|
||||
$statements_analyzer->data_flow_graph->addNode($new_parent_node);
|
||||
|
||||
foreach ($context->vars_in_scope[$list_var_id]->parent_nodes as $parent_node) {
|
||||
$data_flow_graph->addPath(
|
||||
$parent_node,
|
||||
$new_parent_node,
|
||||
'=',
|
||||
[],
|
||||
self::taintAssignment(
|
||||
$context->vars_in_scope[$list_var_id],
|
||||
$data_flow_graph,
|
||||
$list_var_id,
|
||||
$var_location,
|
||||
$removed_taints
|
||||
);
|
||||
}
|
||||
|
||||
$context->vars_in_scope[$list_var_id]->parent_nodes = [
|
||||
$new_parent_node->id => $new_parent_node
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1101,17 +1091,13 @@ class AssignmentAnalyzer
|
||||
} else {
|
||||
$var_location = new CodeLocation($statements_analyzer->getSource(), $assign_var);
|
||||
|
||||
$new_parent_node = DataFlowNode::getForAssignment($var_id, $var_location);
|
||||
|
||||
$data_flow_graph->addNode($new_parent_node);
|
||||
|
||||
foreach ($context->vars_in_scope[$var_id]->parent_nodes as $parent_node) {
|
||||
$data_flow_graph->addPath($parent_node, $new_parent_node, '=', [], $removed_taints);
|
||||
}
|
||||
|
||||
$context->vars_in_scope[$var_id]->parent_nodes = [
|
||||
$new_parent_node->id => $new_parent_node
|
||||
];
|
||||
self::taintAssignment(
|
||||
$context->vars_in_scope[$var_id],
|
||||
$data_flow_graph,
|
||||
$var_id,
|
||||
$var_location,
|
||||
$removed_taints
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1234,6 +1220,68 @@ class AssignmentAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string> $removed_taints
|
||||
*/
|
||||
private static function taintAssignment(
|
||||
Type\Union $type,
|
||||
\Psalm\Internal\Codebase\DataFlowGraph $data_flow_graph,
|
||||
string $var_id,
|
||||
CodeLocation $var_location,
|
||||
array $removed_taints
|
||||
) : void {
|
||||
$parent_nodes = $type->parent_nodes;
|
||||
|
||||
$unspecialized_parent_nodes = \array_filter(
|
||||
$parent_nodes,
|
||||
function ($parent_node) {
|
||||
return !$parent_node->specialization_key;
|
||||
}
|
||||
);
|
||||
|
||||
$specialized_parent_nodes = \array_filter(
|
||||
$parent_nodes,
|
||||
function ($parent_node) {
|
||||
return (bool) $parent_node->specialization_key;
|
||||
}
|
||||
);
|
||||
|
||||
$new_parent_nodes = [];
|
||||
|
||||
foreach ($specialized_parent_nodes as $parent_node) {
|
||||
$new_parent_node = DataFlowNode::getForAssignment($var_id, $var_location);
|
||||
$new_parent_node->specialization_key = $parent_node->specialization_key;
|
||||
|
||||
$data_flow_graph->addNode($new_parent_node);
|
||||
$new_parent_nodes += [$new_parent_node->id => $new_parent_node];
|
||||
$data_flow_graph->addPath(
|
||||
$parent_node,
|
||||
$new_parent_node,
|
||||
'=',
|
||||
[],
|
||||
$removed_taints
|
||||
);
|
||||
}
|
||||
|
||||
if ($unspecialized_parent_nodes) {
|
||||
$new_parent_node = DataFlowNode::getForAssignment($var_id, $var_location);
|
||||
$data_flow_graph->addNode($new_parent_node);
|
||||
$new_parent_nodes += [$new_parent_node->id => $new_parent_node];
|
||||
|
||||
foreach ($unspecialized_parent_nodes as $parent_node) {
|
||||
$data_flow_graph->addPath(
|
||||
$parent_node,
|
||||
$new_parent_node,
|
||||
'=',
|
||||
[],
|
||||
$removed_taints
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$type->parent_nodes = $new_parent_nodes;
|
||||
}
|
||||
|
||||
public static function analyzeAssignmentOperation(
|
||||
StatementsAnalyzer $statements_analyzer,
|
||||
PhpParser\Node\Expr\AssignOp $stmt,
|
||||
|
@ -239,11 +239,138 @@ class MethodCallReturnTypeFetcher
|
||||
|
||||
$is_declaring = (string) $declaring_method_id === (string) $method_id;
|
||||
|
||||
if ($method_storage->specialize_call) {
|
||||
$var_id = ExpressionIdentifier::getArrayVarId(
|
||||
$var_expr,
|
||||
null,
|
||||
$statements_analyzer
|
||||
);
|
||||
|
||||
if ($var_id && isset($context->vars_in_scope[$var_id])) {
|
||||
$var_nodes = [];
|
||||
|
||||
$parent_nodes = $context->vars_in_scope[$var_id]->parent_nodes;
|
||||
|
||||
$unspecialized_parent_nodes = \array_filter(
|
||||
$parent_nodes,
|
||||
function ($parent_node) {
|
||||
return !$parent_node->specialization_key;
|
||||
}
|
||||
);
|
||||
|
||||
$specialized_parent_nodes = \array_filter(
|
||||
$parent_nodes,
|
||||
function ($parent_node) {
|
||||
return (bool) $parent_node->specialization_key;
|
||||
}
|
||||
);
|
||||
|
||||
$var_node = DataFlowNode::getForAssignment(
|
||||
$var_id,
|
||||
new CodeLocation($statements_analyzer, $var_expr)
|
||||
);
|
||||
|
||||
if ($method_storage->location) {
|
||||
$this_parent_node = DataFlowNode::getForAssignment(
|
||||
'$this in ' . $method_id,
|
||||
$method_storage->location
|
||||
);
|
||||
|
||||
foreach ($parent_nodes as $parent_node) {
|
||||
$statements_analyzer->data_flow_graph->addPath($parent_node, $this_parent_node, '=');
|
||||
}
|
||||
}
|
||||
|
||||
$var_nodes[$var_node->id] = $var_node;
|
||||
|
||||
$method_call_nodes = [];
|
||||
|
||||
if ($unspecialized_parent_nodes) {
|
||||
$method_call_node = DataFlowNode::getForMethodReturn(
|
||||
(string) $method_id,
|
||||
$cased_method_id,
|
||||
$is_declaring ? ($method_storage->signature_return_type_location ?: $method_storage->location) : null,
|
||||
$method_storage->specialize_call ? $node_location : null
|
||||
$is_declaring ? ($method_storage->signature_return_type_location
|
||||
?: $method_storage->location) : null,
|
||||
$node_location
|
||||
);
|
||||
|
||||
$method_call_nodes[$method_call_node->id] = $method_call_node;
|
||||
}
|
||||
|
||||
foreach ($specialized_parent_nodes as $parent_node) {
|
||||
$universal_method_call_node = DataFlowNode::getForMethodReturn(
|
||||
(string) $method_id,
|
||||
$cased_method_id,
|
||||
$is_declaring ? ($method_storage->signature_return_type_location
|
||||
?: $method_storage->location) : null,
|
||||
null
|
||||
);
|
||||
|
||||
$method_call_node = new DataFlowNode(
|
||||
strtolower((string) $method_id),
|
||||
$cased_method_id,
|
||||
$is_declaring ? ($method_storage->signature_return_type_location
|
||||
?: $method_storage->location) : null,
|
||||
$parent_node->specialization_key
|
||||
);
|
||||
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$universal_method_call_node,
|
||||
$method_call_node,
|
||||
'='
|
||||
);
|
||||
|
||||
$method_call_nodes[$method_call_node->id] = $method_call_node;
|
||||
}
|
||||
|
||||
foreach ($method_call_nodes as $method_call_node) {
|
||||
$statements_analyzer->data_flow_graph->addNode($method_call_node);
|
||||
|
||||
foreach ($var_nodes as $var_node) {
|
||||
$statements_analyzer->data_flow_graph->addNode($var_node);
|
||||
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$method_call_node,
|
||||
$var_node,
|
||||
'method-call-' . $method_id->method_name
|
||||
);
|
||||
}
|
||||
|
||||
if (!$is_declaring) {
|
||||
$cased_declaring_method_id = $codebase->methods->getCasedMethodId($declaring_method_id);
|
||||
|
||||
$declaring_method_call_node = new DataFlowNode(
|
||||
strtolower((string) $declaring_method_id),
|
||||
$cased_declaring_method_id,
|
||||
$method_storage->signature_return_type_location ?: $method_storage->location,
|
||||
$method_call_node->specialization_key
|
||||
);
|
||||
|
||||
$statements_analyzer->data_flow_graph->addNode($declaring_method_call_node);
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$declaring_method_call_node,
|
||||
$method_call_node,
|
||||
'parent'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$return_type_candidate->parent_nodes = $method_call_nodes;
|
||||
|
||||
$stmt_var_type = clone $context->vars_in_scope[$var_id];
|
||||
|
||||
$stmt_var_type->parent_nodes = $var_nodes;
|
||||
|
||||
$context->vars_in_scope[$var_id] = $stmt_var_type;
|
||||
}
|
||||
} else {
|
||||
$method_call_node = DataFlowNode::getForMethodReturn(
|
||||
(string) $method_id,
|
||||
$cased_method_id,
|
||||
$is_declaring
|
||||
? ($method_storage->signature_return_type_location ?: $method_storage->location)
|
||||
: null,
|
||||
null
|
||||
);
|
||||
|
||||
if (!$is_declaring) {
|
||||
@ -253,7 +380,7 @@ class MethodCallReturnTypeFetcher
|
||||
(string) $declaring_method_id,
|
||||
$cased_declaring_method_id,
|
||||
$method_storage->signature_return_type_location ?: $method_storage->location,
|
||||
$method_storage->specialize_call ? $node_location : null
|
||||
null
|
||||
);
|
||||
|
||||
$statements_analyzer->data_flow_graph->addNode($declaring_method_call_node);
|
||||
@ -269,40 +396,6 @@ class MethodCallReturnTypeFetcher
|
||||
$return_type_candidate->parent_nodes = [
|
||||
$method_call_node->id => $method_call_node
|
||||
];
|
||||
|
||||
if ($method_storage->specialize_call) {
|
||||
$var_id = ExpressionIdentifier::getArrayVarId(
|
||||
$var_expr,
|
||||
null,
|
||||
$statements_analyzer
|
||||
);
|
||||
|
||||
if ($var_id && isset($context->vars_in_scope[$var_id])) {
|
||||
$var_node = DataFlowNode::getForAssignment(
|
||||
$var_id,
|
||||
new CodeLocation($statements_analyzer, $var_expr)
|
||||
);
|
||||
|
||||
$statements_analyzer->data_flow_graph->addNode($var_node);
|
||||
|
||||
$statements_analyzer->data_flow_graph->addPath(
|
||||
$method_call_node,
|
||||
$var_node,
|
||||
'method-call-' . $method_id->method_name
|
||||
);
|
||||
|
||||
$stmt_var_type = clone $context->vars_in_scope[$var_id];
|
||||
|
||||
if ($context->vars_in_scope[$var_id]->parent_nodes) {
|
||||
foreach ($context->vars_in_scope[$var_id]->parent_nodes as $parent_node) {
|
||||
$statements_analyzer->data_flow_graph->addPath($parent_node, $var_node, '=');
|
||||
}
|
||||
}
|
||||
|
||||
$stmt_var_type->parent_nodes = [$var_node->id => $var_node];
|
||||
|
||||
$context->vars_in_scope[$var_id] = $stmt_var_type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($method_storage->taint_source_types) {
|
||||
|
@ -95,14 +95,15 @@ class DataFlowNode
|
||||
*/
|
||||
final public static function getForAssignment(
|
||||
string $var_id,
|
||||
CodeLocation $assignment_location
|
||||
CodeLocation $assignment_location,
|
||||
?string $specialization_key = null
|
||||
): self {
|
||||
$id = $var_id
|
||||
. '-' . $assignment_location->file_name
|
||||
. ':' . $assignment_location->raw_file_start
|
||||
. '-' . $assignment_location->raw_file_end;
|
||||
|
||||
return new static($id, $var_id, $assignment_location, null);
|
||||
return new static($id, $var_id, $assignment_location, $specialization_key);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -383,27 +383,27 @@ class TaintTest extends TestCase
|
||||
UserUpdater::doDelete(new PDO(), $userObj);'
|
||||
],
|
||||
'taintPropertyWithoutPassingObject' => [
|
||||
'<?phps
|
||||
/** @psalm-immutable */
|
||||
'<?php
|
||||
/** @psalm-taint-specialize */
|
||||
class User {
|
||||
public string $id;
|
||||
|
||||
public function __construct(string $userId) {
|
||||
$this->id = $userId;
|
||||
}
|
||||
}
|
||||
|
||||
class UserUpdater {
|
||||
public static function doDelete(PDO $pdo, User $user) : void {
|
||||
self::deleteUser($pdo, $user->id);
|
||||
}
|
||||
|
||||
public static function deleteUser(PDO $pdo, string $userId) : void {
|
||||
$pdo->exec("delete from users where user_id = " . $userId);
|
||||
public function setId(string $userId) : void {
|
||||
$this->id = $userId;
|
||||
}
|
||||
}
|
||||
|
||||
$userObj = new User((string) $_GET["user_id"]);',
|
||||
function echoId(User $u2) : void {
|
||||
echo $u2->id;
|
||||
}
|
||||
|
||||
$u = new User("5");
|
||||
echoId($u);
|
||||
$u->setId($_GET["user_id"]);',
|
||||
],
|
||||
'specializeStaticMethod' => [
|
||||
'<?php
|
||||
@ -523,6 +523,27 @@ class TaintTest extends TestCase
|
||||
$b = new B($_GET["bar"]);
|
||||
echo $b->getTaint();'
|
||||
],
|
||||
'immutableClassTrackInputThroughMethod' => [
|
||||
'<?php
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class A {
|
||||
private string $taint = "";
|
||||
|
||||
public function __construct(string $taint) {
|
||||
$this->taint = $taint;
|
||||
}
|
||||
|
||||
public function getTaint() : string {
|
||||
return $this->taint;
|
||||
}
|
||||
}
|
||||
|
||||
$b = new A($_GET["bar"]);
|
||||
$a = new A("bar");
|
||||
echo $a->getTaint();',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -1236,20 +1257,14 @@ class TaintTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
class UserUpdater {
|
||||
public static function doDelete(PDO $pdo, User $user) : void {
|
||||
self::deleteUser($pdo, $user->id);
|
||||
function echoId(User $u2) : void {
|
||||
echo $u2->id;
|
||||
}
|
||||
|
||||
public static function deleteUser(PDO $pdo, string $userId) : void {
|
||||
$pdo->exec("delete from users where user_id = " . $userId);
|
||||
}
|
||||
}
|
||||
|
||||
$userObj = new User("5");
|
||||
$userObj->setId((string) $_GET["user_id"]);
|
||||
UserUpdater::doDelete(new PDO(), $userObj);',
|
||||
'error_message' => 'TaintedSql',
|
||||
$u = new User("5");
|
||||
$u->setId($_GET["user_id"]);
|
||||
echoId($u);',
|
||||
'error_message' => 'TaintedHtml',
|
||||
],
|
||||
'ImplodeExplode' => [
|
||||
'<?php
|
||||
@ -1524,7 +1539,7 @@ class TaintTest extends TestCase
|
||||
echo $a->isUnsafe();',
|
||||
'error_message' => 'TaintedHtml',
|
||||
],
|
||||
'taintSpecializedInstanceProperty' => [
|
||||
'doTaintSpecializedInstanceProperty' => [
|
||||
'<?php
|
||||
/** @psalm-taint-specialize */
|
||||
class StringHolder {
|
||||
@ -1802,6 +1817,27 @@ class TaintTest extends TestCase
|
||||
echo $b->getTaint();',
|
||||
'error_message' => 'TaintedHtml',
|
||||
],
|
||||
'immutableClassTrackInputThroughMethod' => [
|
||||
'<?php
|
||||
/**
|
||||
* @psalm-immutable
|
||||
*/
|
||||
class A {
|
||||
private string $taint = "";
|
||||
|
||||
public function __construct(string $taint) {
|
||||
$this->taint = $taint;
|
||||
}
|
||||
|
||||
public function getTaint() : string {
|
||||
return $this->taint;
|
||||
}
|
||||
}
|
||||
|
||||
$a = new A($_GET["bar"]);
|
||||
echo $a->getTaint();',
|
||||
'error_message' => 'TaintedHtml',
|
||||
],
|
||||
/*
|
||||
// TODO: Stubs do not support this type of inference even with $this->message = $message.
|
||||
// Most uses of getMessage() would be with caught exceptions, so this is not representative of real code.
|
||||
|
Loading…
x
Reference in New Issue
Block a user