From 2a2ac99da47787269cc686f91a5eb5a567c69e27 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 26 Feb 2025 20:54:42 +0100 Subject: [PATCH 01/42] Refactor taints to use bitmaps --- dictionaries/InternalTaintSinkMap.php | 106 +++++++++--------- examples/plugins/SafeArrayKeyChecker.php | 11 +- src/Psalm/Codebase.php | 9 +- .../Internal/Analyzer/CommentAnalyzer.php | 7 +- .../Analyzer/Statements/EchoAnalyzer.php | 10 +- .../Statements/Expression/ArrayAnalyzer.php | 9 +- .../InstancePropertyAssignmentAnalyzer.php | 9 +- .../Expression/AssignmentAnalyzer.php | 42 +++---- .../Expression/BinaryOpAnalyzer.php | 5 +- .../Expression/Call/ArgumentAnalyzer.php | 5 +- .../Expression/Call/FunctionCallAnalyzer.php | 7 +- .../Call/FunctionCallReturnTypeFetcher.php | 45 ++++---- .../Expression/Call/NewAnalyzer.php | 7 +- .../Expression/Call/StaticCallAnalyzer.php | 14 +-- .../Expression/EncapsulatedStringAnalyzer.php | 5 +- .../Statements/Expression/EvalAnalyzer.php | 7 +- .../Statements/Expression/ExitAnalyzer.php | 10 +- .../Expression/Fetch/ArrayFetchAnalyzer.php | 9 +- .../Fetch/AtomicPropertyFetchAnalyzer.php | 24 ++-- .../Fetch/StaticPropertyFetchAnalyzer.php | 8 +- .../Fetch/VariableFetchAnalyzer.php | 11 +- .../Statements/Expression/IncludeAnalyzer.php | 7 +- .../Statements/Expression/PrintAnalyzer.php | 11 +- src/Psalm/Internal/Codebase/DataFlowGraph.php | 9 +- .../Codebase/InternalCallMapHandler.php | 4 +- .../Internal/Codebase/TaintFlowGraph.php | 31 ++--- .../Internal/Codebase/VariableUseGraph.php | 8 +- src/Psalm/Internal/DataFlow/DataFlowNode.php | 5 +- src/Psalm/Internal/DataFlow/Path.php | 9 +- src/Psalm/Internal/EventDispatcher.php | 17 +-- .../Reflector/FunctionLikeDocblockParser.php | 23 +--- .../Reflector/FunctionLikeDocblockScanner.php | 23 +--- .../AddRemoveTaints/HtmlFunctionTainter.php | 36 +++--- .../Scanner/FunctionDocblockComment.php | 6 +- .../Internal/Scanner/VarDocblockComment.php | 5 +- .../EventHandler/AddTaintsInterface.php | 5 +- .../EventHandler/RemoveTaintsInterface.php | 5 +- src/Psalm/Storage/FunctionLikeStorage.php | 13 ++- src/Psalm/Type/TaintKind.php | 39 ++++--- src/Psalm/Type/TaintKindGroup.php | 61 +++++++--- 40 files changed, 328 insertions(+), 349 deletions(-) diff --git a/dictionaries/InternalTaintSinkMap.php b/dictionaries/InternalTaintSinkMap.php index 574312991a3..f4bc8fc50f3 100644 --- a/dictionaries/InternalTaintSinkMap.php +++ b/dictionaries/InternalTaintSinkMap.php @@ -5,59 +5,59 @@ // This maps internal function names to sink types that we don’t want to end up there /** - * @var non-empty-array>> + * @var non-empty-array>> */ return [ -'exec' => [['shell']], -'create_function' => [[], ['eval']], -'file_get_contents' => [['file']], -'file_put_contents' => [['file']], -'fopen' => [['file']], -'unlink' => [['file']], -'copy' => [['file'], ['file']], -'file' => [['file']], -'link' => [['file'], ['file']], -'mkdir' => [['file']], -'move_uploaded_file' => [['file'], ['file']], -'parse_ini_file' => [['file']], -'chown' => [['file']], -'lchown' => [['file']], -'readfile' => [['file']], -'rename' => [['file'], ['file']], -'rmdir' => [['file']], -'header' => [['header']], -'symlink' => [['file']], -'tempnam' => [['file']], -'igbinary_unserialize' => [['unserialize']], -'ldap_search' => [[], ['ldap'], ['ldap']], -'mysqli_query' => [[], ['sql']], -'mysqli::query' => [['sql']], -'mysqli_real_query' => [[], ['sql']], -'mysqli::real_query' => [['sql']], -'mysqli_multi_query' => [[], ['sql']], -'mysqli::multi_query' => [['sql']], -'mysqli_prepare' => [[], ['sql']], -'mysqli::prepare' => [['sql']], -'mysqli_stmt::__construct' => [[], ['sql']], -'mysqli_stmt_prepare' => [[], ['sql']], -'mysqli_stmt::prepare' => [['sql']], -'passthru' => [['shell']], -'pcntl_exec' => [['shell']], -'pg_exec' => [[], ['sql']], -'pg_prepare' => [[], [], ['sql']], -'pg_put_line' => [[], ['sql']], -'pg_query' => [[], ['sql']], -'pg_query_params' => [[], ['sql']], -'pg_send_prepare' => [[], [], ['sql']], -'pg_send_query' => [[], ['sql']], -'pg_send_query_params' => [[], ['sql'], []], -'setcookie' => [['cookie'], ['cookie']], -'shell_exec' => [['shell']], -'system' => [['shell']], -'unserialize' => [['unserialize']], -'popen' => [['shell']], -'proc_open' => [['shell']], -'curl_init' => [['ssrf']], -'curl_setopt' => [[], [], ['ssrf']], -'getimagesize' => [['ssrf']], +'exec' => [[TaintKind::INPUT_SHELL]], +'create_function' => [[], [TaintKind::INPUT_EVAL]], +'file_get_contents' => [[TaintKind::INPUT_FILE]], +'file_put_contents' => [[TaintKind::INPUT_FILE]], +'fopen' => [[TaintKind::INPUT_FILE]], +'unlink' => [[TaintKind::INPUT_FILE]], +'copy' => [[TaintKind::INPUT_FILE], [TaintKind::INPUT_FILE]], +'file' => [[TaintKind::INPUT_FILE]], +'link' => [[TaintKind::INPUT_FILE], [TaintKind::INPUT_FILE]], +'mkdir' => [[TaintKind::INPUT_FILE]], +'move_uploaded_file' => [[TaintKind::INPUT_FILE], [TaintKind::INPUT_FILE]], +'parse_ini_file' => [[TaintKind::INPUT_FILE]], +'chown' => [[TaintKind::INPUT_FILE]], +'lchown' => [[TaintKind::INPUT_FILE]], +'readfile' => [[TaintKind::INPUT_FILE]], +'rename' => [[TaintKind::INPUT_FILE], [TaintKind::INPUT_FILE]], +'rmdir' => [[TaintKind::INPUT_FILE]], +'header' => [[TaintKind::INPUT_HEADER]], +'symlink' => [[TaintKind::INPUT_FILE]], +'tempnam' => [[TaintKind::INPUT_FILE]], +'igbinary_unserialize' => [[TaintKind::INPUT_UNSERIALIZE]], +'ldap_search' => [[], [TaintKind::INPUT_LDAP], [TaintKind::INPUT_LDAP]], +'mysqli_query' => [[], [TaintKind::INPUT_SQL]], +'mysqli::query' => [[TaintKind::INPUT_SQL]], +'mysqli_real_query' => [[], [TaintKind::INPUT_SQL]], +'mysqli::real_query' => [[TaintKind::INPUT_SQL]], +'mysqli_multi_query' => [[], [TaintKind::INPUT_SQL]], +'mysqli::multi_query' => [[TaintKind::INPUT_SQL]], +'mysqli_prepare' => [[], [TaintKind::INPUT_SQL]], +'mysqli::prepare' => [[TaintKind::INPUT_SQL]], +'mysqli_stmt::__construct' => [[], [TaintKind::INPUT_SQL]], +'mysqli_stmt_prepare' => [[], [TaintKind::INPUT_SQL]], +'mysqli_stmt::prepare' => [[TaintKind::INPUT_SQL]], +'passthru' => [[TaintKind::INPUT_SHELL]], +'pcntl_exec' => [[TaintKind::INPUT_SHELL]], +'pg_exec' => [[], [TaintKind::INPUT_SQL]], +'pg_prepare' => [[], [], [TaintKind::INPUT_SQL]], +'pg_put_line' => [[], [TaintKind::INPUT_SQL]], +'pg_query' => [[], [TaintKind::INPUT_SQL]], +'pg_query_params' => [[], [TaintKind::INPUT_SQL]], +'pg_send_prepare' => [[], [], [TaintKind::INPUT_SQL]], +'pg_send_query' => [[], [TaintKind::INPUT_SQL]], +'pg_send_query_params' => [[], [TaintKind::INPUT_SQL], []], +'setcookie' => [[TaintKind::INPUT_COOKIE], [TaintKind::INPUT_COOKIE]], +'shell_exec' => [[TaintKind::INPUT_SHELL]], +'system' => [[TaintKind::INPUT_SHELL]], +'unserialize' => [[TaintKind::INPUT_UNSERIALIZE]], +'popen' => [[TaintKind::INPUT_SHELL]], +'proc_open' => [[TaintKind::INPUT_SHELL]], +'curl_init' => [[TaintKind::INPUT_SSRF]], +'curl_setopt' => [[], [], [TaintKind::INPUT_SSRF]], +'getimagesize' => [[TaintKind::INPUT_SSRF]], ]; diff --git a/examples/plugins/SafeArrayKeyChecker.php b/examples/plugins/SafeArrayKeyChecker.php index d4304462dde..8713f15a9a1 100644 --- a/examples/plugins/SafeArrayKeyChecker.php +++ b/examples/plugins/SafeArrayKeyChecker.php @@ -6,21 +6,22 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Plugin\EventHandler\RemoveTaintsInterface; +use Psalm\Type\TaintKind; final class SafeArrayKeyChecker implements RemoveTaintsInterface { /** * Called to see what taints should be removed * - * @return list + * @return int-mask-of */ #[\Override] - public static function removeTaints(AddRemoveTaintsEvent $event): array + public static function removeTaints(AddRemoveTaintsEvent $event): int { $item = $event->getExpr(); $statements_analyzer = $event->getStatementsSource(); if (!($item instanceof ArrayItem) || !($statements_analyzer instanceof StatementsAnalyzer)) { - return []; + return 0; } $item_key_value = ''; if ($item->key) { @@ -34,8 +35,8 @@ public static function removeTaints(AddRemoveTaintsEvent $event): array } if ($item_key_value === 'safe_key') { - return ['html']; + return TaintKind::INPUT_HTML; } - return []; + return 0; } } diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 47d0af2bb46..80e584f06cf 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -65,6 +65,7 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\TaintKind; use Psalm\Type\TaintKindGroup; use Psalm\Type\Union; use ReflectionProperty; @@ -2143,12 +2144,12 @@ public function queueClassLikeForScanning( } /** - * @param array $taints + * @param int-mask-of $taints */ public function addTaintSource( Union $expr_type, string $taint_id, - array $taints = TaintKindGroup::ALL_INPUT, + int $taints = TaintKindGroup::ALL_INPUT, ?CodeLocation $code_location = null, ): Union { if (!$this->taint_flow_graph) { @@ -2169,11 +2170,11 @@ public function addTaintSource( } /** - * @param array $taints + * @param int-mask-of $taints */ public function addTaintSink( string $taint_id, - array $taints = TaintKindGroup::ALL_INPUT, + int $taints = TaintKindGroup::ALL_INPUT, ?CodeLocation $code_location = null, ): void { if (!$this->taint_flow_graph) { diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index 572df0f5078..50ac39f5d66 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -24,6 +24,7 @@ use Psalm\Issue\InvalidDocblock; use Psalm\Issue\MissingDocblockType; use Psalm\IssueBuffer; +use Psalm\Type\TaintKindGroup; use Psalm\Type\Union; use UnexpectedValueException; @@ -238,7 +239,11 @@ private static function decorateVarDocblockComment( 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 (!isset(TaintKindGroup::NAME_TO_TAINT[$param])) { + // TODO error out, and add support for conditional taints + continue; + } + $var_comment->removed_taints |= TaintKindGroup::NAME_TO_TAINT[$param]; } } diff --git a/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php index f04d8c131e8..5e97b12c55b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php @@ -65,12 +65,10 @@ public static function analyze( $call_location, ); - $echo_param_sink->taints = [ - TaintKind::INPUT_HTML, - TaintKind::INPUT_HAS_QUOTES, - TaintKind::USER_SECRET, - TaintKind::SYSTEM_SECRET, - ]; + $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); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index 0afd08d1eab..07704fb2cbe 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -43,7 +43,6 @@ use Psalm\Type\Atomic\TTrue; use Psalm\Type\Union; -use function array_diff; use function array_merge; use function array_values; use function count; @@ -443,8 +442,8 @@ private static function analyzeArrayItem( $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - $taints = array_diff($added_taints, $removed_taints); - if ($taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { $taint_source = TaintSource::fromNode($new_parent_node); $statements_analyzer->data_flow_graph->addSource($taint_source); } @@ -484,8 +483,8 @@ private static function analyzeArrayItem( $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - $taints = array_diff($added_taints, $removed_taints); - if ($taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { $taint_source = TaintSource::fromNode($new_parent_node); $taint_source->taints = $taints; $statements_analyzer->data_flow_graph->addSource($taint_source); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php index 0c48e9146d9..9dc38883289 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php @@ -79,7 +79,6 @@ use Psalm\Type\Union; use UnexpectedValueException; -use function array_diff; use function array_merge; use function array_pop; use function count; @@ -507,8 +506,8 @@ private static function taintProperty( $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - $taints = array_diff($added_taints, $removed_taints); - if ($taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { $taint_source = TaintSource::fromNode($property_node); $taint_source->taints = $taints; $statements_analyzer->data_flow_graph->addSource($taint_source); @@ -609,8 +608,8 @@ public static function taintUnspecializedProperty( $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - $taints = array_diff($added_taints, $removed_taints); - if ($taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { $taint_source = TaintSource::fromNode($property_node); $taint_source->taints = $taints; $statements_analyzer->data_flow_graph->addSource($taint_source); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 291d95edea7..25c09b89c8e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -85,10 +85,10 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNull; +use Psalm\Type\TaintKind; use Psalm\Type\Union; use UnexpectedValueException; -use function array_diff; use function count; use function in_array; use function is_string; @@ -152,7 +152,7 @@ public static function analyze( $assign_value_type = $statements_analyzer->node_data->getType($base_assign_value) ?? $assign_value_type; } - $removed_taints = []; + $removed_taints = 0; self::analyzeDocComment( $statements_analyzer, @@ -513,7 +513,7 @@ public static function analyze( /** * @param list $var_comments - * @param list $removed_taints + * @param int-mask-of $removed_taints * @return null|false */ private static function analyzeAssignment( @@ -527,7 +527,7 @@ private static function analyzeAssignment( ?Doc $doc_comment, ?string $extended_var_id, array $var_comments, - array $removed_taints, + int $removed_taints, ): ?bool { if ($assign_var instanceof PhpParser\Node\Expr\Variable) { self::analyzeAssignmentToVariable( @@ -598,7 +598,7 @@ private static function analyzeAssignment( /** * @param list $var_comments - * @param list $removed_taints + * @param int-mask-of $removed_taints */ private static function analyzeDocComment( StatementsAnalyzer $statements_analyzer, @@ -612,7 +612,7 @@ private static function analyzeDocComment( ?Union &$comment_type, ?DocblockTypeLocation &$comment_type_location, array $not_ignored_docblock_var_ids, - array &$removed_taints, + int &$removed_taints, ): void { if (!$doc_comment) { return; @@ -790,16 +790,16 @@ public static function assignTypeFromVarDocblock( } /** - * @param list $removed_taints - * @param list $added_taints + * @param int-mask-of $removed_taints + * @param int-mask-of $added_taints */ private static function taintAssignment( Union &$type, DataFlowGraph $data_flow_graph, string $var_id, CodeLocation $var_location, - array $removed_taints, - array $added_taints, + int $removed_taints, + int $added_taints, ): void { $parent_nodes = $type->parent_nodes; @@ -809,8 +809,8 @@ private static function taintAssignment( // If taints get added (e.g. due to plugin) this assignment needs to // become a new taint source - $taints = array_diff($added_taints, $removed_taints); - if ($taints !== [] && $data_flow_graph instanceof TaintFlowGraph) { + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0 && $data_flow_graph instanceof TaintFlowGraph) { $taint_source = TaintSource::fromNode($new_parent_node); $taint_source->taints = $taints; $data_flow_graph->addSource($taint_source); @@ -1153,7 +1153,7 @@ public static function assignByRefParam( /** * @param PhpParser\Node\Expr\List_|PhpParser\Node\Expr\Array_ $assign_var * @param list $var_comments - * @param list $removed_taints + * @param int-mask-of $removed_taints */ private static function analyzeDestructuringAssignment( StatementsAnalyzer $statements_analyzer, @@ -1165,7 +1165,7 @@ private static function analyzeDestructuringAssignment( ?PhpParser\Comment\Doc $doc_comment, ?string $extended_var_id, array $var_comments, - array $removed_taints, + int $removed_taints, ): void { if (!$assign_value_type->hasArray() && !$assign_value_type->isMixed() @@ -1555,10 +1555,7 @@ private static function analyzeDestructuringAssignment( $event = new AddRemoveTaintsEvent($var, $context, $statements_analyzer, $codebase); $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); - $removed_taints = [ - ...$removed_taints, - ...$codebase->config->eventDispatcher->dispatchRemoveTaints($event), - ]; + $removed_taints |= $codebase->config->eventDispatcher->dispatchRemoveTaints($event); self::taintAssignment( $context->vars_in_scope[$list_var_id], @@ -1873,7 +1870,7 @@ private static function analyzeVariableUse( } /** - * @param list $removed_taints + * @param int-mask-of $removed_taints */ private static function analyzeAssignValueDataFlow( StatementsAnalyzer $statements_analyzer, @@ -1883,7 +1880,7 @@ private static function analyzeAssignValueDataFlow( Union &$assign_value_type, string $var_id, Context $context, - array $removed_taints, + int $removed_taints, ): void { if (!$statements_analyzer->data_flow_graph || !$context->vars_in_scope[$var_id]->parent_nodes) { @@ -1901,10 +1898,7 @@ private static function analyzeAssignValueDataFlow( $event = new AddRemoveTaintsEvent($assign_var, $context, $statements_analyzer, $codebase); $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); - $removed_taints = [ - ...$removed_taints, - ...$codebase->config->eventDispatcher->dispatchRemoveTaints($event), - ]; + $removed_taints |= $codebase->config->eventDispatcher->dispatchRemoveTaints($event); self::taintAssignment( $context->vars_in_scope[$var_id], diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php index abbdd8fff52..7dd7f6fb8d5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php @@ -35,7 +35,6 @@ use Psalm\Type\Union; use UnexpectedValueException; -use function array_diff; use function in_array; use function strlen; @@ -173,8 +172,8 @@ public static function analyze( $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - $taints = array_diff($added_taints, $removed_taints); - if ($taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { $taint_source = TaintSource::fromNode($new_parent_node); $taint_source->taints = $taints; $statements_analyzer->data_flow_graph->addSource($taint_source); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index a252cdf5c21..e7bda48a7d4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -65,7 +65,6 @@ use Psalm\Type\Union; use UnexpectedValueException; -use function array_diff; use function array_filter; use function count; use function explode; @@ -1894,8 +1893,8 @@ private static function processTaintedness( ); } - $taints = array_diff($added_taints, $removed_taints); - if ($taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { $taint_source = TaintSource::fromNode($argument_value_node); $taint_source->taints = $taints; $statements_analyzer->data_flow_graph->addSource($taint_source); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 18db16871fc..c27f5da3d63 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -63,7 +63,6 @@ use Psalm\Type\Union; use UnexpectedValueException; -use function array_diff; use function array_map; use function array_merge; use function array_shift; @@ -846,7 +845,7 @@ private static function getAnalyzeNamedExpression( $arg_location, ); - $custom_call_sink->taints = [TaintKind::INPUT_CALLABLE]; + $custom_call_sink->taints = TaintKind::INPUT_CALLABLE; $statements_analyzer->data_flow_graph->addSink($custom_call_sink); @@ -855,8 +854,8 @@ private static function getAnalyzeNamedExpression( $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - $taints = array_diff($added_taints, $removed_taints); - if ($taints !== []) { + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0) { $taint_source = TaintSource::fromNode($custom_call_sink); $taint_source->taints = $taints; $statements_analyzer->data_flow_graph->addSource($taint_source); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 9aec38e44a9..300fb0ccb0c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -40,11 +40,10 @@ use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TString; use Psalm\Type\TaintKind; +use Psalm\Type\TaintKindGroup; use Psalm\Type\Union; use UnexpectedValueException; -use function array_diff; -use function array_merge; use function array_values; use function count; use function explode; @@ -573,7 +572,7 @@ private static function taintReturnType( $codebase = $statements_analyzer->getCodebase(); - $conditionally_removed_taints = []; + $conditionally_removed_taints = 0; foreach ($function_storage->conditionally_removed_taints as $conditionally_removed_taint) { $conditionally_removed_taint = TemplateInferredTypeReplacer::replace( @@ -594,7 +593,7 @@ private static function taintReturnType( if (!$expanded_type->isNullable()) { foreach ($expanded_type->getLiteralStrings() as $literal_string) { - $conditionally_removed_taints[] = $literal_string->value; + $conditionally_removed_taints |= TaintKindGroup::NAME_TO_TAINT[$literal_string->value]; } } } @@ -611,7 +610,7 @@ private static function taintReturnType( $assignment_node, 'conditionally-escaped', $added_taints, - [...$removed_taints, ...$conditionally_removed_taints], + $removed_taints | $conditionally_removed_taints, ); $stmt_type = $stmt_type->addParentNodes([$assignment_node->id => $assignment_node]); @@ -647,9 +646,9 @@ private static function taintReturnType( $pattern = substr($pattern, 2, -1); if (self::simpleExclusion($pattern, $first_arg_value[0])) { - $removed_taints[] = TaintKind::INPUT_HTML; - $removed_taints[] = TaintKind::INPUT_HAS_QUOTES; - $removed_taints[] = TaintKind::INPUT_SQL; + $removed_taints |= TaintKind::INPUT_HTML; + $removed_taints |= TaintKind::INPUT_HAS_QUOTES; + $removed_taints |= TaintKind::INPUT_SQL; } } } @@ -659,10 +658,7 @@ private static function taintReturnType( $event = new AddRemoveTaintsEvent($stmt, $context, $statements_analyzer, $codebase); $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); - $removed_taints = array_merge( - $removed_taints, - $codebase->config->eventDispatcher->dispatchRemoveTaints($event), - ); + $removed_taints |= $codebase->config->eventDispatcher->dispatchRemoveTaints($event); self::taintUsingFlows( $statements_analyzer, @@ -672,7 +668,7 @@ private static function taintReturnType( $stmt->getArgs(), $node_location, $function_call_node, - array_merge($removed_taints, $conditionally_removed_taints), + $removed_taints | $conditionally_removed_taints, $added_taints, ); } @@ -684,8 +680,8 @@ private static function taintReturnType( /** * @param array $args - * @param array $removed_taints - * @param array $added_taints + * @param int-mask-of $removed_taints + * @param int-mask-of $added_taints */ public static function taintUsingFlows( StatementsAnalyzer $statements_analyzer, @@ -695,8 +691,8 @@ public static function taintUsingFlows( array $args, CodeLocation $node_location, DataFlowNode $function_call_node, - array $removed_taints, - array $added_taints = [], + int $removed_taints, + int $added_taints = 0, ): void { foreach ($function_storage->return_source_params as $i => $path_type) { if (!isset($args[$i])) { @@ -733,7 +729,7 @@ public static function taintUsingFlows( $function_param_sink, $function_call_node, $path_type, - array_merge($added_taints, $function_storage->added_taints), + $added_taints | $function_storage->added_taints, $removed_taints, ); } @@ -746,18 +742,15 @@ public static function taintUsingStorage( DataFlowNode $function_call_node, ): void { // Docblock-defined taints should override inherited - $added_taints = []; - if ($function_storage->taint_source_types !== []) { + $added_taints = 0; + if ($function_storage->taint_source_types !== 0) { $added_taints = $function_storage->taint_source_types; - } elseif ($function_storage->added_taints !== []) { + } elseif ($function_storage->added_taints !== 0) { $added_taints = $function_storage->added_taints; } - $taints = array_diff( - $added_taints, - $function_storage->removed_taints, - ); - if ($taints !== []) { + $taints = $added_taints & ~$function_storage->removed_taints; + if ($taints !== 0) { $taint_source = TaintSource::fromNode($function_call_node); $taint_source->taints = $taints; $graph->addSource($taint_source); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 92edae0c543..e8667a1af4f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -61,7 +61,6 @@ use Psalm\Type\TaintKind; use Psalm\Type\Union; -use function array_diff; use function array_map; use function array_values; use function count; @@ -733,7 +732,7 @@ private static function analyzeConstructorExpression( $arg_location, ); - $custom_call_sink->taints = [TaintKind::INPUT_CALLABLE]; + $custom_call_sink->taints = TaintKind::INPUT_CALLABLE; $statements_analyzer->data_flow_graph->addSink($custom_call_sink); @@ -742,8 +741,8 @@ private static function analyzeConstructorExpression( $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - $taints = array_diff($added_taints, $removed_taints); - if ($added_taints !== []) { + $taints = $added_taints & ~$removed_taints; + if ($added_taints !== 0) { $taint_source = TaintSource::fromNode($custom_call_sink); $taint_source->taints = $taints; $statements_analyzer->data_flow_graph->addSource($taint_source); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index 007886756e3..a25718f8fda 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -27,9 +27,9 @@ use Psalm\Storage\MethodStorage; use Psalm\Type; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\TaintKindGroup; use Psalm\Type\Union; -use function array_merge; use function count; use function in_array; use function md5; @@ -309,7 +309,7 @@ public static function taintReturnType( $codebase = $statements_analyzer->getCodebase(); - $conditionally_removed_taints = []; + $conditionally_removed_taints = 0; if ($method_storage && $template_result) { foreach ($method_storage->conditionally_removed_taints as $conditionally_removed_taint) { @@ -330,13 +330,13 @@ public static function taintReturnType( ); foreach ($expanded_type->getLiteralStrings() as $literal_string) { - $conditionally_removed_taints[] = $literal_string->value; + $conditionally_removed_taints |= TaintKindGroup::NAME_TO_TAINT[$literal_string->value]; } } } - $added_taints = []; - $removed_taints = []; + $added_taints = 0; + $removed_taints = 0; if ($context) { $event = new AddRemoveTaintsEvent($stmt, $context, $statements_analyzer, $codebase); @@ -357,7 +357,7 @@ public static function taintReturnType( $assignment_node, 'conditionally-escaped', $added_taints, - [...$conditionally_removed_taints, ...$removed_taints], + $conditionally_removed_taints | $removed_taints, ); $return_type_candidate = $return_type_candidate->addParentNodes([$assignment_node->id => $assignment_node]); @@ -389,7 +389,7 @@ public static function taintReturnType( $stmt->getArgs(), $node_location, $method_source, - array_merge($method_storage->removed_taints, $removed_taints), + $method_storage->removed_taints | $removed_taints, $added_taints, ); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php index ba5a41f0f44..cc2613fd3b6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php @@ -26,7 +26,6 @@ use Psalm\Type\Atomic\TString; use Psalm\Type\Union; -use function array_diff; use function in_array; /** @@ -109,8 +108,8 @@ public static function analyze( $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - $taints = array_diff($added_taints, $removed_taints); - if ($taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { $taint_source = TaintSource::fromNode($new_parent_node); $taint_source->taints = $taints; $statements_analyzer->data_flow_graph->addSource($taint_source); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php index 421545eec60..a1cb0e65ea3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php @@ -17,7 +17,6 @@ use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Type\TaintKind; -use function array_diff; use function in_array; /** @@ -54,7 +53,7 @@ public static function analyze( $arg_location, ); - $eval_param_sink->taints = [TaintKind::INPUT_EVAL]; + $eval_param_sink->taints = TaintKind::INPUT_EVAL; $statements_analyzer->data_flow_graph->addSink($eval_param_sink); @@ -64,8 +63,8 @@ public static function analyze( $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - $taints = array_diff($added_taints, $removed_taints); - if ($taints !== []) { + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0) { $taint_source = TaintSource::fromNode($eval_param_sink); $taint_source->taints = $taints; $statements_analyzer->data_flow_graph->addSource($taint_source); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php index 17d5827e742..5fd6872e10f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php @@ -77,12 +77,10 @@ public static function analyze( $call_location, ); - $echo_param_sink->taints = [ - TaintKind::INPUT_HTML, - TaintKind::INPUT_HAS_QUOTES, - TaintKind::USER_SECRET, - TaintKind::SYSTEM_SECRET, - ]; + $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); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 4186103eecd..5dde0c1c67c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -89,7 +89,6 @@ use Psalm\Type\Union; use UnexpectedValueException; -use function array_diff; use function array_keys; use function array_map; use function array_pop; @@ -403,8 +402,8 @@ public static function taintArrayFetch( $var_location, ); - $added_taints = []; - $removed_taints = []; + $added_taints = 0; + $removed_taints = 0; if ($context) { $codebase = $statements_analyzer->getCodebase(); @@ -413,8 +412,8 @@ public static function taintArrayFetch( $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - $taints = array_diff($added_taints, $removed_taints); - if ($taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { $taint_source = TaintSource::fromNode($new_parent_node); $taint_source->taints = $taints; $statements_analyzer->data_flow_graph->addSource($taint_source); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index ae51eb7fd9e..f4304a18e6f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -61,9 +61,9 @@ use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\TaintKind; use Psalm\Type\Union; -use function array_diff; use function array_filter; use function array_keys; use function array_map; @@ -826,8 +826,8 @@ public static function processTaints( $data_flow_graph = $statements_analyzer->data_flow_graph; - $added_taints = []; - $removed_taints = []; + $added_taints = 0; + $removed_taints = 0; if ($context) { $codebase = $statements_analyzer->getCodebase(); @@ -901,8 +901,8 @@ public static function processTaints( $type = $type->setParentNodes([$property_node->id => $property_node], true); - $taints = array_diff($added_taints, $removed_taints); - if ($taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { $taint_source = TaintSource::fromNode($var_node); $taint_source->taints = $taints; $statements_analyzer->data_flow_graph->addSource($taint_source); @@ -922,8 +922,8 @@ public static function processTaints( } /** - * @param ?array $added_taints - * @param ?array $removed_taints + * @param ?int-mask-of $added_taints + * @param ?int-mask-of $removed_taints */ public static function processUnspecialTaints( StatementsAnalyzer $statements_analyzer, @@ -931,8 +931,8 @@ public static function processUnspecialTaints( Union &$type, string $property_id, bool $in_assignment, - ?array $added_taints, - ?array $removed_taints, + ?int $added_taints, + ?int $removed_taints, ): void { if (!$statements_analyzer->data_flow_graph) { return; @@ -984,8 +984,10 @@ public static function processUnspecialTaints( $type = $type->setParentNodes([$localized_property_node->id => $localized_property_node], true); - $taints = array_diff($added_taints ?? [], $removed_taints ?? []); - if ($taints !== [] && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { + $added_taints ??= 0; + $removed_taints ??= 0; + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { $taint_source = TaintSource::fromNode($localized_property_node); $taint_source->taints = $taints; $statements_analyzer->data_flow_graph->addSource($taint_source); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php index bc20fc4974e..67bdfc1abdc 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php @@ -220,8 +220,8 @@ public static function analyze( $stmt_type, $property_id, false, - [], - [], + 0, + 0, ); $context->vars_in_scope[$var_id] = $stmt_type; @@ -405,8 +405,8 @@ public static function analyze( $stmt_type, $property_id, false, - [], - [], + 0, + 0, ); $context->vars_in_scope[$var_id] = $stmt_type; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 53a6b6542e9..4c8b4dd6dab 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -37,9 +37,6 @@ use Psalm\Type\TaintKindGroup; use Psalm\Type\Union; -use function array_diff; -use function array_merge; -use function array_unique; use function in_array; use function is_string; use function time; @@ -530,7 +527,7 @@ private static function taintVariable( ) { $taints = TaintKindGroup::ALL_INPUT; } else { - $taints = []; + $taints = 0; } // Trigger event to possibly get more/less taints @@ -539,10 +536,10 @@ private static function taintVariable( $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - $taints = array_unique(array_merge($taints, $added_taints)); - $taints = array_diff($taints, $removed_taints); + $taints |= $added_taints; + $taints &= ~$removed_taints; - if ($taints === []) { + if ($taints === 0) { return; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php index 32d46362a6b..e4ac3c2b2a9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php @@ -25,7 +25,6 @@ use Psalm\Type\TaintKind; use Symfony\Component\Filesystem\Path; -use function array_diff; use function constant; use function defined; use function dirname; @@ -126,7 +125,7 @@ public static function analyze( $arg_location, ); - $include_param_sink->taints = [TaintKind::INPUT_INCLUDE]; + $include_param_sink->taints = TaintKind::INPUT_INCLUDE; $statements_analyzer->data_flow_graph->addSink($include_param_sink); @@ -136,8 +135,8 @@ public static function analyze( $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - $taints = array_diff($added_taints, $removed_taints); - if ($taints !== []) { + $taints = $added_taints & ~$removed_taints; + if ($taints !== 0) { $taint_source = TaintSource::fromNode($include_param_sink); $taint_source->taints = $taints; $statements_analyzer->data_flow_graph->addSource($taint_source); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php index 25f06a23ce7..d49a41a6e23 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php @@ -47,12 +47,11 @@ public static function analyze( $call_location, ); - $print_param_sink->taints = [ - TaintKind::INPUT_HTML, - TaintKind::INPUT_HAS_QUOTES, - TaintKind::USER_SECRET, - TaintKind::SYSTEM_SECRET, - ]; + $print_param_sink->taints = TaintKind::INPUT_HTML + | TaintKind::INPUT_HAS_QUOTES + | TaintKind::USER_SECRET + | TaintKind::SYSTEM_SECRET + ; $statements_analyzer->data_flow_graph->addSink($print_param_sink); } diff --git a/src/Psalm/Internal/Codebase/DataFlowGraph.php b/src/Psalm/Internal/Codebase/DataFlowGraph.php index 6099745b9b8..cc861d57f25 100644 --- a/src/Psalm/Internal/Codebase/DataFlowGraph.php +++ b/src/Psalm/Internal/Codebase/DataFlowGraph.php @@ -6,6 +6,7 @@ use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\DataFlow\Path; +use Psalm\Type\TaintKind; use function abs; use function array_keys; @@ -27,15 +28,15 @@ abstract class DataFlowGraph abstract public function addNode(DataFlowNode $node): void; /** - * @param array $added_taints - * @param array $removed_taints + * @param int-mask-of $added_taints + * @param int-mask-of $removed_taints */ public function addPath( DataFlowNode $from, DataFlowNode $to, string $path_type, - ?array $added_taints = null, - ?array $removed_taints = null, + ?int $added_taints = null, + ?int $removed_taints = null, ): void { $from_id = $from->id; $to_id = $to->id; diff --git a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php index 512c12a71c4..2fd2990b3bc 100644 --- a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php +++ b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php @@ -53,7 +53,7 @@ final class InternalCallMapHandler private static ?array $call_map_callables = []; /** - * @var non-empty-array>>|null + * @var non-empty-array>>|null */ private static ?array $taint_sink_map = null; @@ -367,7 +367,7 @@ public static function getCallMap(): array self::$loaded_php_minor_version = $analyzer_minor_version; /** - * @var non-empty-array>> + * @var non-empty-array>> */ $taint_map_data = require(dirname(__DIR__, 4) . '/dictionaries/InternalTaintSinkMap.php'); diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 64bc7ac67a0..56feb66652d 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -32,12 +32,9 @@ use Psalm\Issue\TaintedXpath; use Psalm\IssueBuffer; use Psalm\Type\TaintKind; +use Psalm\Type\TaintKindGroup; -use function array_diff; use function array_filter; -use function array_intersect; -use function array_merge; -use function array_unique; use function count; use function end; use function implode; @@ -241,13 +238,13 @@ public function connectSinksAndSources(): void } /** - * @param array $source_taints + * @param int-mask-of $source_taints * @param array $sinks * @return array */ private function getChildNodes( DataFlowNode $generated_source, - array $source_taints, + int $source_taints, array $sinks, array $visited_source_ids, ): array { @@ -259,8 +256,8 @@ private function getChildNodes( foreach ($this->forward_edges[$generated_source->id] as $to_id => $path) { $path_type = $path->type; - $added_taints = $path->unescaped_taints ?: []; - $removed_taints = $path->escaped_taints ?: []; + $added_taints = $path->unescaped_taints ?? 0; + $removed_taints = $path->escaped_taints ?? 0; if (!isset($this->nodes[$to_id])) { continue; @@ -268,16 +265,9 @@ private function getChildNodes( $destination_node = $this->nodes[$to_id]; - $new_taints = array_unique( - array_diff( - array_merge($source_taints, $added_taints), - $removed_taints, - ), - ); + $new_taints = ($source_taints | $added_taints) & ~$removed_taints; - sort($new_taints); - - if (isset($visited_source_ids[$to_id][implode(',', $new_taints)])) { + if (isset($visited_source_ids[$to_id][$new_taints])) { continue; } @@ -301,7 +291,7 @@ private function getChildNodes( } if (isset($sinks[$to_id])) { - $matching_taints = array_intersect($sinks[$to_id]->taints, $new_taints); + $matching_taints = $sinks[$to_id]->taints & $new_taints; if ($matching_taints && $generated_source->code_location) { if ($sinks[$to_id]->code_location @@ -316,7 +306,10 @@ private function getChildNodes( $path = $this->getPredecessorPath($generated_source) . ' -> ' . $this->getSuccessorPath($sinks[$to_id]); - foreach ($matching_taints as $matching_taint) { + foreach (TaintKindGroup::TAINT_TO_NAME as $matching_taint) { + if (!($matching_taints & $matching_taint)) { + continue; + } $issue = match ($matching_taint) { TaintKind::INPUT_CALLABLE => new TaintedCallable( 'Detected tainted text', diff --git a/src/Psalm/Internal/Codebase/VariableUseGraph.php b/src/Psalm/Internal/Codebase/VariableUseGraph.php index 83f02c11537..c9cbcb0fc56 100644 --- a/src/Psalm/Internal/Codebase/VariableUseGraph.php +++ b/src/Psalm/Internal/Codebase/VariableUseGraph.php @@ -33,16 +33,16 @@ public function addNode(DataFlowNode $node): void } /** - * @param array $added_taints - * @param array $removed_taints + * @param null $added_taints + * @param null $removed_taints */ #[Override] public function addPath( DataFlowNode $from, DataFlowNode $to, string $path_type, - ?array $added_taints = null, - ?array $removed_taints = null, + ?int $added_taints = null, + ?int $removed_taints = null, ): void { $from_id = $from->id; $to_id = $to->id; diff --git a/src/Psalm/Internal/DataFlow/DataFlowNode.php b/src/Psalm/Internal/DataFlow/DataFlowNode.php index 5f2cc1963eb..16d9d68c101 100644 --- a/src/Psalm/Internal/DataFlow/DataFlowNode.php +++ b/src/Psalm/Internal/DataFlow/DataFlowNode.php @@ -6,6 +6,7 @@ use Override; use Psalm\CodeLocation; +use Psalm\Type\TaintKind; use Stringable; use function strtolower; @@ -32,14 +33,14 @@ class DataFlowNode implements Stringable public array $specialized_calls = []; /** - * @param array $taints + * @param int-mask-of $taints */ public function __construct( public string $id, public string $label, public ?CodeLocation $code_location, ?string $specialization_key = null, - public array $taints = [], + public int $taints = 0, ) { if ($specialization_key) { $this->unspecialized_id = $id; diff --git a/src/Psalm/Internal/DataFlow/Path.php b/src/Psalm/Internal/DataFlow/Path.php index 785bb8e511a..d3c84a963d1 100644 --- a/src/Psalm/Internal/DataFlow/Path.php +++ b/src/Psalm/Internal/DataFlow/Path.php @@ -5,6 +5,7 @@ namespace Psalm\Internal\DataFlow; use Psalm\Storage\ImmutableNonCloneableTrait; +use Psalm\Type\TaintKind; /** * @psalm-immutable @@ -15,14 +16,14 @@ final class Path use ImmutableNonCloneableTrait; /** - * @param ?array $unescaped_taints - * @param ?array $escaped_taints + * @param ?int-mask-of $unescaped_taints + * @param ?int-mask-of $escaped_taints */ public function __construct( public readonly string $type, public readonly int $length, - public readonly ?array $unescaped_taints = null, - public readonly ?array $escaped_taints = null, + public readonly ?int $unescaped_taints = null, + public readonly ?int $escaped_taints = null, ) { } } diff --git a/src/Psalm/Internal/EventDispatcher.php b/src/Psalm/Internal/EventDispatcher.php index e09a2b03b02..561b87a1c68 100644 --- a/src/Psalm/Internal/EventDispatcher.php +++ b/src/Psalm/Internal/EventDispatcher.php @@ -42,6 +42,7 @@ use Psalm\Plugin\EventHandler\RemoveTaintsInterface; use Psalm\Plugin\EventHandler\StringInterpreterInterface; use Psalm\Type\Atomic\TLiteralString; +use Psalm\Type\TaintKind; use function count; use function is_bool; @@ -434,28 +435,28 @@ public function dispatchAfterFunctionLikeAnalysis(AfterFunctionLikeAnalysisEvent } /** - * @return list + * @return int-mask-of */ - public function dispatchAddTaints(AddRemoveTaintsEvent $event): array + public function dispatchAddTaints(AddRemoveTaintsEvent $event): int { - $added_taints = []; + $added_taints = 0; foreach ($this->add_taints_checks as $handler) { - $added_taints = [...$added_taints, ...$handler::addTaints($event)]; + $added_taints |= $handler::addTaints($event); } return $added_taints; } /** - * @return list + * @return int-mask-of */ - public function dispatchRemoveTaints(AddRemoveTaintsEvent $event): array + public function dispatchRemoveTaints(AddRemoveTaintsEvent $event): int { - $removed_taints = []; + $removed_taints = 0; foreach ($this->remove_taints_checks as $handler) { - $removed_taints = [...$removed_taints, ...$handler::removeTaints($event)]; + $removed_taints |= $handler::removeTaints($event); } return $removed_taints; diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index e2a8e39e913..8c71bea39ce 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -271,15 +271,7 @@ public static function parse( if (str_starts_with($taint_type, 'exec_')) { $taint_type = substr($taint_type, 5); - - if ($taint_type === 'tainted') { - $taint_type = TaintKindGroup::GROUP_INPUT; - } - - if ($taint_type === 'misc') { - // @todo `text` is semantically not defined in `TaintKind`, maybe drop it - $taint_type = 'text'; - } + $taint_type = TaintKindGroup::NAME_TO_TAINT[$taint_type]; $info->taint_sink_params[] = ['name' => $param_parts[0], 'taint' => $taint_type]; } @@ -295,7 +287,7 @@ public static function parse( } if ($param_parts[0]) { - $info->taint_source_types[] = $param_parts[0]; + $info->taint_source_types |= TaintKindGroup::NAME_TO_TAINT[$param_parts[0]]; } else { IssueBuffer::maybeAdd( new InvalidDocblock( @@ -314,17 +306,8 @@ public static function parse( } if ($param_parts[0]) { - if ($param_parts[0] === 'tainted') { - $param_parts[0] = TaintKindGroup::GROUP_INPUT; - } - - if ($param_parts[0] === 'misc') { - // @todo `text` is semantically not defined in `TaintKind`, maybe drop it - $param_parts[0] = 'text'; - } - if ($param_parts[0] !== 'none') { - $info->taint_source_types[] = $param_parts[0]; + $info->taint_source_types |= TaintKindGroup::NAME_TO_TAINT[$param_parts[0]]; } } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 9408afed92d..04282b9f5ca 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -53,9 +53,6 @@ use function array_any; use function array_filter; use function array_merge; -use function array_search; -use function array_splice; -use function array_unique; use function array_values; use function count; use function explode; @@ -360,22 +357,12 @@ public static function addDocblockInfo( } } - $docblock_info->taint_source_types = array_values(array_unique($docblock_info->taint_source_types)); - // expand 'input' group to all items, e.g. `['other', 'input']` -> `['other', 'html', 'sql', 'shell', ...]` - $inputIndex = array_search(TaintKindGroup::GROUP_INPUT, $docblock_info->taint_source_types, true); - if ($inputIndex !== false) { - array_splice( - $docblock_info->taint_source_types, - $inputIndex, - 1, - TaintKindGroup::ALL_INPUT, - ); - } // merge taints from doc block to storage, enforce uniqueness and having consecutive index keys - $storage->taint_source_types = array_merge($storage->taint_source_types, $docblock_info->taint_source_types); - $storage->taint_source_types = array_values(array_unique($storage->taint_source_types)); + $storage->taint_source_types |= $docblock_info->taint_source_types; - $storage->added_taints = $docblock_info->added_taints; + foreach ($docblock_info->added_taints as $taint) { + $storage->added_taints |= TaintKindGroup::NAME_TO_TAINT[$taint]; + } foreach ($docblock_info->removed_taints as $removed_taint) { if ($removed_taint[0] === '(') { @@ -394,7 +381,7 @@ public static function addDocblockInfo( $file_scanner, ); } else { - $storage->removed_taints[] = $removed_taint; + $storage->removed_taints |= TaintKindGroup::NAME_TO_TAINT[$removed_taint]; } } diff --git a/src/Psalm/Internal/Provider/AddRemoveTaints/HtmlFunctionTainter.php b/src/Psalm/Internal/Provider/AddRemoveTaints/HtmlFunctionTainter.php index aa9f34eb9d5..bedd454e5d2 100644 --- a/src/Psalm/Internal/Provider/AddRemoveTaints/HtmlFunctionTainter.php +++ b/src/Psalm/Internal/Provider/AddRemoveTaints/HtmlFunctionTainter.php @@ -25,10 +25,10 @@ final class HtmlFunctionTainter implements AddTaintsInterface, RemoveTaintsInter /** * Called to see what taints should be added * - * @return list + * @return int-mask-of */ #[Override] - public static function addTaints(AddRemoveTaintsEvent $event): array + public static function addTaints(AddRemoveTaintsEvent $event): int { $item = $event->getExpr(); $statements_analyzer = $event->getStatementsSource(); @@ -40,7 +40,7 @@ public static function addTaints(AddRemoveTaintsEvent $event): array || count($item->name->getParts()) !== 1 || count($item->getArgs()) === 0 ) { - return []; + return 0; } $function_id = strtolower($item->name->getFirst()); @@ -52,36 +52,36 @@ public static function addTaints(AddRemoveTaintsEvent $event): array if ($second_arg === null) { if ($statements_analyzer->getCodebase()->analysis_php_version_id >= 8_01_00) { - return [TaintKind::INPUT_HTML, TaintKind::INPUT_HAS_QUOTES]; + return TaintKind::INPUT_HTML|TaintKind::INPUT_HAS_QUOTES; } - return [TaintKind::INPUT_HTML]; + return TaintKind::INPUT_HTML; } $second_arg_value = $statements_analyzer->node_data->getType($second_arg); if (!$second_arg_value || !$second_arg_value->isSingleIntLiteral()) { - return [TaintKind::INPUT_HTML]; + return TaintKind::INPUT_HTML; } $second_arg_value = $second_arg_value->getSingleIntLiteral()->value; if (($second_arg_value & ENT_QUOTES) === ENT_QUOTES) { - return [TaintKind::INPUT_HTML, TaintKind::INPUT_HAS_QUOTES]; + return TaintKind::INPUT_HTML|TaintKind::INPUT_HAS_QUOTES; } - return [TaintKind::INPUT_HTML]; + return TaintKind::INPUT_HTML; } - return []; + return 0; } /** * Called to see what taints should be removed * - * @return list + * @return int-mask-of */ #[Override] - public static function removeTaints(AddRemoveTaintsEvent $event): array + public static function removeTaints(AddRemoveTaintsEvent $event): int { $item = $event->getExpr(); $statements_analyzer = $event->getStatementsSource(); @@ -93,7 +93,7 @@ public static function removeTaints(AddRemoveTaintsEvent $event): array || count($item->name->getParts()) !== 1 || count($item->getArgs()) === 0 ) { - return []; + return 0; } $function_id = strtolower($item->name->getFirst()); @@ -105,26 +105,26 @@ public static function removeTaints(AddRemoveTaintsEvent $event): array if ($second_arg === null) { if ($statements_analyzer->getCodebase()->analysis_php_version_id >= 8_01_00) { - return [TaintKind::INPUT_HTML, TaintKind::INPUT_HAS_QUOTES]; + return TaintKind::INPUT_HTML|TaintKind::INPUT_HAS_QUOTES; } - return [TaintKind::INPUT_HTML]; + return TaintKind::INPUT_HTML; } $second_arg_value = $statements_analyzer->node_data->getType($second_arg); if (!$second_arg_value || !$second_arg_value->isSingleIntLiteral()) { - return [TaintKind::INPUT_HTML]; + return TaintKind::INPUT_HTML; } $second_arg_value = $second_arg_value->getSingleIntLiteral()->value; if (($second_arg_value & ENT_QUOTES) === ENT_QUOTES) { - return [TaintKind::INPUT_HTML, TaintKind::INPUT_HAS_QUOTES]; + return TaintKind::INPUT_HTML|TaintKind::INPUT_HAS_QUOTES; } - return [TaintKind::INPUT_HTML]; + return TaintKind::INPUT_HTML; } - return []; + return 0; } } diff --git a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php index 82d8d96f4a2..800ba7f7ad7 100644 --- a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php +++ b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php @@ -4,6 +4,8 @@ namespace Psalm\Internal\Scanner; +use Psalm\Type\TaintKind; + /** * @internal */ @@ -109,9 +111,9 @@ final class FunctionDocblockComment public array $taint_sink_params = []; /** - * @var array + * @var int-mask-of */ - public array $taint_source_types = []; + public int $taint_source_types = 0; /** * @var array diff --git a/src/Psalm/Internal/Scanner/VarDocblockComment.php b/src/Psalm/Internal/Scanner/VarDocblockComment.php index c8869a578db..b834e900f05 100644 --- a/src/Psalm/Internal/Scanner/VarDocblockComment.php +++ b/src/Psalm/Internal/Scanner/VarDocblockComment.php @@ -4,6 +4,7 @@ namespace Psalm\Internal\Scanner; +use Psalm\Type\TaintKind; use Psalm\Type\Union; /** @@ -49,9 +50,9 @@ final class VarDocblockComment public bool $allow_private_mutation = false; /** - * @var list + * @var int-mask-of */ - public array $removed_taints = []; + public int $removed_taints = 0; /** * @var array diff --git a/src/Psalm/Plugin/EventHandler/AddTaintsInterface.php b/src/Psalm/Plugin/EventHandler/AddTaintsInterface.php index e00d65e8e29..7854252a618 100644 --- a/src/Psalm/Plugin/EventHandler/AddTaintsInterface.php +++ b/src/Psalm/Plugin/EventHandler/AddTaintsInterface.php @@ -5,13 +5,14 @@ namespace Psalm\Plugin\EventHandler; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; +use Psalm\Type\TaintKind; interface AddTaintsInterface { /** * Called to see what taints should be added * - * @return list + * @return int-mask-of */ - public static function addTaints(AddRemoveTaintsEvent $event): array; + public static function addTaints(AddRemoveTaintsEvent $event): int; } diff --git a/src/Psalm/Plugin/EventHandler/RemoveTaintsInterface.php b/src/Psalm/Plugin/EventHandler/RemoveTaintsInterface.php index 4b6ed36e8c6..ef0f70ad405 100644 --- a/src/Psalm/Plugin/EventHandler/RemoveTaintsInterface.php +++ b/src/Psalm/Plugin/EventHandler/RemoveTaintsInterface.php @@ -5,13 +5,14 @@ namespace Psalm\Plugin\EventHandler; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; +use Psalm\Type\TaintKind; interface RemoveTaintsInterface { /** * Called to see what taints should be removed * - * @return list + * @return int-mask-of */ - public static function removeTaints(AddRemoveTaintsEvent $event): array; + public static function removeTaints(AddRemoveTaintsEvent $event): int; } diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index 384d0ecbd48..4a0fa3a919f 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -8,6 +8,7 @@ use Psalm\CodeLocation; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Issue\CodeIssue; +use Psalm\Type\TaintKind; use Psalm\Type\Union; use Stringable; @@ -150,19 +151,19 @@ abstract class FunctionLikeStorage implements HasAttributesInterface, Stringable public bool $specialize_call = false; /** - * @var array + * @var int-mask-of */ - public array $taint_source_types = []; + public int $taint_source_types = 0; /** - * @var array + * @var int-mask-of */ - public array $added_taints = []; + public int $added_taints = 0; /** - * @var array + * @var int-mask-of */ - public array $removed_taints = []; + public int $removed_taints = 0; /** * @var array diff --git a/src/Psalm/Type/TaintKind.php b/src/Psalm/Type/TaintKind.php index 5b2ac02a8ae..234e0643d05 100644 --- a/src/Psalm/Type/TaintKind.php +++ b/src/Psalm/Type/TaintKind.php @@ -6,25 +6,28 @@ /** * An Enum class holding all the taint types that Psalm recognises + * + * Not using an enum since real code usages will use only the integer value, + * and extracting it with ->value every time is a pain. */ final class TaintKind { - public const INPUT_CALLABLE = 'callable'; - public const INPUT_UNSERIALIZE = 'unserialize'; - public const INPUT_INCLUDE = 'include'; - public const INPUT_EVAL = 'eval'; - public const INPUT_LDAP = 'ldap'; - public const INPUT_SQL = 'sql'; - public const INPUT_HTML = 'html'; - public const INPUT_HAS_QUOTES = 'has_quotes'; - public const INPUT_SHELL = 'shell'; - public const INPUT_SSRF = 'ssrf'; - public const INPUT_FILE = 'file'; - public const INPUT_COOKIE = 'cookie'; - public const INPUT_HEADER = 'header'; - public const INPUT_XPATH = 'xpath'; - public const INPUT_SLEEP = 'sleep'; - public const INPUT_EXTRACT = 'extract'; - public const USER_SECRET = 'user_secret'; - public const SYSTEM_SECRET = 'system_secret'; + public const INPUT_CALLABLE = (1 << 0); + public const INPUT_UNSERIALIZE = (1 << 2); + public const INPUT_INCLUDE = (1 << 3); + public const INPUT_EVAL = (1 << 4); + public const INPUT_LDAP = (1 << 5); + public const INPUT_SQL = (1 << 6); + public const INPUT_HTML = (1 << 7); + public const INPUT_HAS_QUOTES = (1 << 8); + public const INPUT_SHELL = (1 << 9); + public const INPUT_SSRF = (1 << 10); + public const INPUT_FILE = (1 << 11); + public const INPUT_COOKIE = (1 << 12); + public const INPUT_HEADER = (1 << 13); + public const INPUT_XPATH = (1 << 14); + public const INPUT_SLEEP = (1 << 15); + public const INPUT_EXTRACT = (1 << 16); + public const USER_SECRET = (1 << 17); + public const SYSTEM_SECRET = (1 << 18); } diff --git a/src/Psalm/Type/TaintKindGroup.php b/src/Psalm/Type/TaintKindGroup.php index 2049012cfb1..858c4dea64c 100644 --- a/src/Psalm/Type/TaintKindGroup.php +++ b/src/Psalm/Type/TaintKindGroup.php @@ -9,24 +9,49 @@ */ final class TaintKindGroup { - public const GROUP_INPUT = 'input'; + public const ALL_INPUT = (TaintKind::INPUT_EXTRACT << 1) - 1; - public const ALL_INPUT = [ - TaintKind::INPUT_HTML, - TaintKind::INPUT_HAS_QUOTES, - TaintKind::INPUT_SHELL, - TaintKind::INPUT_SQL, - TaintKind::INPUT_CALLABLE, - TaintKind::INPUT_EVAL, - TaintKind::INPUT_UNSERIALIZE, - TaintKind::INPUT_INCLUDE, - TaintKind::INPUT_SSRF, - TaintKind::INPUT_LDAP, - TaintKind::INPUT_FILE, - TaintKind::INPUT_HEADER, - TaintKind::INPUT_COOKIE, - TaintKind::INPUT_XPATH, - TaintKind::INPUT_SLEEP, - TaintKind::INPUT_EXTRACT, + public const TAINT_TO_NAME = [ + TaintKind::INPUT_CALLABLE => 'callable', + TaintKind::INPUT_UNSERIALIZE => 'unserialize', + TaintKind::INPUT_INCLUDE => 'include', + TaintKind::INPUT_EVAL => 'eval', + TaintKind::INPUT_LDAP => 'ldap', + TaintKind::INPUT_SQL => 'sql', + TaintKind::INPUT_HTML => 'html', + TaintKind::INPUT_HAS_QUOTES => 'has_quotes', + TaintKind::INPUT_SHELL => 'shell', + TaintKind::INPUT_SSRF => 'ssrf', + TaintKind::INPUT_FILE => 'file', + TaintKind::INPUT_COOKIE => 'cookie', + TaintKind::INPUT_HEADER => 'header', + TaintKind::INPUT_XPATH => 'xpath', + TaintKind::INPUT_SLEEP => 'sleep', + TaintKind::INPUT_EXTRACT => 'extract', + TaintKind::USER_SECRET => 'user_secret', + TaintKind::SYSTEM_SECRET => 'system_secret', + self::ALL_INPUT => 'input', + ]; + public const NAME_TO_TAINT = [ + 'callable' => TaintKind::INPUT_CALLABLE, + 'unserialize' => TaintKind::INPUT_UNSERIALIZE, + 'include' => TaintKind::INPUT_INCLUDE, + 'eval' => TaintKind::INPUT_EVAL, + 'ldap' => TaintKind::INPUT_LDAP, + 'sql' => TaintKind::INPUT_SQL, + 'html' => TaintKind::INPUT_HTML, + 'has_quotes' => TaintKind::INPUT_HAS_QUOTES, + 'shell' => TaintKind::INPUT_SHELL, + 'ssrf' => TaintKind::INPUT_SSRF, + 'file' => TaintKind::INPUT_FILE, + 'cookie' => TaintKind::INPUT_COOKIE, + 'header' => TaintKind::INPUT_HEADER, + 'xpath' => TaintKind::INPUT_XPATH, + 'sleep' => TaintKind::INPUT_SLEEP, + 'extract' => TaintKind::INPUT_EXTRACT, + 'user_secret' => TaintKind::USER_SECRET, + 'system_secret' => TaintKind::SYSTEM_SECRET, + 'input' => self::ALL_INPUT, + 'tainted' => self::ALL_INPUT, ]; } From 0dc132aeb827d4260d6b3da68e7bd9c6d7f2da24 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 26 Feb 2025 21:05:13 +0100 Subject: [PATCH 02/42] Finalize --- docs/security_analysis/custom_taint_sources.md | 7 ++++--- examples/plugins/TaintActiveRecords.php | 8 ++++---- src/Psalm/Internal/Codebase/TaintFlowGraph.php | 5 ++--- src/Psalm/Type/TaintKindGroup.php | 5 +++-- .../AddTaints/TaintBadDataPlugin.php | 16 +++++++--------- .../RemoveTaints/RemoveAllTaintsPlugin.php | 5 +++-- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/security_analysis/custom_taint_sources.md b/docs/security_analysis/custom_taint_sources.md index 938bd3455ba..a65decc66b0 100644 --- a/docs/security_analysis/custom_taint_sources.md +++ b/docs/security_analysis/custom_taint_sources.md @@ -28,6 +28,7 @@ namespace Psalm\Example\Plugin; use PhpParser\Node\Expr\Variable; use Psalm\Plugin\EventHandler\AddTaintsInterface; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; +use Psalm\Type\TaintKind; use Psalm\Type\TaintKindGroup; /** @@ -38,9 +39,9 @@ class TaintBadDataPlugin implements AddTaintsInterface /** * Called to see what taints should be added * - * @return list + * @return int-mask-of */ - public static function addTaints(AddRemoveTaintsEvent $event): array + public static function addTaints(AddRemoveTaintsEvent $event): int { $expr = $event->getExpr(); @@ -48,7 +49,7 @@ class TaintBadDataPlugin implements AddTaintsInterface return TaintKindGroup::ALL_INPUT; } - return []; + return 0; } } ``` diff --git a/examples/plugins/TaintActiveRecords.php b/examples/plugins/TaintActiveRecords.php index 242e9e3a864..49380624ad8 100644 --- a/examples/plugins/TaintActiveRecords.php +++ b/examples/plugins/TaintActiveRecords.php @@ -27,16 +27,16 @@ final class TaintActiveRecords implements AddTaintsInterface /** * Called to see what taints should be added * - * @return list + * @return int-mask-of */ #[Override] - public static function addTaints(AddRemoveTaintsEvent $event): array + public static function addTaints(AddRemoveTaintsEvent $event): int { $expr = $event->getExpr(); // Model properties are accessed by property fetch, so abort here if ($expr instanceof ArrayItem) { - return []; + return 0; } $statements_source = $event->getStatementsSource(); @@ -55,7 +55,7 @@ public static function addTaints(AddRemoveTaintsEvent $event): array } } while ($expr = self::getParentNode($expr)); - return []; + return 0; } /** diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 56feb66652d..292513ec239 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -217,9 +217,8 @@ public function connectSinksAndSources(): void foreach ($sources as $source) { $source_taints = $source->taints; - sort($source_taints); - $visited_source_ids[$source->id][implode(',', $source_taints)] = true; + $visited_source_ids[$source->id][$source_taints] = true; $generated_sources = $this->getSpecializedSources($source); @@ -306,7 +305,7 @@ private function getChildNodes( $path = $this->getPredecessorPath($generated_source) . ' -> ' . $this->getSuccessorPath($sinks[$to_id]); - foreach (TaintKindGroup::TAINT_TO_NAME as $matching_taint) { + foreach (TaintKindGroup::TAINT_TO_NAME as $matching_taint => $_) { if (!($matching_taints & $matching_taint)) { continue; } diff --git a/src/Psalm/Type/TaintKindGroup.php b/src/Psalm/Type/TaintKindGroup.php index 858c4dea64c..77a92061a14 100644 --- a/src/Psalm/Type/TaintKindGroup.php +++ b/src/Psalm/Type/TaintKindGroup.php @@ -9,8 +9,9 @@ */ final class TaintKindGroup { - public const ALL_INPUT = (TaintKind::INPUT_EXTRACT << 1) - 1; + public const ALL_INPUT = (1 << 17) - 1; + /** @var array, string> */ public const TAINT_TO_NAME = [ TaintKind::INPUT_CALLABLE => 'callable', TaintKind::INPUT_UNSERIALIZE => 'unserialize', @@ -30,8 +31,8 @@ final class TaintKindGroup TaintKind::INPUT_EXTRACT => 'extract', TaintKind::USER_SECRET => 'user_secret', TaintKind::SYSTEM_SECRET => 'system_secret', - self::ALL_INPUT => 'input', ]; + /** @var array> */ public const NAME_TO_TAINT = [ 'callable' => TaintKind::INPUT_CALLABLE, 'unserialize' => TaintKind::INPUT_UNSERIALIZE, diff --git a/tests/Config/Plugin/EventHandler/AddTaints/TaintBadDataPlugin.php b/tests/Config/Plugin/EventHandler/AddTaints/TaintBadDataPlugin.php index 31229b50287..54f9e97ebe8 100644 --- a/tests/Config/Plugin/EventHandler/AddTaints/TaintBadDataPlugin.php +++ b/tests/Config/Plugin/EventHandler/AddTaints/TaintBadDataPlugin.php @@ -20,31 +20,29 @@ final class TaintBadDataPlugin implements AddTaintsInterface { /** * Called to see what taints should be added - * - * @return list */ #[Override] - public static function addTaints(AddRemoveTaintsEvent $event): array + public static function addTaints(AddRemoveTaintsEvent $event): int { $expr = $event->getExpr(); if (!$expr instanceof Variable) { - return []; + return 0; } switch ($expr->name) { case 'bad_data': return TaintKindGroup::ALL_INPUT; case 'bad_sql': - return [TaintKind::INPUT_SQL]; + return TaintKind::INPUT_SQL; case 'bad_html': - return [TaintKind::INPUT_HTML]; + return TaintKind::INPUT_HTML; case 'bad_eval': - return [TaintKind::INPUT_EVAL]; + return TaintKind::INPUT_EVAL; case 'bad_file': - return [TaintKind::INPUT_FILE]; + return TaintKind::INPUT_FILE; } - return []; + return 0; } } diff --git a/tests/Config/Plugin/EventHandler/RemoveTaints/RemoveAllTaintsPlugin.php b/tests/Config/Plugin/EventHandler/RemoveTaints/RemoveAllTaintsPlugin.php index a6eea0763e9..e84a4dbf52d 100644 --- a/tests/Config/Plugin/EventHandler/RemoveTaints/RemoveAllTaintsPlugin.php +++ b/tests/Config/Plugin/EventHandler/RemoveTaints/RemoveAllTaintsPlugin.php @@ -7,6 +7,7 @@ use Override; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Plugin\EventHandler\RemoveTaintsInterface; +use Psalm\Type\TaintKind; use Psalm\Type\TaintKindGroup; /** @@ -17,10 +18,10 @@ final class RemoveAllTaintsPlugin implements RemoveTaintsInterface /** * Called to see what taints should be removed * - * @return list + * @return int-mask-of */ #[Override] - public static function removeTaints(AddRemoveTaintsEvent $event): array + public static function removeTaints(AddRemoveTaintsEvent $event): int { return TaintKindGroup::ALL_INPUT; } From 4f8725e5cc4a0548ebf75f6555561e834d35ee72 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 26 Feb 2025 21:15:00 +0100 Subject: [PATCH 03/42] Finalize --- dictionaries/InternalTaintSinkMap.php | 104 +++++++++--------- .../Analyzer/Statements/ReturnAnalyzer.php | 14 +-- .../Reflector/FunctionLikeDocblockParser.php | 2 +- .../Reflector/FunctionLikeDocblockScanner.php | 2 +- .../Scanner/FunctionDocblockComment.php | 2 +- src/Psalm/Storage/FunctionLikeParameter.php | 5 +- 6 files changed, 60 insertions(+), 69 deletions(-) diff --git a/dictionaries/InternalTaintSinkMap.php b/dictionaries/InternalTaintSinkMap.php index f4bc8fc50f3..1e9aefed084 100644 --- a/dictionaries/InternalTaintSinkMap.php +++ b/dictionaries/InternalTaintSinkMap.php @@ -8,56 +8,56 @@ * @var non-empty-array>> */ return [ -'exec' => [[TaintKind::INPUT_SHELL]], -'create_function' => [[], [TaintKind::INPUT_EVAL]], -'file_get_contents' => [[TaintKind::INPUT_FILE]], -'file_put_contents' => [[TaintKind::INPUT_FILE]], -'fopen' => [[TaintKind::INPUT_FILE]], -'unlink' => [[TaintKind::INPUT_FILE]], -'copy' => [[TaintKind::INPUT_FILE], [TaintKind::INPUT_FILE]], -'file' => [[TaintKind::INPUT_FILE]], -'link' => [[TaintKind::INPUT_FILE], [TaintKind::INPUT_FILE]], -'mkdir' => [[TaintKind::INPUT_FILE]], -'move_uploaded_file' => [[TaintKind::INPUT_FILE], [TaintKind::INPUT_FILE]], -'parse_ini_file' => [[TaintKind::INPUT_FILE]], -'chown' => [[TaintKind::INPUT_FILE]], -'lchown' => [[TaintKind::INPUT_FILE]], -'readfile' => [[TaintKind::INPUT_FILE]], -'rename' => [[TaintKind::INPUT_FILE], [TaintKind::INPUT_FILE]], -'rmdir' => [[TaintKind::INPUT_FILE]], -'header' => [[TaintKind::INPUT_HEADER]], -'symlink' => [[TaintKind::INPUT_FILE]], -'tempnam' => [[TaintKind::INPUT_FILE]], -'igbinary_unserialize' => [[TaintKind::INPUT_UNSERIALIZE]], -'ldap_search' => [[], [TaintKind::INPUT_LDAP], [TaintKind::INPUT_LDAP]], -'mysqli_query' => [[], [TaintKind::INPUT_SQL]], -'mysqli::query' => [[TaintKind::INPUT_SQL]], -'mysqli_real_query' => [[], [TaintKind::INPUT_SQL]], -'mysqli::real_query' => [[TaintKind::INPUT_SQL]], -'mysqli_multi_query' => [[], [TaintKind::INPUT_SQL]], -'mysqli::multi_query' => [[TaintKind::INPUT_SQL]], -'mysqli_prepare' => [[], [TaintKind::INPUT_SQL]], -'mysqli::prepare' => [[TaintKind::INPUT_SQL]], -'mysqli_stmt::__construct' => [[], [TaintKind::INPUT_SQL]], -'mysqli_stmt_prepare' => [[], [TaintKind::INPUT_SQL]], -'mysqli_stmt::prepare' => [[TaintKind::INPUT_SQL]], -'passthru' => [[TaintKind::INPUT_SHELL]], -'pcntl_exec' => [[TaintKind::INPUT_SHELL]], -'pg_exec' => [[], [TaintKind::INPUT_SQL]], -'pg_prepare' => [[], [], [TaintKind::INPUT_SQL]], -'pg_put_line' => [[], [TaintKind::INPUT_SQL]], -'pg_query' => [[], [TaintKind::INPUT_SQL]], -'pg_query_params' => [[], [TaintKind::INPUT_SQL]], -'pg_send_prepare' => [[], [], [TaintKind::INPUT_SQL]], -'pg_send_query' => [[], [TaintKind::INPUT_SQL]], -'pg_send_query_params' => [[], [TaintKind::INPUT_SQL], []], -'setcookie' => [[TaintKind::INPUT_COOKIE], [TaintKind::INPUT_COOKIE]], -'shell_exec' => [[TaintKind::INPUT_SHELL]], -'system' => [[TaintKind::INPUT_SHELL]], -'unserialize' => [[TaintKind::INPUT_UNSERIALIZE]], -'popen' => [[TaintKind::INPUT_SHELL]], -'proc_open' => [[TaintKind::INPUT_SHELL]], -'curl_init' => [[TaintKind::INPUT_SSRF]], -'curl_setopt' => [[], [], [TaintKind::INPUT_SSRF]], -'getimagesize' => [[TaintKind::INPUT_SSRF]], +'exec' => [TaintKind::INPUT_SHELL], +'create_function' => [0, TaintKind::INPUT_EVAL], +'file_get_contents' => [TaintKind::INPUT_FILE], +'file_put_contents' => [TaintKind::INPUT_FILE], +'fopen' => [TaintKind::INPUT_FILE], +'unlink' => [TaintKind::INPUT_FILE], +'copy' => [TaintKind::INPUT_FILE, TaintKind::INPUT_FILE], +'file' => [TaintKind::INPUT_FILE], +'link' => [TaintKind::INPUT_FILE, TaintKind::INPUT_FILE], +'mkdir' => [TaintKind::INPUT_FILE], +'move_uploaded_file' => [TaintKind::INPUT_FILE, TaintKind::INPUT_FILE], +'parse_ini_file' => [TaintKind::INPUT_FILE], +'chown' => [TaintKind::INPUT_FILE], +'lchown' => [TaintKind::INPUT_FILE], +'readfile' => [TaintKind::INPUT_FILE], +'rename' => [TaintKind::INPUT_FILE, TaintKind::INPUT_FILE], +'rmdir' => [TaintKind::INPUT_FILE], +'header' => [TaintKind::INPUT_HEADER], +'symlink' => [TaintKind::INPUT_FILE], +'tempnam' => [TaintKind::INPUT_FILE], +'igbinary_unserialize' => [TaintKind::INPUT_UNSERIALIZE], +'ldap_search' => [0, TaintKind::INPUT_LDAP, TaintKind::INPUT_LDAP], +'mysqli_query' => [0, TaintKind::INPUT_SQL], +'mysqli::query' => [TaintKind::INPUT_SQL], +'mysqli_real_query' => [0, TaintKind::INPUT_SQL], +'mysqli::real_query' => [TaintKind::INPUT_SQL], +'mysqli_multi_query' => [0, TaintKind::INPUT_SQL], +'mysqli::multi_query' => [TaintKind::INPUT_SQL], +'mysqli_prepare' => [0, TaintKind::INPUT_SQL], +'mysqli::prepare' => [TaintKind::INPUT_SQL], +'mysqli_stmt::__construct' => [0, TaintKind::INPUT_SQL], +'mysqli_stmt_prepare' => [0, TaintKind::INPUT_SQL], +'mysqli_stmt::prepare' => [TaintKind::INPUT_SQL], +'passthru' => [TaintKind::INPUT_SHELL], +'pcntl_exec' => [TaintKind::INPUT_SHELL], +'pg_exec' => [0, TaintKind::INPUT_SQL], +'pg_prepare' => [0, 0, TaintKind::INPUT_SQL], +'pg_put_line' => [0, TaintKind::INPUT_SQL], +'pg_query' => [0, TaintKind::INPUT_SQL], +'pg_query_params' => [0, TaintKind::INPUT_SQL], +'pg_send_prepare' => [0, 0, TaintKind::INPUT_SQL], +'pg_send_query' => [0, TaintKind::INPUT_SQL], +'pg_send_query_params' => [0, TaintKind::INPUT_SQL, 0], +'setcookie' => [TaintKind::INPUT_COOKIE, TaintKind::INPUT_COOKIE], +'shell_exec' => [TaintKind::INPUT_SHELL], +'system' => [TaintKind::INPUT_SHELL], +'unserialize' => [TaintKind::INPUT_UNSERIALIZE], +'popen' => [TaintKind::INPUT_SHELL], +'proc_open' => [TaintKind::INPUT_SHELL], +'curl_init' => [TaintKind::INPUT_SSRF], +'curl_setopt' => [0, 0, TaintKind::INPUT_SSRF], +'getimagesize' => [TaintKind::INPUT_SSRF], ]; diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index 7d70252fc3b..9b03afe39c4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -605,18 +605,8 @@ private static function handleTaints( $event = new AddRemoveTaintsEvent($stmt->expr, $context, $statements_analyzer, $codebase); $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); - $storage->added_taints = array_unique( - array_merge( - $storage->added_taints, - $added_taints, - ), - ); - $storage->removed_taints = array_unique( - array_merge( - $storage->removed_taints, - $codebase->config->eventDispatcher->dispatchRemoveTaints($event), - ), - ); + $storage->added_taints |= $added_taints; + $storage->removed_taints |= $codebase->config->eventDispatcher->dispatchRemoveTaints($event); if ($inferred_type->parent_nodes) { foreach ($inferred_type->parent_nodes as $parent_node) { diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 8c71bea39ce..9141f0ff23f 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -246,7 +246,7 @@ public static function parse( } if (count($param_parts) >= 2) { - $info->taint_sink_params[] = ['name' => $param_parts[1], 'taint' => $param_parts[0]]; + $info->taint_sink_params[] = ['name' => $param_parts[1], 'taint' => TaintKindGroup::NAME_TO_TAINT[$param_parts[0]]]; } else { IssueBuffer::maybeAdd( new InvalidDocblock( diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 04282b9f5ca..e4237f0064f 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -352,7 +352,7 @@ public static function addDocblockInfo( foreach ($storage->params as $param_storage) { if ($param_storage->name === $param_name) { - $param_storage->sinks[] = $taint_sink_param['taint']; + $param_storage->sinks |= $taint_sink_param['taint']; } } } diff --git a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php index 800ba7f7ad7..6fbaeb4182b 100644 --- a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php +++ b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php @@ -106,7 +106,7 @@ final class FunctionDocblockComment public array $removed_taints = []; /** - * @var array + * @var array}> */ public array $taint_sink_params = []; diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index 8214dab83e8..aaca5fece93 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -8,6 +8,7 @@ use Psalm\CodeLocation; use Psalm\Internal\Scanner\UnresolvedConstantComponent; use Psalm\Type\MutableTypeVisitor; +use Psalm\Type\TaintKind; use Psalm\Type\TypeNode; use Psalm\Type\TypeVisitor; use Psalm\Type\Union; @@ -22,9 +23,9 @@ final class FunctionLikeParameter implements HasAttributesInterface, TypeNode public ?CodeLocation $signature_type_location = null; /** - * @var array|null + * @var int-mask-of */ - public ?array $sinks = null; + public int $sinks = 0; public bool $assert_untainted = false; From ac46aa8e7c94549b72adf7a19334da483595828c Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 26 Feb 2025 21:53:01 +0100 Subject: [PATCH 04/42] Cleanup --- src/Psalm/Internal/Codebase/DataFlowGraph.php | 4 ++-- .../Internal/Codebase/TaintFlowGraph.php | 21 +++++++++---------- src/Psalm/Internal/DataFlow/Path.php | 8 +++---- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/Psalm/Internal/Codebase/DataFlowGraph.php b/src/Psalm/Internal/Codebase/DataFlowGraph.php index cc861d57f25..abce3e81e40 100644 --- a/src/Psalm/Internal/Codebase/DataFlowGraph.php +++ b/src/Psalm/Internal/Codebase/DataFlowGraph.php @@ -35,8 +35,8 @@ public function addPath( DataFlowNode $from, DataFlowNode $to, string $path_type, - ?int $added_taints = null, - ?int $removed_taints = null, + int $added_taints = 0, + int $removed_taints = 0, ): void { $from_id = $from->id; $to_id = $to->id; diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 292513ec239..90c61445ced 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -254,15 +254,13 @@ private function getChildNodes( $project_analyzer = ProjectAnalyzer::getInstance(); foreach ($this->forward_edges[$generated_source->id] as $to_id => $path) { - $path_type = $path->type; - $added_taints = $path->unescaped_taints ?? 0; - $removed_taints = $path->escaped_taints ?? 0; - if (!isset($this->nodes[$to_id])) { continue; } - $destination_node = $this->nodes[$to_id]; + $path_type = $path->type; + $added_taints = $path->unescaped_taints; + $removed_taints = $path->escaped_taints; $new_taints = ($source_taints | $added_taints) & ~$removed_taints; @@ -290,20 +288,21 @@ private function getChildNodes( } if (isset($sinks[$to_id])) { - $matching_taints = $sinks[$to_id]->taints & $new_taints; + $sink = $sinks[$to_id]; + $matching_taints = $sink->taints & $new_taints; if ($matching_taints && $generated_source->code_location) { - if ($sinks[$to_id]->code_location - && $config->reportIssueInFile('TaintedInput', $sinks[$to_id]->code_location->file_path) + if ($sink->code_location + && $config->reportIssueInFile('TaintedInput', $sink->code_location->file_path) ) { - $issue_location = $sinks[$to_id]->code_location; + $issue_location = $sink->code_location; } else { $issue_location = $generated_source->code_location; } $issue_trace = $this->getIssueTrace($generated_source); $path = $this->getPredecessorPath($generated_source) - . ' -> ' . $this->getSuccessorPath($sinks[$to_id]); + . ' -> ' . $this->getSuccessorPath($sink); foreach (TaintKindGroup::TAINT_TO_NAME as $matching_taint => $_) { if (!($matching_taints & $matching_taint)) { @@ -431,7 +430,7 @@ private function getChildNodes( } } - $new_destination = clone $destination_node; + $new_destination = clone $this->nodes[$to_id]; $new_destination->previous = $generated_source; $new_destination->taints = $new_taints; $new_destination->specialized_calls = $generated_source->specialized_calls; diff --git a/src/Psalm/Internal/DataFlow/Path.php b/src/Psalm/Internal/DataFlow/Path.php index d3c84a963d1..35882ebc6f1 100644 --- a/src/Psalm/Internal/DataFlow/Path.php +++ b/src/Psalm/Internal/DataFlow/Path.php @@ -16,14 +16,14 @@ final class Path use ImmutableNonCloneableTrait; /** - * @param ?int-mask-of $unescaped_taints - * @param ?int-mask-of $escaped_taints + * @param int-mask-of $unescaped_taints + * @param int-mask-of $escaped_taints */ public function __construct( public readonly string $type, public readonly int $length, - public readonly ?int $unescaped_taints = null, - public readonly ?int $escaped_taints = null, + public readonly int $unescaped_taints = 0, + public readonly int $escaped_taints = 0, ) { } } From ce1513b41c5e2f05906e7b3753a24eed54ef739e Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 27 Feb 2025 18:25:32 +0100 Subject: [PATCH 05/42] Cleanup --- src/Psalm/Codebase.php | 7 ------- .../Statements/Expression/AssignmentAnalyzer.php | 11 ----------- .../Call/FunctionCallReturnTypeFetcher.php | 2 -- .../Fetch/AtomicPropertyFetchAnalyzer.php | 5 ----- .../Internal/Analyzer/Statements/ReturnAnalyzer.php | 2 -- src/Psalm/Internal/Codebase/DataFlowGraph.php | 5 ----- .../Internal/Codebase/InternalCallMapHandler.php | 5 ++--- src/Psalm/Internal/Codebase/TaintFlowGraph.php | 13 ++++--------- src/Psalm/Internal/DataFlow/DataFlowNode.php | 4 ---- src/Psalm/Internal/DataFlow/Path.php | 9 ++------- src/Psalm/Internal/EventDispatcher.php | 7 ------- .../AddRemoveTaints/HtmlFunctionTainter.php | 4 ---- .../Internal/Scanner/FunctionDocblockComment.php | 7 +------ src/Psalm/Internal/Scanner/VarDocblockComment.php | 4 ---- .../Plugin/EventHandler/AddTaintsInterface.php | 3 --- .../Plugin/EventHandler/RemoveTaintsInterface.php | 3 --- src/Psalm/Storage/FunctionLikeParameter.php | 4 ---- src/Psalm/Storage/FunctionLikeStorage.php | 10 ---------- src/Psalm/Type/TaintKindGroup.php | 2 +- 19 files changed, 10 insertions(+), 97 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 80e584f06cf..821c0b1b85c 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -65,7 +65,6 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\TaintKind; use Psalm\Type\TaintKindGroup; use Psalm\Type\Union; use ReflectionProperty; @@ -2143,9 +2142,6 @@ public function queueClassLikeForScanning( $this->scanner->queueClassLikeForScanning($fq_classlike_name, $analyze_too, $store_failure, $phantom_classes); } - /** - * @param int-mask-of $taints - */ public function addTaintSource( Union $expr_type, string $taint_id, @@ -2169,9 +2165,6 @@ public function addTaintSource( return $expr_type->addParentNodes([$source->id => $source]); } - /** - * @param int-mask-of $taints - */ public function addTaintSink( string $taint_id, int $taints = TaintKindGroup::ALL_INPUT, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 25c09b89c8e..33cdf4fa878 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -85,7 +85,6 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNull; -use Psalm\Type\TaintKind; use Psalm\Type\Union; use UnexpectedValueException; @@ -513,7 +512,6 @@ public static function analyze( /** * @param list $var_comments - * @param int-mask-of $removed_taints * @return null|false */ private static function analyzeAssignment( @@ -598,7 +596,6 @@ private static function analyzeAssignment( /** * @param list $var_comments - * @param int-mask-of $removed_taints */ private static function analyzeDocComment( StatementsAnalyzer $statements_analyzer, @@ -789,10 +786,6 @@ public static function assignTypeFromVarDocblock( } } - /** - * @param int-mask-of $removed_taints - * @param int-mask-of $added_taints - */ private static function taintAssignment( Union &$type, DataFlowGraph $data_flow_graph, @@ -1153,7 +1146,6 @@ public static function assignByRefParam( /** * @param PhpParser\Node\Expr\List_|PhpParser\Node\Expr\Array_ $assign_var * @param list $var_comments - * @param int-mask-of $removed_taints */ private static function analyzeDestructuringAssignment( StatementsAnalyzer $statements_analyzer, @@ -1869,9 +1861,6 @@ private static function analyzeVariableUse( return $assign_value_type->setParentNodes($parent_nodes); } - /** - * @param int-mask-of $removed_taints - */ private static function analyzeAssignValueDataFlow( StatementsAnalyzer $statements_analyzer, Codebase $codebase, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 300fb0ccb0c..a040727e21a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -680,8 +680,6 @@ private static function taintReturnType( /** * @param array $args - * @param int-mask-of $removed_taints - * @param int-mask-of $added_taints */ public static function taintUsingFlows( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index f4304a18e6f..6e9497a2b9b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -61,7 +61,6 @@ use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\TaintKind; use Psalm\Type\Union; use function array_filter; @@ -921,10 +920,6 @@ public static function processTaints( } } - /** - * @param ?int-mask-of $added_taints - * @param ?int-mask-of $removed_taints - */ public static function processUnspecialTaints( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr $stmt, diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index 9b03afe39c4..4c1336755de 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -47,8 +47,6 @@ use Psalm\Type\Atomic\TClosure; use Psalm\Type\Union; -use function array_merge; -use function array_unique; use function count; use function explode; use function implode; diff --git a/src/Psalm/Internal/Codebase/DataFlowGraph.php b/src/Psalm/Internal/Codebase/DataFlowGraph.php index abce3e81e40..d60acd7fed9 100644 --- a/src/Psalm/Internal/Codebase/DataFlowGraph.php +++ b/src/Psalm/Internal/Codebase/DataFlowGraph.php @@ -6,7 +6,6 @@ use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\DataFlow\Path; -use Psalm\Type\TaintKind; use function abs; use function array_keys; @@ -27,10 +26,6 @@ abstract class DataFlowGraph abstract public function addNode(DataFlowNode $node): void; - /** - * @param int-mask-of $added_taints - * @param int-mask-of $removed_taints - */ public function addPath( DataFlowNode $from, DataFlowNode $to, diff --git a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php index 2fd2990b3bc..db41a01eb0f 100644 --- a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php +++ b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php @@ -16,7 +16,6 @@ use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\TaintKind; use UnexpectedValueException; use function array_shift; @@ -53,7 +52,7 @@ final class InternalCallMapHandler private static ?array $call_map_callables = []; /** - * @var non-empty-array>>|null + * @var non-empty-array>|null */ private static ?array $taint_sink_map = null; @@ -367,7 +366,7 @@ public static function getCallMap(): array self::$loaded_php_minor_version = $analyzer_minor_version; /** - * @var non-empty-array>> + * @var non-empty-array> */ $taint_map_data = require(dirname(__DIR__, 4) . '/dictionaries/InternalTaintSinkMap.php'); diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 90c61445ced..08c4e64ab42 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -37,10 +37,8 @@ use function array_filter; use function count; use function end; -use function implode; use function json_encode; use function ksort; -use function sort; use function strlen; use function substr; @@ -237,7 +235,6 @@ public function connectSinksAndSources(): void } /** - * @param int-mask-of $source_taints * @param array $sinks * @return array */ @@ -258,16 +255,14 @@ private function getChildNodes( continue; } - $path_type = $path->type; - $added_taints = $path->unescaped_taints; - $removed_taints = $path->escaped_taints; - - $new_taints = ($source_taints | $added_taints) & ~$removed_taints; + $new_taints = ($source_taints | $path->added_taints) & ~$path->removed_taints; if (isset($visited_source_ids[$to_id][$new_taints])) { continue; } + $path_type = $path->type; + if (self::shouldIgnoreFetch($path_type, 'arraykey', $generated_source->path_types)) { continue; } @@ -438,7 +433,7 @@ private function getChildNodes( $key = $to_id . ' ' . json_encode($new_destination->specialized_calls, JSON_THROW_ON_ERROR) . - ' ' . json_encode($new_destination->taints, JSON_THROW_ON_ERROR); + ' ' . $new_destination->taints; $new_sources[$key] = $new_destination; } diff --git a/src/Psalm/Internal/DataFlow/DataFlowNode.php b/src/Psalm/Internal/DataFlow/DataFlowNode.php index 16d9d68c101..cd4de6f0e45 100644 --- a/src/Psalm/Internal/DataFlow/DataFlowNode.php +++ b/src/Psalm/Internal/DataFlow/DataFlowNode.php @@ -6,7 +6,6 @@ use Override; use Psalm\CodeLocation; -use Psalm\Type\TaintKind; use Stringable; use function strtolower; @@ -32,9 +31,6 @@ class DataFlowNode implements Stringable */ public array $specialized_calls = []; - /** - * @param int-mask-of $taints - */ public function __construct( public string $id, public string $label, diff --git a/src/Psalm/Internal/DataFlow/Path.php b/src/Psalm/Internal/DataFlow/Path.php index 35882ebc6f1..16ba662cf7e 100644 --- a/src/Psalm/Internal/DataFlow/Path.php +++ b/src/Psalm/Internal/DataFlow/Path.php @@ -5,7 +5,6 @@ namespace Psalm\Internal\DataFlow; use Psalm\Storage\ImmutableNonCloneableTrait; -use Psalm\Type\TaintKind; /** * @psalm-immutable @@ -15,15 +14,11 @@ final class Path { use ImmutableNonCloneableTrait; - /** - * @param int-mask-of $unescaped_taints - * @param int-mask-of $escaped_taints - */ public function __construct( public readonly string $type, public readonly int $length, - public readonly int $unescaped_taints = 0, - public readonly int $escaped_taints = 0, + public readonly int $added_taints = 0, + public readonly int $removed_taints = 0, ) { } } diff --git a/src/Psalm/Internal/EventDispatcher.php b/src/Psalm/Internal/EventDispatcher.php index 561b87a1c68..595bc31ebd1 100644 --- a/src/Psalm/Internal/EventDispatcher.php +++ b/src/Psalm/Internal/EventDispatcher.php @@ -42,7 +42,6 @@ use Psalm\Plugin\EventHandler\RemoveTaintsInterface; use Psalm\Plugin\EventHandler\StringInterpreterInterface; use Psalm\Type\Atomic\TLiteralString; -use Psalm\Type\TaintKind; use function count; use function is_bool; @@ -434,9 +433,6 @@ public function dispatchAfterFunctionLikeAnalysis(AfterFunctionLikeAnalysisEvent return null; } - /** - * @return int-mask-of - */ public function dispatchAddTaints(AddRemoveTaintsEvent $event): int { $added_taints = 0; @@ -448,9 +444,6 @@ public function dispatchAddTaints(AddRemoveTaintsEvent $event): int return $added_taints; } - /** - * @return int-mask-of - */ public function dispatchRemoveTaints(AddRemoveTaintsEvent $event): int { $removed_taints = 0; diff --git a/src/Psalm/Internal/Provider/AddRemoveTaints/HtmlFunctionTainter.php b/src/Psalm/Internal/Provider/AddRemoveTaints/HtmlFunctionTainter.php index bedd454e5d2..3effa05b0df 100644 --- a/src/Psalm/Internal/Provider/AddRemoveTaints/HtmlFunctionTainter.php +++ b/src/Psalm/Internal/Provider/AddRemoveTaints/HtmlFunctionTainter.php @@ -24,8 +24,6 @@ final class HtmlFunctionTainter implements AddTaintsInterface, RemoveTaintsInter { /** * Called to see what taints should be added - * - * @return int-mask-of */ #[Override] public static function addTaints(AddRemoveTaintsEvent $event): int @@ -77,8 +75,6 @@ public static function addTaints(AddRemoveTaintsEvent $event): int /** * Called to see what taints should be removed - * - * @return int-mask-of */ #[Override] public static function removeTaints(AddRemoveTaintsEvent $event): int diff --git a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php index 6fbaeb4182b..c85ae72cc9a 100644 --- a/src/Psalm/Internal/Scanner/FunctionDocblockComment.php +++ b/src/Psalm/Internal/Scanner/FunctionDocblockComment.php @@ -4,8 +4,6 @@ namespace Psalm\Internal\Scanner; -use Psalm\Type\TaintKind; - /** * @internal */ @@ -106,13 +104,10 @@ final class FunctionDocblockComment public array $removed_taints = []; /** - * @var array}> + * @var array */ public array $taint_sink_params = []; - /** - * @var int-mask-of - */ public int $taint_source_types = 0; /** diff --git a/src/Psalm/Internal/Scanner/VarDocblockComment.php b/src/Psalm/Internal/Scanner/VarDocblockComment.php index b834e900f05..8b5361d8b05 100644 --- a/src/Psalm/Internal/Scanner/VarDocblockComment.php +++ b/src/Psalm/Internal/Scanner/VarDocblockComment.php @@ -4,7 +4,6 @@ namespace Psalm\Internal\Scanner; -use Psalm\Type\TaintKind; use Psalm\Type\Union; /** @@ -49,9 +48,6 @@ final class VarDocblockComment */ public bool $allow_private_mutation = false; - /** - * @var int-mask-of - */ public int $removed_taints = 0; /** diff --git a/src/Psalm/Plugin/EventHandler/AddTaintsInterface.php b/src/Psalm/Plugin/EventHandler/AddTaintsInterface.php index 7854252a618..6bde436f86a 100644 --- a/src/Psalm/Plugin/EventHandler/AddTaintsInterface.php +++ b/src/Psalm/Plugin/EventHandler/AddTaintsInterface.php @@ -5,14 +5,11 @@ namespace Psalm\Plugin\EventHandler; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; -use Psalm\Type\TaintKind; interface AddTaintsInterface { /** * Called to see what taints should be added - * - * @return int-mask-of */ public static function addTaints(AddRemoveTaintsEvent $event): int; } diff --git a/src/Psalm/Plugin/EventHandler/RemoveTaintsInterface.php b/src/Psalm/Plugin/EventHandler/RemoveTaintsInterface.php index ef0f70ad405..2f1a2c43c40 100644 --- a/src/Psalm/Plugin/EventHandler/RemoveTaintsInterface.php +++ b/src/Psalm/Plugin/EventHandler/RemoveTaintsInterface.php @@ -5,14 +5,11 @@ namespace Psalm\Plugin\EventHandler; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; -use Psalm\Type\TaintKind; interface RemoveTaintsInterface { /** * Called to see what taints should be removed - * - * @return int-mask-of */ public static function removeTaints(AddRemoveTaintsEvent $event): int; } diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index aaca5fece93..9556b204292 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -8,7 +8,6 @@ use Psalm\CodeLocation; use Psalm\Internal\Scanner\UnresolvedConstantComponent; use Psalm\Type\MutableTypeVisitor; -use Psalm\Type\TaintKind; use Psalm\Type\TypeNode; use Psalm\Type\TypeVisitor; use Psalm\Type\Union; @@ -22,9 +21,6 @@ final class FunctionLikeParameter implements HasAttributesInterface, TypeNode public ?CodeLocation $signature_type_location = null; - /** - * @var int-mask-of - */ public int $sinks = 0; public bool $assert_untainted = false; diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index 4a0fa3a919f..15dd876c10a 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -8,7 +8,6 @@ use Psalm\CodeLocation; use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Issue\CodeIssue; -use Psalm\Type\TaintKind; use Psalm\Type\Union; use Stringable; @@ -150,19 +149,10 @@ abstract class FunctionLikeStorage implements HasAttributesInterface, Stringable */ public bool $specialize_call = false; - /** - * @var int-mask-of - */ public int $taint_source_types = 0; - /** - * @var int-mask-of - */ public int $added_taints = 0; - /** - * @var int-mask-of - */ public int $removed_taints = 0; /** diff --git a/src/Psalm/Type/TaintKindGroup.php b/src/Psalm/Type/TaintKindGroup.php index 77a92061a14..4bf2aebe964 100644 --- a/src/Psalm/Type/TaintKindGroup.php +++ b/src/Psalm/Type/TaintKindGroup.php @@ -11,7 +11,7 @@ final class TaintKindGroup { public const ALL_INPUT = (1 << 17) - 1; - /** @var array, string> */ + /** @var array */ public const TAINT_TO_NAME = [ TaintKind::INPUT_CALLABLE => 'callable', TaintKind::INPUT_UNSERIALIZE => 'unserialize', From aeab1543abb709bc2e75389842cd5bd04c1f5b53 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 27 Feb 2025 18:42:08 +0100 Subject: [PATCH 06/42] Cleanup --- .../Internal/Analyzer/ClosureAnalyzer.php | 4 +-- .../Analyzer/FunctionLikeAnalyzer.php | 2 +- .../Analyzer/Statements/Block/TryAnalyzer.php | 2 +- .../Assignment/ArrayAssignmentAnalyzer.php | 2 +- .../Expression/AssignmentAnalyzer.php | 8 ++--- .../Expression/BinaryOpAnalyzer.php | 6 ++-- .../Call/NamedFunctionCallHandler.php | 2 +- .../Expression/Fetch/ArrayFetchAnalyzer.php | 2 +- .../Fetch/AtomicPropertyFetchAnalyzer.php | 6 ++-- .../Fetch/VariableFetchAnalyzer.php | 24 +++----------- .../Internal/Codebase/TaintFlowGraph.php | 16 +++++----- .../Internal/Codebase/VariableUseGraph.php | 8 ++--- src/Psalm/Internal/DataFlow/DataFlowNode.php | 31 +++++++++++++++++-- 13 files changed, 59 insertions(+), 54 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php index 4b5f1d7918d..fdc27a23c1d 100644 --- a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php @@ -146,7 +146,7 @@ public static function analyzeExpression( foreach ($parent_nodes as $parent_node) { $statements_analyzer->data_flow_graph->addPath( $parent_node, - new DataFlowNode('closure-use', 'closure use', null), + DataFlowNode::getForClosureUse(), 'closure-use', ); } @@ -190,7 +190,7 @@ public static function analyzeExpression( foreach ($parent_nodes as $parent_node) { $statements_analyzer->data_flow_graph->addPath( $parent_node, - new DataFlowNode('closure-use', 'closure use', null), + DataFlowNode::getForClosureUse(), 'closure-use', ); } diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 917b8136cc5..5f7c3197cb2 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -275,7 +275,7 @@ public function analyze( if ($statements_analyzer->data_flow_graph && $use_assignment) { $statements_analyzer->data_flow_graph->addPath( $use_assignment, - new DataFlowNode('closure-use', 'closure use', null), + DataFlowNode::getForClosureUse(), 'closure-use', ); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php index d8b02dc00b3..d3bc22343e2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php @@ -313,7 +313,7 @@ public static function analyze( if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { $statements_analyzer->data_flow_graph->addPath( $catch_var_node, - new DataFlowNode('variable-use', 'variable use', null), + DataFlowNode::getForVariableUse(), 'variable-use', ); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index 721cc9ad5f5..04e4737c52e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -884,7 +884,7 @@ private static function analyzeNestedArrayAssignment( $root_var_id, new CodeLocation($statements_analyzer->getSource(), $root_var), ), - new DataFlowNode('variable-use', 'variable use', null), + DataFlowNode::getForVariableUse(), 'variable-use', ); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 33cdf4fa878..32c770bbe0c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -1086,7 +1086,7 @@ public static function assignByRefParam( $statements_analyzer->data_flow_graph->addPath( $byref_node, - new DataFlowNode('variable-use', 'variable use', null), + DataFlowNode::getForVariableUse(), 'variable-use', ); } @@ -1754,7 +1754,7 @@ private static function analyzeAssignmentToVariable( // Mark reference to an external scope as used when a value is assigned to it $statements_analyzer->data_flow_graph->addPath( $assignment_node, - new DataFlowNode('variable-use', 'variable use', null), + DataFlowNode::getForVariableUse(), 'variable-use', ); } @@ -1822,7 +1822,7 @@ private static function analyzeAssignmentToVariable( foreach ($assign_value_type->parent_nodes as $parent_node) { $statements_analyzer->data_flow_graph->addPath( $parent_node, - new DataFlowNode('variable-use', 'variable use', null), + DataFlowNode::getForVariableUse(), 'variable-use', ); } @@ -1843,7 +1843,7 @@ private static function analyzeVariableUse( new CodeLocation($statements_analyzer->getSource(), $assign_var), ); } else { - $assignment_node = new DataFlowNode('unknown-origin', 'unknown origin', null); + $assignment_node = DataFlowNode::getForUnknownOrigin(); } $parent_nodes = [ diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php index 7dd7f6fb8d5..71a6c9cb75f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php @@ -441,19 +441,19 @@ public static function addDataFlow( if ($left instanceof PhpParser\Node\Expr\PropertyFetch) { $statements_analyzer->data_flow_graph->addPath( $new_parent_node, - new DataFlowNode('variable-use', 'variable use', null), + DataFlowNode::getForVariableUse(), '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), + DataFlowNode::getForVariableUse(), '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), + DataFlowNode::getForVariableUse(), 'variable-use', ); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php index f29b75fbb5a..6c0a87cfaed 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NamedFunctionCallHandler.php @@ -420,7 +420,7 @@ public static function handle( foreach ($source->param_nodes as $param_node) { $statements_analyzer->data_flow_graph->addPath( $param_node, - new DataFlowNode('variable-use', 'variable use', null), + DataFlowNode::getForVariableUse(), 'variable-use', ); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 5dde0c1c67c..f5e596e56fd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -1115,7 +1115,7 @@ public static function handleMixedArrayAccess( $data_flow_graph->addPath( $parent_node, - new DataFlowNode('variable-use', 'variable use', null), + DataFlowNode::getForVariableUse(), 'variable-use', ); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index 6e9497a2b9b..967ee761b60 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -926,8 +926,8 @@ public static function processUnspecialTaints( Union &$type, string $property_id, bool $in_assignment, - ?int $added_taints, - ?int $removed_taints, + int $added_taints, + int $removed_taints, ): void { if (!$statements_analyzer->data_flow_graph) { return; @@ -979,8 +979,6 @@ public static function processUnspecialTaints( $type = $type->setParentNodes([$localized_property_node->id => $localized_property_node], true); - $added_taints ??= 0; - $removed_taints ??= 0; $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { $taint_source = TaintSource::fromNode($localized_property_node); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 4c8b4dd6dab..12eb0543c70 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -464,41 +464,25 @@ private static function addDataFlowToVariable( if ($context->inside_call || $context->inside_return) { $statements_analyzer->data_flow_graph->addPath( $parent_node, - new DataFlowNode( - 'variable-use', - 'variable use', - null, - ), + DataFlowNode::getForVariableUse(), 'use-inside-call', ); } elseif ($context->inside_conditional) { $statements_analyzer->data_flow_graph->addPath( $parent_node, - new DataFlowNode( - 'variable-use', - 'variable use', - null, - ), + DataFlowNode::getForVariableUse(), 'use-inside-conditional', ); } elseif ($context->inside_isset) { $statements_analyzer->data_flow_graph->addPath( $parent_node, - new DataFlowNode( - 'variable-use', - 'variable use', - null, - ), + DataFlowNode::getForVariableUse(), 'use-inside-isset', ); } else { $statements_analyzer->data_flow_graph->addPath( $parent_node, - new DataFlowNode( - 'variable-use', - 'variable use', - null, - ), + DataFlowNode::getForVariableUse(), 'variable-use', ); } diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 08c4e64ab42..394d0146858 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -121,7 +121,7 @@ public function getPredecessorPath(DataFlowNode $source): string $source_descriptor = $source->label . ($location_summary ? ' (' . $location_summary . ')' : ''); - $previous_source = $source->previous; + $previous_source = $source->taintSource; if ($previous_source) { if ($previous_source === $source) { @@ -131,9 +131,9 @@ public function getPredecessorPath(DataFlowNode $source): string if ($source->code_location && $previous_source->code_location && $previous_source->code_location->getHash() === $source->code_location->getHash() - && $previous_source->previous + && $previous_source->taintSource ) { - return $this->getPredecessorPath($previous_source->previous) . ' -> ' . $source_descriptor; + return $this->getPredecessorPath($previous_source->taintSource) . ' -> ' . $source_descriptor; } return $this->getPredecessorPath($previous_source) . ' -> ' . $source_descriptor; @@ -152,7 +152,7 @@ public function getSuccessorPath(DataFlowNode $sink): string $sink_descriptor = $sink->label . ($location_summary ? ' (' . $location_summary . ')' : ''); - $next_sink = $sink->previous; + $next_sink = $sink->taintSource; if ($next_sink) { if ($next_sink === $sink) { @@ -162,9 +162,9 @@ public function getSuccessorPath(DataFlowNode $sink): string if ($sink->code_location && $next_sink->code_location && $next_sink->code_location->getHash() === $sink->code_location->getHash() - && $next_sink->previous + && $next_sink->taintSource ) { - return $sink_descriptor . ' -> ' . $this->getSuccessorPath($next_sink->previous); + return $sink_descriptor . ' -> ' . $this->getSuccessorPath($next_sink->taintSource); } return $sink_descriptor . ' -> ' . $this->getSuccessorPath($next_sink); @@ -178,7 +178,7 @@ public function getSuccessorPath(DataFlowNode $sink): string */ public function getIssueTrace(DataFlowNode $source): array { - $previous_source = $source->previous; + $previous_source = $source->taintSource; $node = [ 'location' => $source->code_location, @@ -426,7 +426,7 @@ private function getChildNodes( } $new_destination = clone $this->nodes[$to_id]; - $new_destination->previous = $generated_source; + $new_destination->taintSource = $generated_source; $new_destination->taints = $new_taints; $new_destination->specialized_calls = $generated_source->specialized_calls; $new_destination->path_types = [...$generated_source->path_types, $path_type]; diff --git a/src/Psalm/Internal/Codebase/VariableUseGraph.php b/src/Psalm/Internal/Codebase/VariableUseGraph.php index c9cbcb0fc56..f814e809685 100644 --- a/src/Psalm/Internal/Codebase/VariableUseGraph.php +++ b/src/Psalm/Internal/Codebase/VariableUseGraph.php @@ -32,17 +32,13 @@ public function addNode(DataFlowNode $node): void $this->nodes[$node->id] = $node; } - /** - * @param null $added_taints - * @param null $removed_taints - */ #[Override] public function addPath( DataFlowNode $from, DataFlowNode $to, string $path_type, - ?int $added_taints = null, - ?int $removed_taints = null, + int $added_taints = 0, + int $removed_taints = 0, ): void { $from_id = $from->id; $to_id = $to->id; diff --git a/src/Psalm/Internal/DataFlow/DataFlowNode.php b/src/Psalm/Internal/DataFlow/DataFlowNode.php index cd4de6f0e45..a57553e15be 100644 --- a/src/Psalm/Internal/DataFlow/DataFlowNode.php +++ b/src/Psalm/Internal/DataFlow/DataFlowNode.php @@ -21,7 +21,7 @@ class DataFlowNode implements Stringable public ?string $specialization_key = null; /** @var ?self */ - public ?DataFlowNode $previous = null; + public ?DataFlowNode $taintSource = null; /** @var list */ public array $path_types = []; @@ -88,7 +88,6 @@ final public static function getForAssignment( return new static($id, $var_id, $assignment_location, $specialization_key); } - /** * @return static */ @@ -112,6 +111,34 @@ final public static function getForMethodReturn( ); } + + /** + * @return static + */ + final public static function getForVariableUse(): self + { + return new static('variable-use', 'variable use', null); + } + + + + /** + * @return static + */ + final public static function getForUnknownOrigin(): self + { + return new static('unknown-origin', 'unknown origin', null); + } + + + /** + * @return static + */ + final public static function getForClosureUse(): self + { + return new static('closure-use', 'closure use', null); + } + #[Override] public function __toString(): string { From 4230cf57d19854ad3eda59035d01ff854981af15 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 27 Feb 2025 19:17:27 +0100 Subject: [PATCH 07/42] Simplify --- .../Internal/Codebase/TaintFlowGraph.php | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 394d0146858..2361487400e 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -34,7 +34,6 @@ use Psalm\Type\TaintKind; use Psalm\Type\TaintKindGroup; -use function array_filter; use function count; use function end; use function json_encode; @@ -218,15 +217,68 @@ public function connectSinksAndSources(): void $visited_source_ids[$source->id][$source_taints] = true; - $generated_sources = $this->getSpecializedSources($source); - - foreach ($generated_sources as $generated_source) { - $new_sources = [...$new_sources, ...$this->getChildNodes( + if (isset($this->forward_edges[$source->id])) { + $new_sources += $this->getChildNodes( + $source, + $source_taints, + $sinks, + $visited_source_ids, + ); + continue; + } + + if ($source->specialization_key !== null && isset($this->specialized_calls[$source->specialization_key])) { + $new_id = substr($source->id, 0, -strlen($source->specialization_key) - 1); + if (!isset($this->forward_edges[$new_id])) { + continue; + } + $generated_source = clone $source; + $generated_source->id = $new_id; + $generated_source->specialized_calls[$source->specialization_key][$new_id] = true; + + $new_sources += $this->getChildNodes( $generated_source, $source_taints, $sinks, $visited_source_ids, - )]; + ); + } elseif (isset($this->specializations[$source->id])) { + foreach ($this->specializations[$source->id] as $specialization => $_) { + if (!$source->specialized_calls || isset($source->specialized_calls[$specialization])) { + $new_id = $source->id . '-' . $specialization; + if (!isset($this->forward_edges[$new_id])) { + continue; + } + $new_source = clone $source; + $new_source->id = $new_id; + unset($new_source->specialized_calls[$specialization]); + + $new_sources += $this->getChildNodes( + $new_source, + $source_taints, + $sinks, + $visited_source_ids, + ); + } + } + } else { + foreach ($source->specialized_calls as $key => $map) { + if (isset($map[$source->id])) { + $new_id = $source->id . '-' . $key; + if (!isset($this->forward_edges[$new_id])) { + continue; + } + $new_source = clone $source; + $new_source->id = $new_id; + + $new_sources += $this->getChildNodes( + $new_source, + $source_taints, + $sinks, + $visited_source_ids, + ); + } + } } } @@ -439,56 +491,4 @@ private function getChildNodes( return $new_sources; } - - /** @return array */ - private function getSpecializedSources(DataFlowNode $source): array - { - $generated_sources = []; - - if (isset($this->forward_edges[$source->id])) { - return [$source]; - } - - if ($source->specialization_key && isset($this->specialized_calls[$source->specialization_key])) { - $generated_source = clone $source; - - $generated_source->id = substr($source->id, 0, -strlen($source->specialization_key) - 1); - - $generated_source->specialized_calls[$source->specialization_key][$generated_source->id] = true; - - $generated_sources[] = $generated_source; - } elseif (isset($this->specializations[$source->id])) { - foreach ($this->specializations[$source->id] as $specialization => $_) { - if (!$source->specialized_calls || isset($source->specialized_calls[$specialization])) { - $new_source = clone $source; - - $new_source->id = $source->id . '-' . $specialization; - - unset($new_source->specialized_calls[$specialization]); - - $generated_sources[] = $new_source; - } - } - } else { - foreach ($source->specialized_calls as $key => $map) { - if (isset($map[$source->id]) && isset($this->forward_edges[$source->id . '-' . $key])) { - $new_source = clone $source; - - $new_source->id = $source->id . '-' . $key; - - $generated_sources[] = $new_source; - } - } - } - - return array_filter( - $generated_sources, - $this->doesForwardEdgeExist(...), - ); - } - - private function doesForwardEdgeExist(DataFlowNode $new_source): bool - { - return isset($this->forward_edges[$new_source->id]); - } } From 4a44d556b5b535f0763b8f0e59c33ec6889f59ba Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 27 Feb 2025 19:26:59 +0100 Subject: [PATCH 08/42] Cleanup --- .../Internal/Codebase/TaintFlowGraph.php | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 2361487400e..61a95351544 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -218,7 +218,8 @@ public function connectSinksAndSources(): void $visited_source_ids[$source->id][$source_taints] = true; if (isset($this->forward_edges[$source->id])) { - $new_sources += $this->getChildNodes( + $this->getChildNodes( + $new_sources, $source, $source_taints, $sinks, @@ -236,7 +237,8 @@ public function connectSinksAndSources(): void $generated_source->id = $new_id; $generated_source->specialized_calls[$source->specialization_key][$new_id] = true; - $new_sources += $this->getChildNodes( + $this->getChildNodes( + $new_sources, $generated_source, $source_taints, $sinks, @@ -253,7 +255,8 @@ public function connectSinksAndSources(): void $new_source->id = $new_id; unset($new_source->specialized_calls[$specialization]); - $new_sources += $this->getChildNodes( + $this->getChildNodes( + $new_sources, $new_source, $source_taints, $sinks, @@ -271,7 +274,8 @@ public function connectSinksAndSources(): void $new_source = clone $source; $new_source->id = $new_id; - $new_sources += $this->getChildNodes( + $this->getChildNodes( + $new_sources, $new_source, $source_taints, $sinks, @@ -283,21 +287,22 @@ public function connectSinksAndSources(): void } $sources = $new_sources; + unset($new_sources); } } /** * @param array $sinks - * @return array + * @param array $new_sources + * @param-out array $new_sources */ private function getChildNodes( + array &$new_sources, DataFlowNode $generated_source, int $source_taints, array $sinks, array $visited_source_ids, - ): array { - $new_sources = []; - + ): void { $config = Config::getInstance(); $project_analyzer = ProjectAnalyzer::getInstance(); @@ -488,7 +493,5 @@ private function getChildNodes( ' ' . $new_destination->taints; $new_sources[$key] = $new_destination; } - - return $new_sources; } } From fffe341d157613d72b5467d7e3cba81808c99bf4 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 27 Feb 2025 19:27:17 +0100 Subject: [PATCH 09/42] Cleanup --- src/Psalm/Internal/Codebase/TaintFlowGraph.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 61a95351544..bf21aec95c8 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -203,6 +203,9 @@ public function connectSinksAndSources(): void $sources = $this->sources; $sinks = $this->sinks; + $this->sinks = []; + $this->sources = []; + ksort($this->specializations); ksort($this->forward_edges); From 0d7f3c5fea6bd5881f2e6a93e1d5632fb4771651 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 27 Feb 2025 19:37:44 +0100 Subject: [PATCH 10/42] Cleanup --- .../Analyzer/Statements/Expression/ArrayAnalyzer.php | 5 ++--- .../Assignment/InstancePropertyAssignmentAnalyzer.php | 6 ++---- .../Analyzer/Statements/Expression/AssignmentAnalyzer.php | 4 +--- .../Analyzer/Statements/Expression/BinaryOpAnalyzer.php | 3 +-- .../Statements/Expression/Call/ArgumentAnalyzer.php | 3 +-- .../Statements/Expression/Call/FunctionCallAnalyzer.php | 3 +-- .../Expression/Call/FunctionCallReturnTypeFetcher.php | 3 +-- .../Analyzer/Statements/Expression/Call/NewAnalyzer.php | 3 +-- .../Statements/Expression/EncapsulatedStringAnalyzer.php | 3 +-- .../Analyzer/Statements/Expression/EvalAnalyzer.php | 3 +-- .../Statements/Expression/Fetch/ArrayFetchAnalyzer.php | 3 +-- .../Expression/Fetch/AtomicPropertyFetchAnalyzer.php | 6 ++---- .../Analyzer/Statements/Expression/IncludeAnalyzer.php | 3 +-- src/Psalm/Internal/DataFlow/TaintSource.php | 6 ++++-- 14 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index 07704fb2cbe..5cb2de00f60 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -444,7 +444,7 @@ private static function analyzeArrayItem( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($new_parent_node); + $taint_source = TaintSource::fromNode($new_parent_node, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } @@ -485,8 +485,7 @@ private static function analyzeArrayItem( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($new_parent_node); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($new_parent_node, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php index 9dc38883289..60255875bec 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php @@ -508,8 +508,7 @@ private static function taintProperty( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($property_node); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($property_node, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } @@ -610,8 +609,7 @@ public static function taintUnspecializedProperty( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($property_node); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($property_node, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 32c770bbe0c..89c47f040ca 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -804,9 +804,7 @@ private static function taintAssignment( // become a new taint source $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($new_parent_node); - $taint_source->taints = $taints; - $data_flow_graph->addSource($taint_source); + $data_flow_graph->addSource(TaintSource::fromNode($new_parent_node, $taints)); } foreach ($parent_nodes as $parent_node) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php index 71a6c9cb75f..79f1d10c8bb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php @@ -174,8 +174,7 @@ public static function analyze( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($new_parent_node); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($new_parent_node, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index e7bda48a7d4..06f973a11b2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -1895,8 +1895,7 @@ private static function processTaintedness( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($argument_value_node); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($argument_value_node, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index c27f5da3d63..32c77206c54 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -856,8 +856,7 @@ private static function getAnalyzeNamedExpression( $taints = $added_taints & ~$removed_taints; if ($taints !== 0) { - $taint_source = TaintSource::fromNode($custom_call_sink); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($custom_call_sink, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index a040727e21a..050886f2584 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -749,8 +749,7 @@ public static function taintUsingStorage( $taints = $added_taints & ~$function_storage->removed_taints; if ($taints !== 0) { - $taint_source = TaintSource::fromNode($function_call_node); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($function_call_node, $taints); $graph->addSource($taint_source); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index e8667a1af4f..9a9e533b25b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -743,8 +743,7 @@ private static function analyzeConstructorExpression( $taints = $added_taints & ~$removed_taints; if ($added_taints !== 0) { - $taint_source = TaintSource::fromNode($custom_call_sink); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($custom_call_sink, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php index cc2613fd3b6..f6c10274bf9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php @@ -110,8 +110,7 @@ public static function analyze( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($new_parent_node); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($new_parent_node, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php index a1cb0e65ea3..bdb189186cd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php @@ -65,8 +65,7 @@ public static function analyze( $taints = $added_taints & ~$removed_taints; if ($taints !== 0) { - $taint_source = TaintSource::fromNode($eval_param_sink); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($eval_param_sink, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index f5e596e56fd..06d6175ff0d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -414,8 +414,7 @@ public static function taintArrayFetch( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($new_parent_node); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($new_parent_node, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index 967ee761b60..9e4a56ec801 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -902,8 +902,7 @@ public static function processTaints( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($var_node); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($var_node, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } } @@ -981,8 +980,7 @@ public static function processUnspecialTaints( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($localized_property_node); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($localized_property_node, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php index e4ac3c2b2a9..908d9ee2a6b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php @@ -137,8 +137,7 @@ public static function analyze( $taints = $added_taints & ~$removed_taints; if ($taints !== 0) { - $taint_source = TaintSource::fromNode($include_param_sink); - $taint_source->taints = $taints; + $taint_source = TaintSource::fromNode($include_param_sink, $taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/DataFlow/TaintSource.php b/src/Psalm/Internal/DataFlow/TaintSource.php index e971bf19237..f05cc676c22 100644 --- a/src/Psalm/Internal/DataFlow/TaintSource.php +++ b/src/Psalm/Internal/DataFlow/TaintSource.php @@ -9,14 +9,16 @@ */ final class TaintSource extends DataFlowNode { - public static function fromNode(DataFlowNode $node): self + public static function fromNode(DataFlowNode $node, int $taints): self { - return new self( + $v = new self( $node->id, $node->label, $node->code_location, $node->specialization_key, $node->taints, ); + $v->taints = $taints; + return $v; } } From e632fba2c27fa26f169ba5d61412a02f00b30a65 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 27 Feb 2025 20:00:51 +0100 Subject: [PATCH 11/42] Fix --- src/Psalm/Internal/Codebase/TaintFlowGraph.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index bf21aec95c8..dbc0724cc95 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -485,15 +485,20 @@ private function getChildNodes( } } + $key = $to_id . + ' ' . json_encode($generated_source->specialized_calls, JSON_THROW_ON_ERROR) . + ' ' . $new_taints; + + if (isset($new_sources[$key])) { + continue; + } + $new_destination = clone $this->nodes[$to_id]; $new_destination->taintSource = $generated_source; $new_destination->taints = $new_taints; $new_destination->specialized_calls = $generated_source->specialized_calls; $new_destination->path_types = [...$generated_source->path_types, $path_type]; - $key = $to_id . - ' ' . json_encode($new_destination->specialized_calls, JSON_THROW_ON_ERROR) . - ' ' . $new_destination->taints; $new_sources[$key] = $new_destination; } } From f65e027f109b62f28edf9f4c82d957c71d80d851 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 27 Feb 2025 20:19:08 +0100 Subject: [PATCH 12/42] More cleanup --- .../Internal/Codebase/VariableUseGraph.php | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Codebase/VariableUseGraph.php b/src/Psalm/Internal/Codebase/VariableUseGraph.php index f814e809685..08ad5ea6628 100644 --- a/src/Psalm/Internal/Codebase/VariableUseGraph.php +++ b/src/Psalm/Internal/Codebase/VariableUseGraph.php @@ -113,20 +113,19 @@ public function getOriginLocations(DataFlowNode $assignment_node): array foreach ($child_nodes as $child_node) { $visited_child_ids[$child_node->id] = true; - $parent_nodes = $this->getParentNodes( + $had_parent_nodes = $this->getParentNodes( + $new_parent_nodes, $child_node, $visited_child_ids, ); - if (!$parent_nodes) { + if (!$had_parent_nodes) { if ($child_node->code_location) { $origin_locations[] = $child_node->code_location; } continue; } - - $new_parent_nodes = [...$new_parent_nodes, ...$parent_nodes]; } $child_nodes = $new_parent_nodes; @@ -195,18 +194,19 @@ private function getChildNodes( /** * @param array $visited_source_ids - * @return list + * @param list $new_parent_nodes + * @param-out list $new_parent_nodes */ private function getParentNodes( + array &$new_parent_nodes, DataFlowNode $destination, array $visited_source_ids, - ): array { - $new_parent_nodes = []; - + ): bool { if (!isset($this->backward_edges[$destination->id])) { - return []; + return false; } + $had = false; foreach ($this->backward_edges[$destination->id] as $from_id => $_) { if (isset($visited_source_ids[$from_id])) { continue; @@ -214,9 +214,10 @@ private function getParentNodes( if (isset($this->nodes[$from_id])) { $new_parent_nodes[] = $this->nodes[$from_id]; + $had = true; } } - return $new_parent_nodes; + return $had; } } From 6ea418c44d9cf8bb2f0fd4580226380267f4997e Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 27 Feb 2025 20:38:23 +0100 Subject: [PATCH 13/42] Simplify --- .../Call/Method/MethodCallReturnTypeFetcher.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php index b9d2fbcf9ce..575b2623885 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -330,10 +330,13 @@ public static function taintMethodCallResult( $parent_nodes = $context->vars_in_scope[$var_id]->parent_nodes; - $unspecialized_parent_nodes = array_filter( - $parent_nodes, - static fn(DataFlowNode $parent_node): bool => !$parent_node->specialization_key, - ); + $unspecialized_parent_nodes = false; + foreach ($parent_nodes as $parent_node) { + if ($parent_node->specialization_key === null) { + $unspecialized_parent_nodes = true; + break; + } + } $specialized_parent_nodes = array_filter( $parent_nodes, From 17d2d23e79b13974161969eb0af7e63c03c6cbb9 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 27 Feb 2025 20:39:45 +0100 Subject: [PATCH 14/42] Fix --- .../Call/Method/MethodCallReturnTypeFetcher.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php index 575b2623885..ef3027d3049 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -338,11 +338,6 @@ public static function taintMethodCallResult( } } - $specialized_parent_nodes = array_filter( - $parent_nodes, - static fn(DataFlowNode $parent_node): bool => (bool) $parent_node->specialization_key, - ); - $var_node = DataFlowNode::getForAssignment( $var_id, new CodeLocation($statements_analyzer, $var_expr), @@ -381,7 +376,11 @@ public static function taintMethodCallResult( $method_call_nodes[$method_call_node->id] = $method_call_node; } - foreach ($specialized_parent_nodes as $parent_node) { + foreach ($parent_nodes as $parent_node) { + if ($parent_node->specialization_key === null) { + continue; + } + $universal_method_call_node = DataFlowNode::getForMethodReturn( (string) $method_id, $cased_method_id, From aeaf0b0efbe43a85ce76e8adb3ec070715a60364 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 27 Feb 2025 21:04:49 +0100 Subject: [PATCH 15/42] Cleanup --- .../Internal/Codebase/TaintFlowGraph.php | 19 +++++++++++++++---- src/Psalm/Internal/DataFlow/TaintSource.php | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index dbc0724cc95..fe376d65405 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -11,6 +11,7 @@ use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\DataFlow\TaintSink; use Psalm\Internal\DataFlow\TaintSource; +use Psalm\Internal\Fork\Pool; use Psalm\Issue\TaintedCallable; use Psalm\Issue\TaintedCookie; use Psalm\Issue\TaintedCustom; @@ -209,6 +210,10 @@ public function connectSinksAndSources(): void ksort($this->specializations); ksort($this->forward_edges); + $config = Config::getInstance(); + + $project_analyzer = ProjectAnalyzer::getInstance(); + // reprocess resolved descendants up to a maximum nesting level of 40 for ($i = 0; count($sinks) && count($sources) && $i < 40; $i++) { $new_sources = []; @@ -227,6 +232,8 @@ public function connectSinksAndSources(): void $source_taints, $sinks, $visited_source_ids, + $config, + $project_analyzer, ); continue; } @@ -246,6 +253,8 @@ public function connectSinksAndSources(): void $source_taints, $sinks, $visited_source_ids, + $config, + $project_analyzer, ); } elseif (isset($this->specializations[$source->id])) { foreach ($this->specializations[$source->id] as $specialization => $_) { @@ -264,6 +273,8 @@ public function connectSinksAndSources(): void $source_taints, $sinks, $visited_source_ids, + $config, + $project_analyzer, ); } } @@ -283,6 +294,8 @@ public function connectSinksAndSources(): void $source_taints, $sinks, $visited_source_ids, + $config, + $project_analyzer, ); } } @@ -305,11 +318,9 @@ private function getChildNodes( int $source_taints, array $sinks, array $visited_source_ids, + Config $config, + ProjectAnalyzer $project_analyzer, ): void { - $config = Config::getInstance(); - - $project_analyzer = ProjectAnalyzer::getInstance(); - foreach ($this->forward_edges[$generated_source->id] as $to_id => $path) { if (!isset($this->nodes[$to_id])) { continue; diff --git a/src/Psalm/Internal/DataFlow/TaintSource.php b/src/Psalm/Internal/DataFlow/TaintSource.php index f05cc676c22..688aeea849c 100644 --- a/src/Psalm/Internal/DataFlow/TaintSource.php +++ b/src/Psalm/Internal/DataFlow/TaintSource.php @@ -12,7 +12,7 @@ final class TaintSource extends DataFlowNode public static function fromNode(DataFlowNode $node, int $taints): self { $v = new self( - $node->id, + $node->unspecialized_id ?? $node->id, $node->label, $node->code_location, $node->specialization_key, From eceba2ec4309902d9343fe8e78636a16f3a6053a Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 28 Feb 2025 22:46:48 +0100 Subject: [PATCH 16/42] Performance refactoring --- .../Internal/Codebase/TaintFlowGraph.php | 121 ++++++++++++------ src/Psalm/Internal/DataFlow/DataFlowNode.php | 6 +- 2 files changed, 84 insertions(+), 43 deletions(-) diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index fe376d65405..b4b08e6b052 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -11,7 +11,6 @@ use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\DataFlow\TaintSink; use Psalm\Internal\DataFlow\TaintSource; -use Psalm\Internal\Fork\Pool; use Psalm\Issue\TaintedCallable; use Psalm\Issue\TaintedCookie; use Psalm\Issue\TaintedCustom; @@ -39,8 +38,6 @@ use function end; use function json_encode; use function ksort; -use function strlen; -use function substr; use const JSON_THROW_ON_ERROR; @@ -58,10 +55,11 @@ final class TaintFlowGraph extends DataFlowGraph /** @var array */ private array $sinks = []; - /** @var array> */ - private array $specialized_calls = []; - - /** @var array> */ + /** + * Unspecialized ID => (Specialization key => Specialized ID) + * + * @var array> + */ private array $specializations = []; #[Override] @@ -69,9 +67,10 @@ public function addNode(DataFlowNode $node): void { $this->nodes[$node->id] = $node; - if ($node->unspecialized_id && $node->specialization_key) { - $this->specialized_calls[$node->specialization_key][$node->unspecialized_id] = true; - $this->specializations[$node->unspecialized_id][$node->specialization_key] = true; + if ($node->unspecialized_id !== null) { + /** @var string $node->specialization_key */ + $node->is_first_level_and_specialized = true; + $this->specializations[$node->unspecialized_id][$node->specialization_key] = $node->id; } } @@ -87,14 +86,13 @@ public function addSink(TaintSink $node): void $this->nodes[$node->id] = $node; } - public function addGraph(self $taint): void + public function addGraph(self $other): void { - $this->sources += $taint->sources; - $this->sinks += $taint->sinks; - $this->nodes += $taint->nodes; - $this->specialized_calls += $taint->specialized_calls; + $this->sources += $other->sources; + $this->sinks += $other->sinks; + $this->nodes += $other->nodes; - foreach ($taint->forward_edges as $key => $map) { + foreach ($other->forward_edges as $key => $map) { if (!isset($this->forward_edges[$key])) { $this->forward_edges[$key] = $map; } else { @@ -102,7 +100,7 @@ public function addGraph(self $taint): void } } - foreach ($taint->specializations as $key => $map) { + foreach ($other->specializations as $key => $map) { if (!isset($this->specializations[$key])) { $this->specializations[$key] = $map; } else { @@ -207,13 +205,24 @@ public function connectSinksAndSources(): void $this->sinks = []; $this->sources = []; - ksort($this->specializations); ksort($this->forward_edges); $config = Config::getInstance(); $project_analyzer = ProjectAnalyzer::getInstance(); + // Remove all specializations without an outgoing edge + foreach ($this->specializations as $k => &$map) { + foreach ($map as $kk => $specialized_id) { + if (!isset($this->forward_edges[$specialized_id])) { + unset($map[$kk]); + } + } + if (!$map) { + unset($this->specializations[$k]); + } + } unset($map); + // reprocess resolved descendants up to a maximum nesting level of 40 for ($i = 0; count($sinks) && count($sources) && $i < 40; $i++) { $new_sources = []; @@ -225,6 +234,8 @@ public function connectSinksAndSources(): void $visited_source_ids[$source->id][$source_taints] = true; + // If we have one or more edges starting at this node, + // process destinations of those edges. if (isset($this->forward_edges[$source->id])) { $this->getChildNodes( $new_sources, @@ -238,14 +249,18 @@ public function connectSinksAndSources(): void continue; } - if ($source->specialization_key !== null && isset($this->specialized_calls[$source->specialization_key])) { - $new_id = substr($source->id, 0, -strlen($source->specialization_key) - 1); - if (!isset($this->forward_edges[$new_id])) { + // If this is a specialized node AND it was added using addNode, de-specialize; + // Then, if we have one or more edges starting at the de-specialized node, + // process destinations of those edges. + if ($source->is_first_level_and_specialized) { + /** @var string $source->unspecialized_id */ + if (!isset($this->forward_edges[$source->unspecialized_id])) { continue; } $generated_source = clone $source; - $generated_source->id = $new_id; - $generated_source->specialized_calls[$source->specialization_key][$new_id] = true; + $generated_source->id = $source->unspecialized_id; + $generated_source->is_first_level_and_specialized = false; + $generated_source->processing_specialized_descendants_of[$source->specialization_key][$source->unspecialized_id] = $source->id; $this->getChildNodes( $new_sources, @@ -256,17 +271,38 @@ public function connectSinksAndSources(): void $config, $project_analyzer, ); + + // If this node has first level specializations (=> is first-level & unspecialized), + // process them all } elseif (isset($this->specializations[$source->id])) { - foreach ($this->specializations[$source->id] as $specialization => $_) { - if (!$source->specialized_calls || isset($source->specialized_calls[$specialization])) { - $new_id = $source->id . '-' . $specialization; - if (!isset($this->forward_edges[$new_id])) { + if ($source->processing_specialized_descendants_of) { + // If processing descendants of a specialized call, accept only descendants. + foreach ($this->specializations[$source->id] as $specialization => $specialized_id) { + if (!isset($source->processing_specialized_descendants_of[$specialization])) { continue; } $new_source = clone $source; - $new_source->id = $new_id; - unset($new_source->specialized_calls[$specialization]); - + $new_source->is_first_level_and_specialized = false; + $new_source->id = $specialized_id; + unset($new_source->processing_specialized_descendants_of[$specialization]); + + $this->getChildNodes( + $new_sources, + $new_source, + $source_taints, + $sinks, + $visited_source_ids, + $config, + $project_analyzer, + ); + } + } else { + // If not processing descendants, accept all specializations. + foreach ($this->specializations[$source->id] as $specialization => $specialized_id) { + $new_source = clone $source; + $new_source->is_first_level_and_specialized = false; + $new_source->id = $specialized_id; + $this->getChildNodes( $new_sources, $new_source, @@ -279,14 +315,16 @@ public function connectSinksAndSources(): void } } } else { - foreach ($source->specialized_calls as $key => $map) { + // Process all descendants + foreach ($source->processing_specialized_descendants_of as $map) { if (isset($map[$source->id])) { - $new_id = $source->id . '-' . $key; - if (!isset($this->forward_edges[$new_id])) { + $specialized_id = $map[$source->id]; + if (!isset($this->forward_edges[$specialized_id])) { continue; } $new_source = clone $source; - $new_source->id = $new_id; + $new_source->is_first_level_and_specialized = false; + $new_source->id = $specialized_id; $this->getChildNodes( $new_sources, @@ -347,8 +385,8 @@ private function getChildNodes( } if ($generated_source->code_location - && $project_analyzer->canReportIssues($generated_source->code_location->file_path) - && !$config->reportIssueInFile('TaintedInput', $generated_source->code_location->file_path) + && $project_analyzer->canReportIssues($generated_source->code_location->file_path) + && !$config->reportIssueInFile('TaintedInput', $generated_source->code_location->file_path) ) { continue; } @@ -359,7 +397,7 @@ private function getChildNodes( if ($matching_taints && $generated_source->code_location) { if ($sink->code_location - && $config->reportIssueInFile('TaintedInput', $sink->code_location->file_path) + && $config->reportIssueInFile('TaintedInput', $sink->code_location->file_path) ) { $issue_location = $sink->code_location; } else { @@ -368,7 +406,7 @@ private function getChildNodes( $issue_trace = $this->getIssueTrace($generated_source); $path = $this->getPredecessorPath($generated_source) - . ' -> ' . $this->getSuccessorPath($sink); + . ' -> ' . $this->getSuccessorPath($sink); foreach (TaintKindGroup::TAINT_TO_NAME as $matching_taint => $_) { if (!($matching_taints & $matching_taint)) { @@ -497,17 +535,18 @@ private function getChildNodes( } $key = $to_id . - ' ' . json_encode($generated_source->specialized_calls, JSON_THROW_ON_ERROR) . - ' ' . $new_taints; + ' ' . json_encode($generated_source->processing_specialized_descendants_of, JSON_THROW_ON_ERROR) . + ' ' . $new_taints; if (isset($new_sources[$key])) { continue; } $new_destination = clone $this->nodes[$to_id]; + $new_destination->is_first_level_and_specialized = false; $new_destination->taintSource = $generated_source; $new_destination->taints = $new_taints; - $new_destination->specialized_calls = $generated_source->specialized_calls; + $new_destination->processing_specialized_descendants_of = $generated_source->processing_specialized_descendants_of; $new_destination->path_types = [...$generated_source->path_types, $path_type]; $new_sources[$key] = $new_destination; diff --git a/src/Psalm/Internal/DataFlow/DataFlowNode.php b/src/Psalm/Internal/DataFlow/DataFlowNode.php index a57553e15be..18f17a783ee 100644 --- a/src/Psalm/Internal/DataFlow/DataFlowNode.php +++ b/src/Psalm/Internal/DataFlow/DataFlowNode.php @@ -20,6 +20,8 @@ class DataFlowNode implements Stringable public ?string $specialization_key = null; + public bool $is_first_level_and_specialized = false; + /** @var ?self */ public ?DataFlowNode $taintSource = null; @@ -27,9 +29,9 @@ class DataFlowNode implements Stringable public array $path_types = []; /** - * @var array> + * @var array> */ - public array $specialized_calls = []; + public array $processing_specialized_descendants_of = []; public function __construct( public string $id, From 5f0d74e8321f64139ce8b1347ea074be47aac292 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 4 Mar 2025 09:16:00 +0100 Subject: [PATCH 17/42] Finalize refactoring --- src/Psalm/Codebase.php | 87 ++++++++++++++++++- .../Call/FunctionCallReturnTypeFetcher.php | 13 ++- .../Method/MethodCallReturnTypeFetcher.php | 1 - .../Expression/Call/StaticCallAnalyzer.php | 12 ++- .../Fetch/VariableFetchAnalyzer.php | 4 +- .../Internal/Codebase/TaintFlowGraph.php | 21 +++-- src/Psalm/Internal/PreloaderList.php | 1 - src/Psalm/Type/TaintKind.php | 5 ++ src/Psalm/Type/TaintKindGroup.php | 58 ------------- 9 files changed, 127 insertions(+), 75 deletions(-) delete mode 100644 src/Psalm/Type/TaintKindGroup.php diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 821c0b1b85c..acec45dd12c 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -4,6 +4,7 @@ namespace Psalm; +use AssertionError; use Exception; use InvalidArgumentException; use LanguageServerProtocol\Command; @@ -65,10 +66,11 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\TaintKindGroup; +use Psalm\Type\TaintKind; use Psalm\Type\Union; use ReflectionProperty; use ReflectionType; +use RuntimeException; use UnexpectedValueException; use function array_combine; @@ -99,6 +101,7 @@ use function substr; use function substr_count; +use const PHP_INT_SIZE; use const PHP_VERSION_ID; /** @@ -254,6 +257,42 @@ final class Codebase public bool $literal_array_key_check = false; + /** @internal */ + public int $taint_count = TaintKind::BUILTIN_TAINT_COUNT; + + /** + * @var array + */ + private array $taint_map = [ + 'callable' => TaintKind::INPUT_CALLABLE, + 'unserialize' => TaintKind::INPUT_UNSERIALIZE, + 'include' => TaintKind::INPUT_INCLUDE, + 'eval' => TaintKind::INPUT_EVAL, + 'ldap' => TaintKind::INPUT_LDAP, + 'sql' => TaintKind::INPUT_SQL, + 'html' => TaintKind::INPUT_HTML, + 'has_quotes' => TaintKind::INPUT_HAS_QUOTES, + 'shell' => TaintKind::INPUT_SHELL, + 'ssrf' => TaintKind::INPUT_SSRF, + 'file' => TaintKind::INPUT_FILE, + 'cookie' => TaintKind::INPUT_COOKIE, + 'header' => TaintKind::INPUT_HEADER, + 'xpath' => TaintKind::INPUT_XPATH, + 'sleep' => TaintKind::INPUT_SLEEP, + 'extract' => TaintKind::INPUT_EXTRACT, + 'user_secret' => TaintKind::USER_SECRET, + 'system_secret' => TaintKind::SYSTEM_SECRET, + + 'input' => TaintKind::ALL_INPUT, + 'tainted' => TaintKind::ALL_INPUT, + ]; + + /** + * @internal + * @var array + */ + public array $custom_taints = []; + /** @internal */ public function __construct( public Config $config, @@ -319,6 +358,48 @@ public function __construct( $this->loadAnalyzer(); } + /** + * Used to register a taint, or to fetch the ID of an already registered taint by its alias. + * + * @throws AssertionError if no more taint slots are left + * @throws RuntimeException if the passed alias uses some unregistered taint slots + * @param ?int $alias Used to register an alias of one or more pre-existing taints. + */ + public function getOrRegisterTaint(string $taint_type, ?int $alias = null): int + { + if (isset($this->taint_map[$taint_type])) { + return $this->taint_map[$taint_type]; + } + if ($alias === null) { + if ($this->taint_count+1 === (PHP_INT_SIZE * 8)) { + if (PHP_INT_SIZE === 8) { + throw new RuntimeException("No more taint slots left, please register fewer taints or use some of the built-in taints!"); + } + throw new RuntimeException("No more taint slots left, please switch to a 64-bit build of PHP to get 32 more taint slots, or register fewer taints or use some of the built-in taints!"); + } + $id = 1 << ($this->taint_count++); + $this->custom_taints[$id] + $taint_type; + } else { + if ($this->taint_count+1 !== (PHP_INT_SIZE * 8)) { + $mask = (1 << $this->taint_count) - 1; + if ($alias & ~$mask) { + throw new AssertionError("The passed alias $alias uses some not yet registered taint slots!"); + } + } + $id = $alias; + } + $this->taint_map[$taint_type] = $id; + return $id; + } + + /** + * Used to to fetch the ID of an already registered taint by its alias, or null if no taint is registered for the alias. + */ + public function getTaint(string $taint_type): ?int + { + return $this->taint_map[$taint_type] ?? null; + } + private function loadAnalyzer(): void { $this->analyzer = new Analyzer( @@ -2145,7 +2226,7 @@ public function queueClassLikeForScanning( public function addTaintSource( Union $expr_type, string $taint_id, - int $taints = TaintKindGroup::ALL_INPUT, + int $taints = TaintKind::ALL_INPUT, ?CodeLocation $code_location = null, ): Union { if (!$this->taint_flow_graph) { @@ -2167,7 +2248,7 @@ public function addTaintSource( public function addTaintSink( string $taint_id, - int $taints = TaintKindGroup::ALL_INPUT, + int $taints = TaintKind::ALL_INPUT, ?CodeLocation $code_location = null, ): void { if (!$this->taint_flow_graph) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 12abcaae1da..53e44637cbc 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -21,6 +21,8 @@ use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeExpander; +use Psalm\Issue\InvalidDocblock; +use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Plugin\EventHandler\Event\AfterFunctionCallAnalysisEvent; use Psalm\Storage\FunctionLikeStorage; @@ -39,7 +41,6 @@ use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TString; use Psalm\Type\TaintKind; -use Psalm\Type\TaintKindGroup; use Psalm\Type\Union; use UnexpectedValueException; @@ -591,7 +592,15 @@ private static function taintReturnType( if (!$expanded_type->isNullable()) { foreach ($expanded_type->getLiteralStrings() as $literal_string) { - $conditionally_removed_taints |= TaintKindGroup::NAME_TO_TAINT[$literal_string->value]; + $taint = $codebase->getTaint($literal_string->value); + if ($taint === null) { + IssueBuffer::maybeAdd(new InvalidDocblock( + "Invalid taint name {$literal_string->value} provided", + $function_storage->location, + )); + continue; + } + $conditionally_removed_taints |= $taint; } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php index ef3027d3049..5a3e77ad5f4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -34,7 +34,6 @@ use Throwable; use UnexpectedValueException; -use function array_filter; use function count; use function in_array; use function strtolower; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index a25718f8fda..fa032e0ea03 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -20,6 +20,7 @@ use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeExpander; +use Psalm\Issue\InvalidDocblock; use Psalm\Issue\NonStaticSelfCall; use Psalm\Issue\ParentNotFound; use Psalm\IssueBuffer; @@ -27,7 +28,6 @@ use Psalm\Storage\MethodStorage; use Psalm\Type; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\TaintKindGroup; use Psalm\Type\Union; use function count; @@ -330,7 +330,15 @@ public static function taintReturnType( ); foreach ($expanded_type->getLiteralStrings() as $literal_string) { - $conditionally_removed_taints |= TaintKindGroup::NAME_TO_TAINT[$literal_string->value]; + $taint = $codebase->getTaint($literal_string->value); + if ($taint === null) { + IssueBuffer::maybeAdd(new InvalidDocblock( + "Invalid taint name {$literal_string->value} provided", + $method_storage->location, + )); + continue; + } + $conditionally_removed_taints |= $taint; } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 06a1d0a56d5..f2e4fbc33a9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -34,7 +34,7 @@ use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TString; -use Psalm\Type\TaintKindGroup; +use Psalm\Type\TaintKind; use Psalm\Type\Union; use function in_array; @@ -509,7 +509,7 @@ private static function taintVariable( || $var_name === '$_COOKIE' || $var_name === '$_REQUEST' ) { - $taints = TaintKindGroup::ALL_INPUT; + $taints = TaintKind::ALL_INPUT; } else { $taints = 0; } diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 121d70e15ec..abc081f4a61 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -6,6 +6,7 @@ use Override; use Psalm\CodeLocation; +use Psalm\Codebase; use Psalm\Config; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\DataFlow\DataFlowNode; @@ -34,7 +35,6 @@ use Psalm\Progress\Phase; use Psalm\Progress\Progress; use Psalm\Type\TaintKind; -use Psalm\Type\TaintKindGroup; use function count; use function end; @@ -215,6 +215,8 @@ public function connectSinksAndSources(Progress $progress): void $project_analyzer = ProjectAnalyzer::getInstance(); + $codebase = $project_analyzer->getCodebase(); + // Remove all specializations without an outgoing edge foreach ($this->specializations as $k => &$map) { foreach ($map as $kk => $specialized_id) { @@ -250,6 +252,7 @@ public function connectSinksAndSources(Progress $progress): void $visited_source_ids, $config, $project_analyzer, + $codebase, ); continue; } @@ -275,6 +278,7 @@ public function connectSinksAndSources(Progress $progress): void $visited_source_ids, $config, $project_analyzer, + $codebase, ); // If this node has first level specializations (=> is first-level & unspecialized), @@ -299,6 +303,7 @@ public function connectSinksAndSources(Progress $progress): void $visited_source_ids, $config, $project_analyzer, + $codebase, ); } } else { @@ -316,11 +321,12 @@ public function connectSinksAndSources(Progress $progress): void $visited_source_ids, $config, $project_analyzer, + $codebase, ); } } } else { - // Process all descendants + // Process all descendants foreach ($source->processing_specialized_descendants_of as $map) { if (isset($map[$source->id])) { $specialized_id = $map[$source->id]; @@ -339,6 +345,7 @@ public function connectSinksAndSources(Progress $progress): void $visited_source_ids, $config, $project_analyzer, + $codebase, ); } } @@ -365,6 +372,7 @@ private function getChildNodes( array $visited_source_ids, Config $config, ProjectAnalyzer $project_analyzer, + Codebase $codebase, ): void { foreach ($this->forward_edges[$generated_source->id] as $to_id => $path) { if (!isset($this->nodes[$to_id])) { @@ -415,11 +423,12 @@ private function getChildNodes( $path = $this->getPredecessorPath($generated_source) . ' -> ' . $this->getSuccessorPath($sink); - foreach (TaintKindGroup::TAINT_TO_NAME as $matching_taint => $_) { - if (!($matching_taints & $matching_taint)) { + for ($x = $codebase->taint_count-1; $x >= 0; $x--) { + $t = 1 << $x; + if (!($matching_taints & $t)) { continue; } - $issue = match ($matching_taint) { + $issue = match ($t) { TaintKind::INPUT_CALLABLE => new TaintedCallable( 'Detected tainted text', $issue_location, @@ -529,7 +538,7 @@ private function getChildNodes( $path, ), default => new TaintedCustom( - 'Detected tainted ' . $matching_taint, + 'Detected tainted ' . $codebase->custom_taints[$t], $issue_location, $issue_trace, $path, diff --git a/src/Psalm/Internal/PreloaderList.php b/src/Psalm/Internal/PreloaderList.php index 105c69de95e..6c66d9d5d85 100644 --- a/src/Psalm/Internal/PreloaderList.php +++ b/src/Psalm/Internal/PreloaderList.php @@ -1755,7 +1755,6 @@ final class PreloaderList { \Psalm\Type\MutableUnion::class, \Psalm\Type\Reconciler::class, \Psalm\Type\TaintKind::class, - \Psalm\Type\TaintKindGroup::class, \Psalm\Type\TypeNode::class, \Psalm\Type\TypeVisitor::class, \Psalm\Type\Union::class, diff --git a/src/Psalm/Type/TaintKind.php b/src/Psalm/Type/TaintKind.php index 234e0643d05..6c77831c3a8 100644 --- a/src/Psalm/Type/TaintKind.php +++ b/src/Psalm/Type/TaintKind.php @@ -30,4 +30,9 @@ final class TaintKind public const INPUT_EXTRACT = (1 << 16); public const USER_SECRET = (1 << 17); public const SYSTEM_SECRET = (1 << 18); + + public const ALL_INPUT = (1 << 17) - 1; + + /** @internal Keep this synced with the above */ + public const BUILTIN_TAINT_COUNT = 19; } diff --git a/src/Psalm/Type/TaintKindGroup.php b/src/Psalm/Type/TaintKindGroup.php deleted file mode 100644 index 4bf2aebe964..00000000000 --- a/src/Psalm/Type/TaintKindGroup.php +++ /dev/null @@ -1,58 +0,0 @@ - */ - public const TAINT_TO_NAME = [ - TaintKind::INPUT_CALLABLE => 'callable', - TaintKind::INPUT_UNSERIALIZE => 'unserialize', - TaintKind::INPUT_INCLUDE => 'include', - TaintKind::INPUT_EVAL => 'eval', - TaintKind::INPUT_LDAP => 'ldap', - TaintKind::INPUT_SQL => 'sql', - TaintKind::INPUT_HTML => 'html', - TaintKind::INPUT_HAS_QUOTES => 'has_quotes', - TaintKind::INPUT_SHELL => 'shell', - TaintKind::INPUT_SSRF => 'ssrf', - TaintKind::INPUT_FILE => 'file', - TaintKind::INPUT_COOKIE => 'cookie', - TaintKind::INPUT_HEADER => 'header', - TaintKind::INPUT_XPATH => 'xpath', - TaintKind::INPUT_SLEEP => 'sleep', - TaintKind::INPUT_EXTRACT => 'extract', - TaintKind::USER_SECRET => 'user_secret', - TaintKind::SYSTEM_SECRET => 'system_secret', - ]; - /** @var array> */ - public const NAME_TO_TAINT = [ - 'callable' => TaintKind::INPUT_CALLABLE, - 'unserialize' => TaintKind::INPUT_UNSERIALIZE, - 'include' => TaintKind::INPUT_INCLUDE, - 'eval' => TaintKind::INPUT_EVAL, - 'ldap' => TaintKind::INPUT_LDAP, - 'sql' => TaintKind::INPUT_SQL, - 'html' => TaintKind::INPUT_HTML, - 'has_quotes' => TaintKind::INPUT_HAS_QUOTES, - 'shell' => TaintKind::INPUT_SHELL, - 'ssrf' => TaintKind::INPUT_SSRF, - 'file' => TaintKind::INPUT_FILE, - 'cookie' => TaintKind::INPUT_COOKIE, - 'header' => TaintKind::INPUT_HEADER, - 'xpath' => TaintKind::INPUT_XPATH, - 'sleep' => TaintKind::INPUT_SLEEP, - 'extract' => TaintKind::INPUT_EXTRACT, - 'user_secret' => TaintKind::USER_SECRET, - 'system_secret' => TaintKind::SYSTEM_SECRET, - 'input' => self::ALL_INPUT, - 'tainted' => self::ALL_INPUT, - ]; -} From a032b3d2f0c50a2000b4b15c1d70db580d0ec9eb Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 4 Mar 2025 09:19:08 +0100 Subject: [PATCH 18/42] Finalize --- .../Reflector/FunctionLikeDocblockScanner.php | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index e4237f0064f..aec54ab9f4b 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -47,7 +47,6 @@ use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\TaintKindGroup; use Psalm\Type\Union; use function array_any; @@ -361,7 +360,15 @@ public static function addDocblockInfo( $storage->taint_source_types |= $docblock_info->taint_source_types; foreach ($docblock_info->added_taints as $taint) { - $storage->added_taints |= TaintKindGroup::NAME_TO_TAINT[$taint]; + $t = $codebase->getTaint($taint); + if ($t === null) { + $storage->docblock_issues[] = new InvalidDocblock( + "Invalid or unregistered taint $taint in docblock for $cased_function_id", + new CodeLocation($file_scanner, $stmt, null, true), + ); + continue; + } + $storage->added_taints |= $t; } foreach ($docblock_info->removed_taints as $removed_taint) { @@ -381,7 +388,15 @@ public static function addDocblockInfo( $file_scanner, ); } else { - $storage->removed_taints |= TaintKindGroup::NAME_TO_TAINT[$removed_taint]; + $t = $codebase->getTaint($removed_taint); + if ($t === null) { + $storage->docblock_issues[] = new InvalidDocblock( + "Invalid or unregistered taint $taint in docblock for $cased_function_id", + new CodeLocation($file_scanner, $stmt, null, true), + ); + continue; + } + $storage->removed_taints |= $t; } } From eeff97975cc869b36aeaa089035985fe1db94047 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 4 Mar 2025 09:38:09 +0100 Subject: [PATCH 19/42] Finalize --- .../Internal/Analyzer/CommentAnalyzer.php | 20 ++++--- .../Statements/Block/ForeachAnalyzer.php | 1 + .../Expression/AssignmentAnalyzer.php | 2 + .../Call/FunctionCallReturnTypeFetcher.php | 2 +- .../Statements/Expression/YieldAnalyzer.php | 1 + .../Analyzer/Statements/GlobalAnalyzer.php | 2 +- .../Analyzer/Statements/StaticAnalyzer.php | 2 +- .../Reflector/ClassLikeNodeScanner.php | 2 + .../Reflector/ExpressionScanner.php | 2 +- .../Reflector/FunctionLikeDocblockParser.php | 53 ++++++++++++++++--- .../Reflector/FunctionLikeNodeScanner.php | 4 +- .../Internal/PhpVisitor/ReflectorVisitor.php | 1 + 12 files changed, 73 insertions(+), 19 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index 50ac39f5d66..ca0fafc3917 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -8,6 +8,7 @@ use Psalm\Aliases; use Psalm\CodeLocation; use Psalm\CodeLocation\DocblockTypeLocation; +use Psalm\Codebase; use Psalm\Context; use Psalm\DocComment; use Psalm\Exception\DocblockParseException; @@ -24,7 +25,6 @@ use Psalm\Issue\InvalidDocblock; use Psalm\Issue\MissingDocblockType; use Psalm\IssueBuffer; -use Psalm\Type\TaintKindGroup; use Psalm\Type\Union; use UnexpectedValueException; @@ -55,6 +55,7 @@ final class CommentAnalyzer * @return list */ public static function getTypeFromComment( + Codebase $codebase, PhpParser\Comment\Doc $comment, FileSource $source, Aliases $aliases, @@ -64,6 +65,7 @@ public static function getTypeFromComment( $parsed_docblock = DocComment::parsePreservingLength($comment); return self::arrayToDocblocks( + $codebase, $comment, $parsed_docblock, $source, @@ -80,6 +82,7 @@ public static function getTypeFromComment( * @throws DocblockParseException if there was a problem parsing the docblock */ public static function arrayToDocblocks( + Codebase $codebase, PhpParser\Comment\Doc $comment, ParsedDocblock $parsed_docblock, FileSource $source, @@ -190,7 +193,7 @@ public static function arrayToDocblocks( $var_comment->type_end = $type_end; $var_comment->description = $description; - self::decorateVarDocblockComment($var_comment, $parsed_docblock); + self::decorateVarDocblockComment($codebase, $var_comment, $parsed_docblock); $var_comments[] = $var_comment; } @@ -210,7 +213,7 @@ public static function arrayToDocblocks( ) { $var_comment = new VarDocblockComment(); - self::decorateVarDocblockComment($var_comment, $parsed_docblock); + self::decorateVarDocblockComment($codebase, $var_comment, $parsed_docblock); $var_comments[] = $var_comment; } @@ -219,6 +222,7 @@ public static function arrayToDocblocks( } private static function decorateVarDocblockComment( + Codebase $codebase, VarDocblockComment $var_comment, ParsedDocblock $parsed_docblock, ): void { @@ -239,11 +243,11 @@ private static function decorateVarDocblockComment( if (isset($parsed_docblock->tags['psalm-taint-escape'])) { foreach ($parsed_docblock->tags['psalm-taint-escape'] as $param) { $param = trim($param); - if (!isset(TaintKindGroup::NAME_TO_TAINT[$param])) { - // TODO error out, and add support for conditional taints - continue; + $t = $codebase->getTaint($param); + if ($t === null) { + throw new IncorrectDocblockException("Got invalid or unregistered taint type $param"); } - $var_comment->removed_taints |= TaintKindGroup::NAME_TO_TAINT[$param]; + $var_comment->removed_taints |= $t; } } @@ -423,6 +427,7 @@ public static function splitDocLine(string $return_block): array /** @return list */ public static function getVarComments( + Codebase $codebase, PhpParser\Comment\Doc $doc_comment, StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\Variable $var, @@ -444,6 +449,7 @@ public static function getVarComments( $var_comments = $codebase->config->disable_var_parsing ? [] : self::arrayToDocblocks( + $codebase, $doc_comment, $parsed_docblock, $statements_analyzer->getSource(), diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index 93165c7a509..519a5a029fb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -96,6 +96,7 @@ public static function analyze( if ($doc_comment) { try { $var_comments = CommentAnalyzer::getTypeFromComment( + $codebase, $doc_comment, $statements_analyzer->getSource(), $statements_analyzer->getSource()->getAliases(), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 89c47f040ca..1ef466b0eaa 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -627,6 +627,7 @@ private static function analyzeDocComment( $var_comments = $codebase->config->disable_var_parsing ? [] : CommentAnalyzer::getTypeFromComment( + $codebase, $doc_comment, $statements_analyzer->getSource(), $statements_analyzer->getAliases(), @@ -903,6 +904,7 @@ public static function analyzeAssignmentRef( if ($doc_comment) { try { $var_comments = CommentAnalyzer::getTypeFromComment( + $statements_analyzer->getCodebase(), $doc_comment, $statements_analyzer->getSource(), $statements_analyzer->getAliases(), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 53e44637cbc..aefa8fc0a3e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -581,7 +581,7 @@ private static function taintReturnType( ); $expanded_type = TypeExpander::expandUnion( - $statements_analyzer->getCodebase(), + $codebase, $conditionally_removed_taint, null, null, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php index c76f52b77c4..e502bd89dc3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php @@ -47,6 +47,7 @@ public static function analyze( if ($doc_comment) { try { $var_comments = CommentAnalyzer::getTypeFromComment( + $codebase, $doc_comment, $statements_analyzer, $statements_analyzer->getAliases(), diff --git a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php index bcb7f693304..66906ee22d9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php @@ -60,7 +60,7 @@ public static function analyze( $comment_type = null; if ($doc_comment) { - $var_comments = CommentAnalyzer::getVarComments($doc_comment, $statements_analyzer, $var); + $var_comments = CommentAnalyzer::getVarComments($codebase, $doc_comment, $statements_analyzer, $var); $comment_type = CommentAnalyzer::populateVarTypesFromDocblock( $var_comments, $var, diff --git a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php index 1cdd6a8b8a2..8aa37bebb5a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php @@ -52,7 +52,7 @@ public static function analyze( $comment_type = null; if ($doc_comment) { - $var_comments = CommentAnalyzer::getVarComments($doc_comment, $statements_analyzer, $var->var); + $var_comments = CommentAnalyzer::getVarComments($codebase, $doc_comment, $statements_analyzer, $var->var); $comment_type = CommentAnalyzer::populateVarTypesFromDocblock( $var_comments, $var->var, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 01c2a48309f..8ccf525ba09 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -1278,6 +1278,7 @@ private function visitClassConstDeclaration( try { $var_comments = CommentAnalyzer::getTypeFromComment( + $this->codebase, $comment, $this->file_scanner, $this->aliases, @@ -1590,6 +1591,7 @@ private function visitPropertyDeclaration( try { $var_comments = CommentAnalyzer::getTypeFromComment( + $this->codebase, $comment, $this->file_scanner, $this->aliases, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php index e203e469204..2cd9c336e5c 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php @@ -162,7 +162,7 @@ private static function registerClassMapFunctionCall( $doc_comment = $second_arg_value->getDocComment(); if ($doc_comment) { try { - $var_comments = CommentAnalyzer::getTypeFromComment($doc_comment, $file_scanner, $aliases); + $var_comments = CommentAnalyzer::getTypeFromComment($codebase, $doc_comment, $file_scanner, $aliases); foreach ($var_comments as $var_comment) { if ($var_comment->type) { $const_type = $var_comment->type; diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 9141f0ff23f..1225b5b4ae7 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -7,6 +7,7 @@ use AssertionError; use PhpParser; use Psalm\CodeLocation; +use Psalm\Codebase; use Psalm\DocComment; use Psalm\Exception\DocblockParseException; use Psalm\Exception\IncorrectDocblockException; @@ -16,7 +17,6 @@ use Psalm\Internal\Scanner\ParsedDocblock; use Psalm\Issue\InvalidDocblock; use Psalm\IssueBuffer; -use Psalm\Type\TaintKindGroup; use function array_keys; use function array_shift; @@ -53,6 +53,7 @@ final class FunctionLikeDocblockParser * @throws DocblockParseException if there was a problem parsing the docblock */ public static function parse( + Codebase $codebase, PhpParser\Comment\Doc $comment, CodeLocation $code_location, string $cased_function_id, @@ -246,7 +247,17 @@ public static function parse( } if (count($param_parts) >= 2) { - $info->taint_sink_params[] = ['name' => $param_parts[1], 'taint' => TaintKindGroup::NAME_TO_TAINT[$param_parts[0]]]; + $t = $codebase->getTaint($param_parts[0]); + if ($t === null) { + IssueBuffer::maybeAdd( + new InvalidDocblock( + "Got invalid or unregistered taint type {$param_parts[0]}", + $code_location, + ), + ); + } else { + $info->taint_sink_params[] = ['name' => $param_parts[1], 'taint' => $t]; + } } else { IssueBuffer::maybeAdd( new InvalidDocblock( @@ -271,9 +282,17 @@ public static function parse( if (str_starts_with($taint_type, 'exec_')) { $taint_type = substr($taint_type, 5); - $taint_type = TaintKindGroup::NAME_TO_TAINT[$taint_type]; - - $info->taint_sink_params[] = ['name' => $param_parts[0], 'taint' => $taint_type]; + $t = $codebase->getTaint($taint_type); + if ($t === null) { + IssueBuffer::maybeAdd( + new InvalidDocblock( + "Got invalid or unregistered taint type $taint_type", + $code_location, + ), + ); + } else { + $info->taint_sink_params[] = ['name' => $param_parts[0], 'taint' => $t]; + } } } } @@ -287,7 +306,17 @@ public static function parse( } if ($param_parts[0]) { - $info->taint_source_types |= TaintKindGroup::NAME_TO_TAINT[$param_parts[0]]; + $t = $codebase->getTaint($param_parts[0]); + if ($t === null) { + IssueBuffer::maybeAdd( + new InvalidDocblock( + "Got invalid or unregistered taint type {$param_parts[0]}", + $code_location, + ), + ); + } else { + $info->taint_source_types |= $t; + } } else { IssueBuffer::maybeAdd( new InvalidDocblock( @@ -307,7 +336,17 @@ public static function parse( if ($param_parts[0]) { if ($param_parts[0] !== 'none') { - $info->taint_source_types |= TaintKindGroup::NAME_TO_TAINT[$param_parts[0]]; + $t = $codebase->getTaint($param_parts[0]); + if ($t === null) { + IssueBuffer::maybeAdd( + new InvalidDocblock( + "Got invalid or unregistered taint type {$param_parts[0]}", + $code_location, + ), + ); + } else { + $info->taint_source_types |= $t; + } } } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index 15a1787c9ba..1d2eca853c3 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -422,7 +422,7 @@ public function start( if ($doc_comment) { try { $code_location = new CodeLocation($this->file_scanner, $stmt, null, true); - $docblock_info = FunctionLikeDocblockParser::parse($doc_comment, $code_location, $cased_function_id); + $docblock_info = FunctionLikeDocblockParser::parse($this->codebase, $doc_comment, $code_location, $cased_function_id); } catch (IncorrectDocblockException $e) { $storage->docblock_issues[] = new MissingDocblockType( $e->getMessage() . ' in docblock for ' . $cased_function_id, @@ -577,6 +577,7 @@ public function start( ; $var_comments = CommentAnalyzer::getTypeFromComment( + $this->codebase, $doc_comment, $this->file_scanner, $this->aliases, @@ -1023,6 +1024,7 @@ private function createStorageForFunctionLike( try { $code_location = new CodeLocation($this->file_scanner, $stmt, null, true); $docblock_info = FunctionLikeDocblockParser::parse( + $this->codebase, $doc_comment, $code_location, $cased_function_id, diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index 76b44d189a1..9c9c74e7b4a 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -368,6 +368,7 @@ public function enterNode(PhpParser\Node $node): ?int try { $var_comments = CommentAnalyzer::getTypeFromComment( + $this->codebase, $doc_comment, $this->file_scanner, $this->aliases, From a4f8a5615b819f869373efe37b2e66975157c993 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 4 Mar 2025 10:08:06 +0100 Subject: [PATCH 20/42] Finalize --- src/Psalm/Codebase.php | 60 ++++++++++++------- .../Internal/Analyzer/CommentAnalyzer.php | 10 ++-- .../Call/FunctionCallReturnTypeFetcher.php | 13 +--- .../Expression/Call/StaticCallAnalyzer.php | 12 +--- .../Internal/Codebase/TaintFlowGraph.php | 6 +- .../Reflector/ExpressionScanner.php | 7 ++- .../Reflector/FunctionLikeDocblockParser.php | 44 +++----------- .../Reflector/FunctionLikeDocblockScanner.php | 23 +++---- .../Reflector/FunctionLikeNodeScanner.php | 7 ++- 9 files changed, 81 insertions(+), 101 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index acec45dd12c..4379b642eb4 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -51,6 +51,7 @@ use Psalm\Internal\Provider\Providers; use Psalm\Internal\Provider\StatementsProvider; use Psalm\Internal\Type\Comparator\UnionTypeComparator; +use Psalm\Issue\InvalidDocblock; use Psalm\Progress\Progress; use Psalm\Progress\VoidProgress; use Psalm\Storage\ClassLikeStorage; @@ -361,43 +362,58 @@ public function __construct( /** * Used to register a taint, or to fetch the ID of an already registered taint by its alias. * - * @throws AssertionError if no more taint slots are left - * @throws RuntimeException if the passed alias uses some unregistered taint slots - * @param ?int $alias Used to register an alias of one or more pre-existing taints. + * Returns null and emits an issue if a code location is passed and there are no more taint slots. + * + * @throws RuntimeException if no code location is passed and there are no more taint slots. + * @return ($location is null ? int|null : int) */ - public function getOrRegisterTaint(string $taint_type, ?int $alias = null): int + public function getOrRegisterTaint(string $taint_type, ?CodeLocation $location = null): ?int { if (isset($this->taint_map[$taint_type])) { return $this->taint_map[$taint_type]; } - if ($alias === null) { - if ($this->taint_count+1 === (PHP_INT_SIZE * 8)) { - if (PHP_INT_SIZE === 8) { - throw new RuntimeException("No more taint slots left, please register fewer taints or use some of the built-in taints!"); - } - throw new RuntimeException("No more taint slots left, please switch to a 64-bit build of PHP to get 32 more taint slots, or register fewer taints or use some of the built-in taints!"); + if ($this->taint_count+1 === (PHP_INT_SIZE * 8)) { + $taints = implode(',', $this->custom_taints); + $err = "No more taint slots left (using $taints), "; + if (PHP_INT_SIZE === 8) { + $err .= "please use fewer custom taints and use some of the built-in taints!"; + } else { + $err .= "please switch to a 64-bit build of PHP to get 32 more taint slots,". + " or use fewer custom taints and use some of the built-in taints!"; } - $id = 1 << ($this->taint_count++); - $this->custom_taints[$id] + $taint_type; - } else { - if ($this->taint_count+1 !== (PHP_INT_SIZE * 8)) { - $mask = (1 << $this->taint_count) - 1; - if ($alias & ~$mask) { - throw new AssertionError("The passed alias $alias uses some not yet registered taint slots!"); - } + if ($location !== null) { + IssueBuffer::maybeAdd(new InvalidDocblock($err, $location)); + return null; } - $id = $alias; + throw new RuntimeException($err); } + $id = 1 << ($this->taint_count++); + $this->custom_taints[$id] = $taint_type; $this->taint_map[$taint_type] = $id; return $id; } /** - * Used to to fetch the ID of an already registered taint by its alias, or null if no taint is registered for the alias. + * Register an alias taint name based on one or more pre-existing taints. + * + * @throws AssertionError if the passed taint is already registered or if the alias uses some unregistered taints. */ - public function getTaint(string $taint_type): ?int + public function registerTaintAlias(string $taint_type, int $alias): int { - return $this->taint_map[$taint_type] ?? null; + if (isset($this->taint_map[$taint_type])) { + throw new AssertionError("A taint called $taint_type is already registered!"); + } + + if ($this->taint_count+1 !== (PHP_INT_SIZE * 8)) { + $mask = (1 << $this->taint_count) - 1; + if ($alias & ~$mask) { + throw new AssertionError("The passed alias $alias uses some not yet registered taint slots!"); + } + } + + $this->taint_map[$taint_type] = $alias; + + return $alias; } private function loadAnalyzer(): void diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index ca0fafc3917..f026436cbc0 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -26,6 +26,7 @@ use Psalm\Issue\MissingDocblockType; use Psalm\IssueBuffer; use Psalm\Type\Union; +use RuntimeException; use UnexpectedValueException; use function count; @@ -243,11 +244,12 @@ private static function decorateVarDocblockComment( if (isset($parsed_docblock->tags['psalm-taint-escape'])) { foreach ($parsed_docblock->tags['psalm-taint-escape'] as $param) { $param = trim($param); - $t = $codebase->getTaint($param); - if ($t === null) { - throw new IncorrectDocblockException("Got invalid or unregistered taint type $param"); + try { + $t = $codebase->getOrRegisterTaint($param); + $var_comment->removed_taints |= $t; + } catch (RuntimeException $e) { + throw new DocblockParseException($e->getMessage(), 0, $e); } - $var_comment->removed_taints |= $t; } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index aefa8fc0a3e..670a77e2b2f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -21,8 +21,6 @@ use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeExpander; -use Psalm\Issue\InvalidDocblock; -use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Plugin\EventHandler\Event\AfterFunctionCallAnalysisEvent; use Psalm\Storage\FunctionLikeStorage; @@ -592,15 +590,10 @@ private static function taintReturnType( if (!$expanded_type->isNullable()) { foreach ($expanded_type->getLiteralStrings() as $literal_string) { - $taint = $codebase->getTaint($literal_string->value); - if ($taint === null) { - IssueBuffer::maybeAdd(new InvalidDocblock( - "Invalid taint name {$literal_string->value} provided", - $function_storage->location, - )); - continue; + $taint = $codebase->getOrRegisterTaint($literal_string->value, $function_storage->location); + if ($taint !== null) { + $conditionally_removed_taints |= $taint; } - $conditionally_removed_taints |= $taint; } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index fa032e0ea03..8d097e27c03 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -20,7 +20,6 @@ use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeExpander; -use Psalm\Issue\InvalidDocblock; use Psalm\Issue\NonStaticSelfCall; use Psalm\Issue\ParentNotFound; use Psalm\IssueBuffer; @@ -330,15 +329,10 @@ public static function taintReturnType( ); foreach ($expanded_type->getLiteralStrings() as $literal_string) { - $taint = $codebase->getTaint($literal_string->value); - if ($taint === null) { - IssueBuffer::maybeAdd(new InvalidDocblock( - "Invalid taint name {$literal_string->value} provided", - $method_storage->location, - )); - continue; + $taint = $codebase->getOrRegisterTaint($literal_string->value, $method_storage->location); + if ($taint !== null) { + $conditionally_removed_taints |= $taint; } - $conditionally_removed_taints |= $taint; } } } diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index abc081f4a61..7aa351a3767 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -268,7 +268,8 @@ public function connectSinksAndSources(Progress $progress): void $generated_source = clone $source; $generated_source->id = $source->unspecialized_id; $generated_source->is_first_level_and_specialized = false; - $generated_source->processing_specialized_descendants_of[$source->specialization_key][$source->unspecialized_id] = $source->id; + $generated_source->processing_specialized_descendants_of + [$source->specialization_key][$source->unspecialized_id] = $source->id; $this->getChildNodes( $new_sources, @@ -562,7 +563,8 @@ private function getChildNodes( $new_destination->is_first_level_and_specialized = false; $new_destination->taintSource = $generated_source; $new_destination->taints = $new_taints; - $new_destination->processing_specialized_descendants_of = $generated_source->processing_specialized_descendants_of; + $new_destination->processing_specialized_descendants_of + = $generated_source->processing_specialized_descendants_of; $new_destination->path_types = [...$generated_source->path_types, $path_type]; $new_sources[$key] = $new_destination; diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php index 2cd9c336e5c..2b44c5c4e12 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php @@ -162,7 +162,12 @@ private static function registerClassMapFunctionCall( $doc_comment = $second_arg_value->getDocComment(); if ($doc_comment) { try { - $var_comments = CommentAnalyzer::getTypeFromComment($codebase, $doc_comment, $file_scanner, $aliases); + $var_comments = CommentAnalyzer::getTypeFromComment( + $codebase, + $doc_comment, + $file_scanner, + $aliases, + ); foreach ($var_comments as $var_comment) { if ($var_comment->type) { $const_type = $var_comment->type; diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 1225b5b4ae7..8dbbdcc99ef 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -247,15 +247,8 @@ public static function parse( } if (count($param_parts) >= 2) { - $t = $codebase->getTaint($param_parts[0]); - if ($t === null) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - "Got invalid or unregistered taint type {$param_parts[0]}", - $code_location, - ), - ); - } else { + $t = $codebase->getOrRegisterTaint($param_parts[0], $code_location); + if ($t !== null) { $info->taint_sink_params[] = ['name' => $param_parts[1], 'taint' => $t]; } } else { @@ -282,15 +275,8 @@ public static function parse( if (str_starts_with($taint_type, 'exec_')) { $taint_type = substr($taint_type, 5); - $t = $codebase->getTaint($taint_type); - if ($t === null) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - "Got invalid or unregistered taint type $taint_type", - $code_location, - ), - ); - } else { + $t = $codebase->getOrRegisterTaint($taint_type, $code_location); + if ($t !== null) { $info->taint_sink_params[] = ['name' => $param_parts[0], 'taint' => $t]; } } @@ -306,15 +292,8 @@ public static function parse( } if ($param_parts[0]) { - $t = $codebase->getTaint($param_parts[0]); - if ($t === null) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - "Got invalid or unregistered taint type {$param_parts[0]}", - $code_location, - ), - ); - } else { + $t = $codebase->getOrRegisterTaint($param_parts[0], $code_location); + if ($t !== null) { $info->taint_source_types |= $t; } } else { @@ -336,15 +315,8 @@ public static function parse( if ($param_parts[0]) { if ($param_parts[0] !== 'none') { - $t = $codebase->getTaint($param_parts[0]); - if ($t === null) { - IssueBuffer::maybeAdd( - new InvalidDocblock( - "Got invalid or unregistered taint type {$param_parts[0]}", - $code_location, - ), - ); - } else { + $t = $codebase->getOrRegisterTaint($param_parts[0], $code_location); + if ($t !== null) { $info->taint_source_types |= $t; } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index aec54ab9f4b..a7d76d50257 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -358,17 +358,13 @@ public static function addDocblockInfo( // merge taints from doc block to storage, enforce uniqueness and having consecutive index keys $storage->taint_source_types |= $docblock_info->taint_source_types; + $location = new CodeLocation($file_scanner, $stmt, null, true); foreach ($docblock_info->added_taints as $taint) { - $t = $codebase->getTaint($taint); - if ($t === null) { - $storage->docblock_issues[] = new InvalidDocblock( - "Invalid or unregistered taint $taint in docblock for $cased_function_id", - new CodeLocation($file_scanner, $stmt, null, true), - ); - continue; + $t = $codebase->getOrRegisterTaint($taint, $location); + if ($t !== null) { + $storage->added_taints |= $t; } - $storage->added_taints |= $t; } foreach ($docblock_info->removed_taints as $removed_taint) { @@ -388,15 +384,10 @@ public static function addDocblockInfo( $file_scanner, ); } else { - $t = $codebase->getTaint($removed_taint); - if ($t === null) { - $storage->docblock_issues[] = new InvalidDocblock( - "Invalid or unregistered taint $taint in docblock for $cased_function_id", - new CodeLocation($file_scanner, $stmt, null, true), - ); - continue; + $t = $codebase->getOrRegisterTaint($removed_taint, $location); + if ($t !== null) { + $storage->removed_taints |= $t; } - $storage->removed_taints |= $t; } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index 1d2eca853c3..8829d04dfc7 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -422,7 +422,12 @@ public function start( if ($doc_comment) { try { $code_location = new CodeLocation($this->file_scanner, $stmt, null, true); - $docblock_info = FunctionLikeDocblockParser::parse($this->codebase, $doc_comment, $code_location, $cased_function_id); + $docblock_info = FunctionLikeDocblockParser::parse( + $this->codebase, + $doc_comment, + $code_location, + $cased_function_id, + ); } catch (IncorrectDocblockException $e) { $storage->docblock_issues[] = new MissingDocblockType( $e->getMessage() . ' in docblock for ' . $cased_function_id, From 117993094efc834e1b26aea14d86913088b382ac Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 4 Mar 2025 10:20:19 +0100 Subject: [PATCH 21/42] Bump docs --- .../security_analysis/custom_taint_sources.md | 48 +++++++++++++++++-- .../Internal/Analyzer/CommentAnalyzer.php | 3 +- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/docs/security_analysis/custom_taint_sources.md b/docs/security_analysis/custom_taint_sources.md index a65decc66b0..e980bdadb9d 100644 --- a/docs/security_analysis/custom_taint_sources.md +++ b/docs/security_analysis/custom_taint_sources.md @@ -26,29 +26,69 @@ For example this plugin treats all variables named `$bad_data` as taint sources. namespace Psalm\Example\Plugin; use PhpParser\Node\Expr\Variable; +use Psalm\Codebase; use Psalm\Plugin\EventHandler\AddTaintsInterface; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Type\TaintKind; -use Psalm\Type\TaintKindGroup; /** - * Add input taints to all variables named 'bad_data' + * Add input taints to all variables named 'bad_data' or 'even_badder_data'. + * + * RemoveTaintsInterface is also available to remove taints. */ class TaintBadDataPlugin implements AddTaintsInterface { + private static int $myCustomTaint; + private static int $myCustomTaintAlias; + /** + * Must be called by the PluginEntryPointInterface (__invoke) of your plugin. + */ + public static function init(Codebase $codebase): void + { + // Register a new custom taint + // The taint name may be used in @psalm-taint-* annotations in the code. + self::$myCustomTaint = $codebase->getOrRegisterTaint("my_custom_taint"); + + // Register a taint alias that combines multiple pre-registered taint types + // Taint alias names may be used in @psalm-taint-* annotations in the code. + self::$myCustomTaintAlias = $codebase->registerTaintAlias( + "my_custom_taint_alias", + self::$myCustomTaint | TaintKind::ALL_INPUT + ); + } + /** * Called to see what taints should be added * - * @return int-mask-of + * @return int A bitmap of taint from the IDs */ public static function addTaints(AddRemoveTaintsEvent $event): int { $expr = $event->getExpr(); if ($expr instanceof Variable && $expr->name === 'bad_data') { - return TaintKindGroup::ALL_INPUT; + return TaintKind::ALL_INPUT; + } + + if ($expr instanceof Variable && $expr->name === 'even_badder_data') { + return self::$myCustomTaint; + } + + if ($expr instanceof Variable && $expr->name === 'even_badder_data_2') { + return self::$myCustomTaintAlias; + } + + if ($expr instanceof Variable && $expr->name === 'secret_even_badder_data_3') { + // Combine taints using | + return self::$myCustomTaintAlias | USER_SECRET; + } + + if ($expr instanceof Variable && $expr->name === 'bad_data_but_ok_cookie') { + // Remove taints using & and ~ to negate a taint (group) + return self::$myCustomTaintAlias & ~TaintKind::INPUT_COOKIE; } + // No taints return 0; } } diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index f026436cbc0..beb26f47cef 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -245,8 +245,7 @@ private static function decorateVarDocblockComment( foreach ($parsed_docblock->tags['psalm-taint-escape'] as $param) { $param = trim($param); try { - $t = $codebase->getOrRegisterTaint($param); - $var_comment->removed_taints |= $t; + $var_comment->removed_taints |= $codebase->getOrRegisterTaint($param); } catch (RuntimeException $e) { throw new DocblockParseException($e->getMessage(), 0, $e); } From b01faad67c98f6715428d1d5dfac8d197d2fb813 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 4 Mar 2025 10:36:28 +0100 Subject: [PATCH 22/42] Finalize --- examples/plugins/TaintActiveRecords.php | 3 ++- src/Psalm/Codebase.php | 4 ++-- src/Psalm/Internal/Analyzer/CommentAnalyzer.php | 1 - .../Internal/Analyzer/Statements/GlobalAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/ReturnAnalyzer.php | 1 + .../Internal/Analyzer/Statements/StaticAnalyzer.php | 2 +- src/Psalm/Internal/Analyzer/StatementsAnalyzer.php | 1 + tests/CommentAnalyzerTest.php | 12 +++++++----- .../EventHandler/AddTaints/TaintBadDataPlugin.php | 2 +- .../RemoveTaints/RemoveAllTaintsPlugin.php | 4 +--- tests/FunctionLikeDocblockParserTest.php | 5 +++++ 11 files changed, 22 insertions(+), 15 deletions(-) diff --git a/examples/plugins/TaintActiveRecords.php b/examples/plugins/TaintActiveRecords.php index 49380624ad8..a1da7623158 100644 --- a/examples/plugins/TaintActiveRecords.php +++ b/examples/plugins/TaintActiveRecords.php @@ -12,6 +12,7 @@ use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Type\Atomic; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\TaintKind; use Psalm\Type\TaintKindGroup; use Psalm\Type\Union; @@ -51,7 +52,7 @@ public static function addTaints(AddRemoveTaintsEvent $event): int } if (self::containsActiveRecord($expr_type)) { - return TaintKindGroup::ALL_INPUT; + return TaintKind::ALL_INPUT; } } while ($expr = self::getParentNode($expr)); diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 4379b642eb4..7e02213310d 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -363,9 +363,9 @@ public function __construct( * Used to register a taint, or to fetch the ID of an already registered taint by its alias. * * Returns null and emits an issue if a code location is passed and there are no more taint slots. - * * @throws RuntimeException if no code location is passed and there are no more taint slots. - * @return ($location is null ? int|null : int) + * + * @return ($location is null ? int : int|null) */ public function getOrRegisterTaint(string $taint_type, ?CodeLocation $location = null): ?int { diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index beb26f47cef..75364dc7cff 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -428,7 +428,6 @@ public static function splitDocLine(string $return_block): array /** @return list */ public static function getVarComments( - Codebase $codebase, PhpParser\Comment\Doc $doc_comment, StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\Variable $var, diff --git a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php index 66906ee22d9..bcb7f693304 100644 --- a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php @@ -60,7 +60,7 @@ public static function analyze( $comment_type = null; if ($doc_comment) { - $var_comments = CommentAnalyzer::getVarComments($codebase, $doc_comment, $statements_analyzer, $var); + $var_comments = CommentAnalyzer::getVarComments($doc_comment, $statements_analyzer, $var); $comment_type = CommentAnalyzer::populateVarTypesFromDocblock( $var_comments, $var, diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index 4c1336755de..99323d647ea 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -81,6 +81,7 @@ public static function analyze( $var_comments = $codebase->config->disable_var_parsing ? [] : CommentAnalyzer::arrayToDocblocks( + $statements_analyzer->getCodebase(), $doc_comment, $parsed_docblock, $statements_analyzer->getSource(), diff --git a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php index 8aa37bebb5a..1cdd6a8b8a2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/StaticAnalyzer.php @@ -52,7 +52,7 @@ public static function analyze( $comment_type = null; if ($doc_comment) { - $var_comments = CommentAnalyzer::getVarComments($codebase, $doc_comment, $statements_analyzer, $var->var); + $var_comments = CommentAnalyzer::getVarComments($doc_comment, $statements_analyzer, $var->var); $comment_type = CommentAnalyzer::populateVarTypesFromDocblock( $var_comments, $var->var, diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 58063919037..e1ae74e6695 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -442,6 +442,7 @@ private static function analyzeStatement( $var_comments = $codebase->config->disable_var_parsing ? [] : CommentAnalyzer::arrayToDocblocks( + $codebase, $docblock, $statements_analyzer->parsed_docblock, $statements_analyzer->getSource(), diff --git a/tests/CommentAnalyzerTest.php b/tests/CommentAnalyzerTest.php index c3e15fb98c1..75a175a3b33 100644 --- a/tests/CommentAnalyzerTest.php +++ b/tests/CommentAnalyzerTest.php @@ -8,7 +8,9 @@ use PHPUnit\Framework\TestCase as BaseTestCase; use PhpParser\Comment\Doc; use Psalm\Aliases; +use Psalm\Codebase; use Psalm\Internal\Analyzer\CommentAnalyzer; +use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\RuntimeCaches; use Psalm\Internal\Scanner\FileScanner; @@ -27,7 +29,7 @@ public function testDocblockVarDescription(): void */ '; $php_parser_doc = new Doc($doc); - $comment_docblock = CommentAnalyzer::getTypeFromComment($php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); + $comment_docblock = CommentAnalyzer::getTypeFromComment(ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); $this->assertSame('Some Description', $comment_docblock[0]->description); } @@ -38,7 +40,7 @@ public function testDocblockVarDescriptionWithVarId(): void */ '; $php_parser_doc = new Doc($doc); - $comment_docblock = CommentAnalyzer::getTypeFromComment($php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); + $comment_docblock = CommentAnalyzer::getTypeFromComment(ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); $this->assertSame('Some Description', $comment_docblock[0]->description); } @@ -50,7 +52,7 @@ public function testDocblockVarDescriptionMultiline(): void */ '; $php_parser_doc = new Doc($doc); - $comment_docblock = CommentAnalyzer::getTypeFromComment($php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); + $comment_docblock = CommentAnalyzer::getTypeFromComment(ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); $this->assertSame('Some Description with a long description.', $comment_docblock[0]->description); } @@ -63,7 +65,7 @@ public function testDocblockDescription(): void */ '; $php_parser_doc = new Doc($doc); - $comment_docblock = CommentAnalyzer::getTypeFromComment($php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); + $comment_docblock = CommentAnalyzer::getTypeFromComment(ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); $this->assertSame('Some Description', $comment_docblock[0]->description); } @@ -76,7 +78,7 @@ public function testDocblockDescriptionWithVarDescription(): void */ '; $php_parser_doc = new Doc($doc); - $comment_docblock = CommentAnalyzer::getTypeFromComment($php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); + $comment_docblock = CommentAnalyzer::getTypeFromComment(ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); $this->assertSame('Use a string', $comment_docblock[0]->description); } diff --git a/tests/Config/Plugin/EventHandler/AddTaints/TaintBadDataPlugin.php b/tests/Config/Plugin/EventHandler/AddTaints/TaintBadDataPlugin.php index 54f9e97ebe8..245708765d9 100644 --- a/tests/Config/Plugin/EventHandler/AddTaints/TaintBadDataPlugin.php +++ b/tests/Config/Plugin/EventHandler/AddTaints/TaintBadDataPlugin.php @@ -32,7 +32,7 @@ public static function addTaints(AddRemoveTaintsEvent $event): int switch ($expr->name) { case 'bad_data': - return TaintKindGroup::ALL_INPUT; + return TaintKind::ALL_INPUT; case 'bad_sql': return TaintKind::INPUT_SQL; case 'bad_html': diff --git a/tests/Config/Plugin/EventHandler/RemoveTaints/RemoveAllTaintsPlugin.php b/tests/Config/Plugin/EventHandler/RemoveTaints/RemoveAllTaintsPlugin.php index e84a4dbf52d..0119f8c043b 100644 --- a/tests/Config/Plugin/EventHandler/RemoveTaints/RemoveAllTaintsPlugin.php +++ b/tests/Config/Plugin/EventHandler/RemoveTaints/RemoveAllTaintsPlugin.php @@ -17,12 +17,10 @@ final class RemoveAllTaintsPlugin implements RemoveTaintsInterface { /** * Called to see what taints should be removed - * - * @return int-mask-of */ #[Override] public static function removeTaints(AddRemoveTaintsEvent $event): int { - return TaintKindGroup::ALL_INPUT; + return TaintKind::ALL_INPUT; } } diff --git a/tests/FunctionLikeDocblockParserTest.php b/tests/FunctionLikeDocblockParserTest.php index c1fba7729af..70621f5e796 100644 --- a/tests/FunctionLikeDocblockParserTest.php +++ b/tests/FunctionLikeDocblockParserTest.php @@ -64,6 +64,7 @@ public function testDocblockDescription(): void '; $php_parser_doc = new Doc($doc); $function_docblock = FunctionLikeDocblockParser::parse( + ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, $this->test_code_location, $this->test_cased_function_id, @@ -88,6 +89,7 @@ public function testDocblockParamDescription(): void '; $php_parser_doc = new Doc($doc); $function_docblock = FunctionLikeDocblockParser::parse( + ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, $this->test_code_location, $this->test_cased_function_id, @@ -110,6 +112,7 @@ public function testMisplacedVariableOnNextLine(): void $this->expectException(IncorrectDocblockException::class); $this->expectExceptionMessage('Misplaced variable'); FunctionLikeDocblockParser::parse( + ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, $this->test_code_location, $this->test_cased_function_id, @@ -125,6 +128,7 @@ public function testPreferPsalmPrefixedAnnotationsOverPhpstanOnes(): void '; $php_parser_doc = new Doc($doc); $function_docblock = FunctionLikeDocblockParser::parse( + ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, $this->test_code_location, $this->test_cased_function_id, @@ -142,6 +146,7 @@ public function testReturnsUnexpectedTags(): void '; $php_parser_doc = new Doc($doc, 0); $function_docblock = FunctionLikeDocblockParser::parse( + ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, $this->test_code_location, $this->test_cased_function_id, From 1b83acb25581970a1cbf8d86689ee031fec53e6b Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 4 Mar 2025 10:42:22 +0100 Subject: [PATCH 23/42] Final performance fix --- src/Psalm/Codebase.php | 2 +- .../Internal/Analyzer/StatementsAnalyzer.php | 22 ++++++++----------- tests/CommentAnalyzerTest.php | 1 - .../AddTaints/TaintBadDataPlugin.php | 1 - .../RemoveTaints/RemoveAllTaintsPlugin.php | 1 - 5 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 7e02213310d..a92f81235c7 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -363,8 +363,8 @@ public function __construct( * Used to register a taint, or to fetch the ID of an already registered taint by its alias. * * Returns null and emits an issue if a code location is passed and there are no more taint slots. + * * @throws RuntimeException if no code location is passed and there are no more taint slots. - * * @return ($location is null ? int : int|null) */ public function getOrRegisterTaint(string $taint_type, ?CodeLocation $location = null): ?int diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index e1ae74e6695..071b7efeba9 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -39,7 +39,6 @@ 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; @@ -155,9 +154,7 @@ public function __construct(protected SourceAnalyzer $source, public NodeDataPro $this->file_analyzer = $source->getFileAnalyzer(); $this->codebase = $source->getCodebase(); - if ($this->codebase->taint_flow_graph) { - $this->data_flow_graph = new TaintFlowGraph(); - } elseif ($this->codebase->find_unused_variables) { + if (!$this->codebase->taint_flow_graph && $this->codebase->find_unused_variables) { $this->data_flow_graph = new VariableUseGraph(); } } @@ -177,13 +174,20 @@ public function analyze( if (!$stmts) { return null; } - // hoist functions to the top $this->hoistFunctions($stmts, $context); $project_analyzer = $this->getFileAnalyzer()->project_analyzer; $codebase = $project_analyzer->getCodebase(); + if ($this->codebase->taint_flow_graph) { + if ($root_scope && $codebase->config->trackTaintsInPath($this->getFilePath())) { + $this->data_flow_graph = $this->codebase->taint_flow_graph; + } else { + $this->data_flow_graph = null; + } + } + if ($codebase->config->hoist_constants) { self::hoistConstants($this, $stmts, $context); } @@ -215,14 +219,6 @@ public function analyze( } } - 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; } diff --git a/tests/CommentAnalyzerTest.php b/tests/CommentAnalyzerTest.php index 75a175a3b33..b9a7ae2ebf0 100644 --- a/tests/CommentAnalyzerTest.php +++ b/tests/CommentAnalyzerTest.php @@ -8,7 +8,6 @@ use PHPUnit\Framework\TestCase as BaseTestCase; use PhpParser\Comment\Doc; use Psalm\Aliases; -use Psalm\Codebase; use Psalm\Internal\Analyzer\CommentAnalyzer; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\RuntimeCaches; diff --git a/tests/Config/Plugin/EventHandler/AddTaints/TaintBadDataPlugin.php b/tests/Config/Plugin/EventHandler/AddTaints/TaintBadDataPlugin.php index 245708765d9..cc86f5f541c 100644 --- a/tests/Config/Plugin/EventHandler/AddTaints/TaintBadDataPlugin.php +++ b/tests/Config/Plugin/EventHandler/AddTaints/TaintBadDataPlugin.php @@ -9,7 +9,6 @@ use Psalm\Plugin\EventHandler\AddTaintsInterface; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Type\TaintKind; -use Psalm\Type\TaintKindGroup; /** * Add input taints to all variables named 'bad_data' diff --git a/tests/Config/Plugin/EventHandler/RemoveTaints/RemoveAllTaintsPlugin.php b/tests/Config/Plugin/EventHandler/RemoveTaints/RemoveAllTaintsPlugin.php index 0119f8c043b..cd836adc491 100644 --- a/tests/Config/Plugin/EventHandler/RemoveTaints/RemoveAllTaintsPlugin.php +++ b/tests/Config/Plugin/EventHandler/RemoveTaints/RemoveAllTaintsPlugin.php @@ -8,7 +8,6 @@ use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Plugin\EventHandler\RemoveTaintsInterface; use Psalm\Type\TaintKind; -use Psalm\Type\TaintKindGroup; /** * @psalm-suppress UnusedClass From bf39498613b38a736f427776b7e4c79b08d78d67 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 5 Mar 2025 19:13:59 +0100 Subject: [PATCH 24/42] Finalize --- .github/workflows/windows-ci.yml | 3 +- src/Psalm/Codebase.php | 7 +- .../Analyzer/Statements/EchoAnalyzer.php | 12 +- .../Statements/Expression/ArrayAnalyzer.php | 5 +- .../InstancePropertyAssignmentAnalyzer.php | 9 +- .../Expression/AssignmentAnalyzer.php | 3 +- .../Expression/BinaryOpAnalyzer.php | 3 +- .../Expression/Call/ArgumentAnalyzer.php | 3 +- .../Expression/Call/ArgumentsAnalyzer.php | 11 +- .../Expression/Call/FunctionCallAnalyzer.php | 10 +- .../Call/FunctionCallReturnTypeFetcher.php | 3 +- .../Method/MethodCallReturnTypeFetcher.php | 4 +- .../Expression/Call/NewAnalyzer.php | 9 +- .../Expression/Call/StaticCallAnalyzer.php | 7 +- .../Expression/EncapsulatedStringAnalyzer.php | 3 +- .../Statements/Expression/EvalAnalyzer.php | 10 +- .../Statements/Expression/ExitAnalyzer.php | 13 +- .../Expression/Fetch/ArrayFetchAnalyzer.php | 3 +- .../Fetch/AtomicPropertyFetchAnalyzer.php | 7 +- .../Fetch/VariableFetchAnalyzer.php | 3 +- .../Statements/Expression/IncludeAnalyzer.php | 10 +- .../Statements/Expression/PrintAnalyzer.php | 13 +- .../Internal/Codebase/TaintFlowGraph.php | 134 ++++++++++++------ .../Internal/Codebase/VariableUseGraph.php | 14 +- src/Psalm/Internal/DataFlow/DataFlowNode.php | 131 +++++++++-------- src/Psalm/Internal/DataFlow/TaintSink.php | 12 -- src/Psalm/Internal/DataFlow/TaintSource.php | 24 ---- src/Psalm/Internal/PreloaderList.php | 4 +- 28 files changed, 245 insertions(+), 225 deletions(-) delete mode 100644 src/Psalm/Internal/DataFlow/TaintSink.php delete mode 100644 src/Psalm/Internal/DataFlow/TaintSource.php diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 88696ba49d3..9bed492db43 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -59,8 +59,7 @@ jobs: #ini-values: zend.assertions=1, assert.exception=1 tools: composer:v2 coverage: none - #extensions: none, curl, dom, filter, intl, json, libxml, mbstring, openssl, opcache, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter - extensions: none, curl, dom, filter, intl, json, libxml, mbstring, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter + extensions: none, curl, dom, filter, intl, json, libxml, mbstring, openssl, opcache, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter env: fail-fast: true diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index a92f81235c7..25ff96a8c08 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -39,8 +39,7 @@ use Psalm\Internal\Codebase\Reflection; use Psalm\Internal\Codebase\Scanner; use Psalm\Internal\Codebase\TaintFlowGraph; -use Psalm\Internal\DataFlow\TaintSink; -use Psalm\Internal\DataFlow\TaintSource; +use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\LanguageServer\PHPMarkdownContent; use Psalm\Internal\LanguageServer\Reference; use Psalm\Internal\MethodIdentifier; @@ -2249,7 +2248,7 @@ public function addTaintSource( return $expr_type; } - $source = new TaintSource( + $source = DataFlowNode::make( $taint_id, $taint_id, $code_location, @@ -2271,7 +2270,7 @@ public function addTaintSink( return; } - $sink = new TaintSink( + $sink = DataFlowNode::make( $taint_id, $taint_id, $code_location, diff --git a/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php index 5e97b12c55b..ae39e946ef9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php @@ -12,7 +12,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\CastAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\TaintFlowGraph; -use Psalm\Internal\DataFlow\TaintSink; +use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Issue\ForbiddenCode; use Psalm\Issue\ImpureFunctionCall; use Psalm\IssueBuffer; @@ -57,18 +57,18 @@ public static function analyze( $call_location = new CodeLocation($statements_analyzer->getSource(), $stmt); - $echo_param_sink = TaintSink::getForMethodArgument( + $echo_param_sink = DataFlowNode::getForMethodArgument( 'echo', 'echo', (int) $i, null, $call_location, + TaintKind::INPUT_HTML + | TaintKind::INPUT_HAS_QUOTES + | TaintKind::USER_SECRET + | TaintKind::SYSTEM_SECRET ); - $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); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index c18df0dfbfa..d40959fa9e2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -14,7 +14,6 @@ use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\Codebase\VariableUseGraph; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSource; use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Internal\Type\TypeCombiner; use Psalm\Issue\DuplicateArrayKey; @@ -444,7 +443,7 @@ private static function analyzeArrayItem( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($new_parent_node, $taints); + $taint_source = $new_parent_node->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } @@ -485,7 +484,7 @@ private static function analyzeArrayItem( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($new_parent_node, $taints); + $taint_source = $new_parent_node->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php index 60255875bec..9dd239daa9a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php @@ -28,7 +28,6 @@ use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\Codebase\VariableUseGraph; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSource; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\Comparator\TypeComparisonResult; @@ -508,7 +507,7 @@ private static function taintProperty( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($property_node, $taints); + $taint_source = $property_node->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } @@ -593,7 +592,7 @@ public static function taintUnspecializedProperty( $data_flow_graph->addNode($localized_property_node); - $property_node = new DataFlowNode( + $property_node = DataFlowNode::make( $property_id, $property_id, null, @@ -609,7 +608,7 @@ public static function taintUnspecializedProperty( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($property_node, $taints); + $taint_source = $property_node->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } @@ -646,7 +645,7 @@ public static function taintUnspecializedProperty( || $stmt instanceof PhpParser\Node\Expr\StaticPropertyFetch) && $stmt->name instanceof PhpParser\Node\Identifier ) { - $declaring_property_node = new DataFlowNode( + $declaring_property_node = DataFlowNode::make( $declaring_property_class . '::$' . $stmt->name, $declaring_property_class . '::$' . $stmt->name, null, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 1ef466b0eaa..1996f1b3800 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -33,7 +33,6 @@ use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\Codebase\VariableUseGraph; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSource; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\ReferenceConstraint; use Psalm\Internal\Scanner\VarDocblockComment; @@ -805,7 +804,7 @@ private static function taintAssignment( // become a new taint source $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $data_flow_graph instanceof TaintFlowGraph) { - $data_flow_graph->addSource(TaintSource::fromNode($new_parent_node, $taints)); + $data_flow_graph->addSource($new_parent_node->setTaints($taints)); } foreach ($parent_nodes as $parent_node) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php index 79f1d10c8bb..5ceb29d6c69 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php @@ -18,7 +18,6 @@ use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\Codebase\VariableUseGraph; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSource; use Psalm\Internal\MethodIdentifier; use Psalm\Issue\DocblockTypeContradiction; use Psalm\Issue\ImpureMethodCall; @@ -174,7 +173,7 @@ public static function analyze( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($new_parent_node, $taints); + $taint_source = $new_parent_node->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index a65a31451a0..f4ee4d7862d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -22,7 +22,6 @@ use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\Codebase\VariableUseGraph; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSource; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\Comparator\CallableTypeComparator; use Psalm\Internal\Type\Comparator\TypeComparisonResult; @@ -1895,7 +1894,7 @@ private static function processTaintedness( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($argument_value_node, $taints); + $taint_source = $argument_value_node->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 986928f7793..ccb131ad97c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -21,7 +21,7 @@ use Psalm\Internal\Codebase\Functions; use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\Codebase\TaintFlowGraph; -use Psalm\Internal\DataFlow\TaintSink; +use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Stubs\Generator\StubsGenerator; use Psalm\Internal\Type\Comparator\UnionTypeComparator; @@ -900,24 +900,25 @@ public static function checkArgumentsMatch( foreach ($arg_function_params[$argument_offset] as $function_param) { if ($function_param->sinks) { if (!$function_storage || $function_storage->specialize_call) { - $sink = TaintSink::getForMethodArgument( + $sink = DataFlowNode::getForMethodArgument( $cased_method_id, $cased_method_id, $argument_offset, $function_param->location, $code_location, + $function_param->sinks, ); } else { - $sink = TaintSink::getForMethodArgument( + $sink = DataFlowNode::getForMethodArgument( $cased_method_id, $cased_method_id, $argument_offset, $function_param->location, + null, + $function_param->sinks, ); } - $sink->taints = $function_param->sinks; - $statements_analyzer->data_flow_graph->addSink($sink); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 32c77206c54..250f4178ef6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -18,8 +18,7 @@ use Psalm\Internal\Analyzer\TraitAnalyzer; use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\Codebase\TaintFlowGraph; -use Psalm\Internal\DataFlow\TaintSink; -use Psalm\Internal\DataFlow\TaintSource; +use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\Comparator\CallableTypeComparator; use Psalm\Internal\Type\TemplateResult; @@ -837,16 +836,15 @@ private static function getAnalyzeNamedExpression( ) { $arg_location = new CodeLocation($statements_analyzer->getSource(), $function_name); - $custom_call_sink = TaintSink::getForMethodArgument( + $custom_call_sink = DataFlowNode::getForMethodArgument( 'variable-call', 'variable-call', 0, $arg_location, $arg_location, + TaintKind::INPUT_CALLABLE, ); - $custom_call_sink->taints = TaintKind::INPUT_CALLABLE; - $statements_analyzer->data_flow_graph->addSink($custom_call_sink); $event = new AddRemoveTaintsEvent($stmt, $context, $statements_analyzer, $codebase); @@ -856,7 +854,7 @@ private static function getAnalyzeNamedExpression( $taints = $added_taints & ~$removed_taints; if ($taints !== 0) { - $taint_source = TaintSource::fromNode($custom_call_sink, $taints); + $taint_source = $custom_call_sink->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 670a77e2b2f..a31e8fc417a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -14,7 +14,6 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSource; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\Type\Comparator\CallableTypeComparator; use Psalm\Internal\Type\TemplateBound; @@ -749,7 +748,7 @@ public static function taintUsingStorage( $taints = $added_taints & ~$function_storage->removed_taints; if ($taints !== 0) { - $taint_source = TaintSource::fromNode($function_call_node, $taints); + $taint_source = $function_call_node->setTaints($taints); $graph->addSource($taint_source); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php index 5a3e77ad5f4..0656b8cf491 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -388,7 +388,7 @@ public static function taintMethodCallResult( null, ); - $method_call_node = new DataFlowNode( + $method_call_node = DataFlowNode::make( strtolower((string) $method_id), $cased_method_id, $is_declaring ? ($method_storage->signature_return_type_location @@ -429,7 +429,7 @@ public static function taintMethodCallResult( if (!$is_declaring) { $cased_declaring_method_id = $codebase->methods->getCasedMethodId($declaring_method_id); - $declaring_method_call_node = new DataFlowNode( + $declaring_method_call_node = DataFlowNode::make( strtolower((string) $declaring_method_id), $cased_declaring_method_id, $method_storage->signature_return_type_location ?: $method_storage->location, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 9a9e533b25b..ca82af2d00f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -20,8 +20,6 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSink; -use Psalm\Internal\DataFlow\TaintSource; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TemplateStandinTypeReplacer; @@ -724,16 +722,15 @@ private static function analyzeConstructorExpression( ) { $arg_location = new CodeLocation($statements_analyzer->getSource(), $stmt_class); - $custom_call_sink = TaintSink::getForMethodArgument( + $custom_call_sink = DataFlowNode::getForMethodArgument( 'variable-call', 'variable-call', 0, $arg_location, $arg_location, + TaintKind::INPUT_CALLABLE, ); - $custom_call_sink->taints = TaintKind::INPUT_CALLABLE; - $statements_analyzer->data_flow_graph->addSink($custom_call_sink); $event = new AddRemoveTaintsEvent($stmt, $context, $statements_analyzer, $codebase); @@ -743,7 +740,7 @@ private static function analyzeConstructorExpression( $taints = $added_taints & ~$removed_taints; if ($added_taints !== 0) { - $taint_source = TaintSource::fromNode($custom_call_sink, $taints); + $taint_source = $custom_call_sink->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index 8d097e27c03..be418b6ecc2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -15,7 +15,6 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSource; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; @@ -371,14 +370,14 @@ public static function taintReturnType( && $method_storage->taint_source_types && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph ) { - $method_node = TaintSource::getForMethodReturn( + $method_node = DataFlowNode::getForMethodReturn( (string) $method_id, $cased_method_id, $method_storage->signature_return_type_location ?: $method_storage->location, + null, + $method_storage->taint_source_types ); - $method_node->taints = $method_storage->taint_source_types; - $statements_analyzer->data_flow_graph->addSource($method_node); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php index f6c10274bf9..c13711f931d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EncapsulatedStringAnalyzer.php @@ -13,7 +13,6 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSource; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; use Psalm\Type; use Psalm\Type\Atomic\TLiteralFloat; @@ -110,7 +109,7 @@ public static function analyze( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($new_parent_node, $taints); + $taint_source = $new_parent_node->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php index bdb189186cd..70e1be60039 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/EvalAnalyzer.php @@ -10,8 +10,7 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\TaintFlowGraph; -use Psalm\Internal\DataFlow\TaintSink; -use Psalm\Internal\DataFlow\TaintSource; +use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Issue\ForbiddenCode; use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent; @@ -45,16 +44,15 @@ public static function analyze( ) { $arg_location = new CodeLocation($statements_analyzer->getSource(), $stmt->expr); - $eval_param_sink = TaintSink::getForMethodArgument( + $eval_param_sink = DataFlowNode::getForMethodArgument( 'eval', 'eval', 0, $arg_location, $arg_location, + TaintKind::INPUT_EVAL, ); - $eval_param_sink->taints = TaintKind::INPUT_EVAL; - $statements_analyzer->data_flow_graph->addSink($eval_param_sink); $codebase = $statements_analyzer->getCodebase(); @@ -65,7 +63,7 @@ public static function analyze( $taints = $added_taints & ~$removed_taints; if ($taints !== 0) { - $taint_source = TaintSource::fromNode($eval_param_sink, $taints); + $taint_source = $eval_param_sink->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php index 5fd6872e10f..4e6197a0040 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php @@ -12,7 +12,7 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\TaintFlowGraph; -use Psalm\Internal\DataFlow\TaintSink; +use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Issue\ForbiddenCode; use Psalm\Issue\ImpureFunctionCall; use Psalm\IssueBuffer; @@ -69,19 +69,18 @@ public static function analyze( if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { $call_location = new CodeLocation($statements_analyzer->getSource(), $stmt); - $echo_param_sink = TaintSink::getForMethodArgument( + $echo_param_sink = DataFlowNode::getForMethodArgument( 'exit', 'exit', 0, null, $call_location, + TaintKind::INPUT_HTML + | TaintKind::INPUT_HAS_QUOTES + | TaintKind::USER_SECRET + | TaintKind::SYSTEM_SECRET ); - $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); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 3ab9fd17a2c..4a7aa72e575 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -19,7 +19,6 @@ use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\Codebase\VariableUseGraph; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSource; use Psalm\Internal\Type\Comparator\AtomicTypeComparator; use Psalm\Internal\Type\Comparator\TypeComparisonResult; use Psalm\Internal\Type\Comparator\UnionTypeComparator; @@ -414,7 +413,7 @@ public static function taintArrayFetch( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($new_parent_node, $taints); + $taint_source = $new_parent_node->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index 9e4a56ec801..16aa7add052 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -24,7 +24,6 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSource; use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\MethodIdentifier; use Psalm\Internal\Type\TemplateInferredTypeReplacer; @@ -902,7 +901,7 @@ public static function processTaints( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($var_node, $taints); + $taint_source = $var_node->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } } @@ -949,7 +948,7 @@ public static function processUnspecialTaints( $data_flow_graph->addNode($localized_property_node); - $property_node = new DataFlowNode( + $property_node = DataFlowNode::make( $property_id, $property_id, null, @@ -980,7 +979,7 @@ public static function processUnspecialTaints( $taints = $added_taints & ~$removed_taints; if ($taints !== 0 && $statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { - $taint_source = TaintSource::fromNode($localized_property_node, $taints); + $taint_source = $localized_property_node->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index f2e4fbc33a9..e6c215a56c1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -14,7 +14,6 @@ use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSource; use Psalm\Issue\ImpureVariable; use Psalm\Issue\InvalidScope; use Psalm\Issue\PossiblyUndefinedGlobalVariable; @@ -529,7 +528,7 @@ private static function taintVariable( $taint_location = new CodeLocation($statements_analyzer->getSource(), $stmt); - $taint_source = new TaintSource( + $taint_source = DataFlowNode::make( $var_name . ':' . $taint_location->file_name . ':' . $taint_location->raw_file_start, $var_name, null, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php index 908d9ee2a6b..1488e9f9184 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php @@ -15,8 +15,7 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\TaintFlowGraph; -use Psalm\Internal\DataFlow\TaintSink; -use Psalm\Internal\DataFlow\TaintSource; +use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\Provider\NodeDataProvider; use Psalm\Issue\MissingFile; use Psalm\Issue\UnresolvableInclude; @@ -117,16 +116,15 @@ public static function analyze( ) { $arg_location = new CodeLocation($statements_analyzer->getSource(), $stmt->expr); - $include_param_sink = TaintSink::getForMethodArgument( + $include_param_sink = DataFlowNode::getForMethodArgument( 'include', 'include', 0, $arg_location, $arg_location, + TaintKind::INPUT_INCLUDE ); - $include_param_sink->taints = TaintKind::INPUT_INCLUDE; - $statements_analyzer->data_flow_graph->addSink($include_param_sink); $codebase = $statements_analyzer->getCodebase(); @@ -137,7 +135,7 @@ public static function analyze( $taints = $added_taints & ~$removed_taints; if ($taints !== 0) { - $taint_source = TaintSource::fromNode($include_param_sink, $taints); + $taint_source = $include_param_sink->setTaints($taints); $statements_analyzer->data_flow_graph->addSource($taint_source); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php index d49a41a6e23..0a48a1ccd12 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php @@ -12,7 +12,7 @@ use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\TaintFlowGraph; -use Psalm\Internal\DataFlow\TaintSink; +use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Issue\ForbiddenCode; use Psalm\Issue\ImpureFunctionCall; use Psalm\IssueBuffer; @@ -39,19 +39,18 @@ public static function analyze( if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph) { $call_location = new CodeLocation($statements_analyzer->getSource(), $stmt); - $print_param_sink = TaintSink::getForMethodArgument( + $print_param_sink = DataFlowNode::getForMethodArgument( 'print', 'print', 0, null, $call_location, + TaintKind::INPUT_HTML + | TaintKind::INPUT_HAS_QUOTES + | TaintKind::USER_SECRET + | TaintKind::SYSTEM_SECRET ); - $print_param_sink->taints = TaintKind::INPUT_HTML - | TaintKind::INPUT_HAS_QUOTES - | TaintKind::USER_SECRET - | TaintKind::SYSTEM_SECRET - ; $statements_analyzer->data_flow_graph->addSink($print_param_sink); } diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 7aa351a3767..a08f5192762 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -10,8 +10,6 @@ use Psalm\Config; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\DataFlow\TaintSink; -use Psalm\Internal\DataFlow\TaintSource; use Psalm\Issue\TaintedCallable; use Psalm\Issue\TaintedCookie; use Psalm\Issue\TaintedCustom; @@ -35,6 +33,7 @@ use Psalm\Progress\Phase; use Psalm\Progress\Progress; use Psalm\Type\TaintKind; +use Webmozart\Assert\Assert; use function count; use function end; @@ -48,13 +47,13 @@ */ final class TaintFlowGraph extends DataFlowGraph { - /** @var array */ + /** @var array */ private array $sources = []; /** @var array */ private array $nodes = []; - /** @var array */ + /** @var array */ private array $sinks = []; /** @@ -64,6 +63,13 @@ final class TaintFlowGraph extends DataFlowGraph */ private array $specializations = []; + /** + * Specialization key => true + * + * @var array + */ + private array $specialized_calls = []; + #[Override] public function addNode(DataFlowNode $node): void { @@ -71,17 +77,17 @@ public function addNode(DataFlowNode $node): void if ($node->unspecialized_id !== null) { /** @var string $node->specialization_key */ - $node->is_first_level_and_specialized = true; + $this->specialized_calls[$node->specialization_key] = true; $this->specializations[$node->unspecialized_id][$node->specialization_key] = $node->id; } } - public function addSource(TaintSource $node): void + public function addSource(DataFlowNode $node): void { $this->sources[$node->id] = $node; } - public function addSink(TaintSink $node): void + public function addSink(DataFlowNode $node): void { $this->sinks[$node->id] = $node; // in the rare case the sink is the _next_ node, this is necessary @@ -93,6 +99,7 @@ public function addGraph(self $other): void $this->sources += $other->sources; $this->sinks += $other->sinks; $this->nodes += $other->nodes; + $this->specialized_calls += $other->specialized_calls; foreach ($other->forward_edges as $key => $map) { if (!isset($this->forward_edges[$key])) { @@ -179,11 +186,11 @@ public function getSuccessorPath(DataFlowNode $sink): string public function getIssueTrace(DataFlowNode $source): array { $previous_source = $source->taintSource; - + $path_types = $source->path_types; $node = [ 'location' => $source->code_location, 'label' => $source->label, - 'entry_path_type' => end($source->path_types) ?: '', + 'entry_path_type' => end($path_types) ?: '', ]; if ($previous_source) { @@ -218,7 +225,7 @@ public function connectSinksAndSources(Progress $progress): void $codebase = $project_analyzer->getCodebase(); // Remove all specializations without an outgoing edge - foreach ($this->specializations as $k => &$map) { + /*foreach ($this->specializations as $k => &$map) { foreach ($map as $kk => $specialized_id) { if (!isset($this->forward_edges[$specialized_id])) { unset($map[$kk]); @@ -227,7 +234,7 @@ public function connectSinksAndSources(Progress $progress): void if (!$map) { unset($this->specializations[$k]); } - } unset($map); + } unset($map);*/ // reprocess resolved descendants up to a maximum nesting level of 40 for ($i = 0; count($sinks) && count($sources) && $i < 40; $i++) { @@ -260,17 +267,25 @@ public function connectSinksAndSources(Progress $progress): void // If this is a specialized node AND it was added using addNode, de-specialize; // Then, if we have one or more edges starting at the de-specialized node, // process destinations of those edges. - if ($source->is_first_level_and_specialized) { + if ($source->specialization_key !== null && isset($this->specialized_calls[$source->specialization_key])) { /** @var string $source->unspecialized_id */ if (!isset($this->forward_edges[$source->unspecialized_id])) { continue; } - $generated_source = clone $source; - $generated_source->id = $source->unspecialized_id; - $generated_source->is_first_level_and_specialized = false; - $generated_source->processing_specialized_descendants_of - [$source->specialization_key][$source->unspecialized_id] = $source->id; - + $specialized_calls = $source->specialized_calls; + $specialized_calls[$source->specialization_key][$source->unspecialized_id] = $source->id; + $generated_source = new DataFlowNode( + $source->unspecialized_id, + null, + null, + $source->label, + $source->code_location, + $source->taints, + $source->taintSource, + $source->path_types, + $specialized_calls + ); + $this->getChildNodes( $new_sources, $generated_source, @@ -285,17 +300,30 @@ public function connectSinksAndSources(Progress $progress): void // If this node has first level specializations (=> is first-level & unspecialized), // process them all } elseif (isset($this->specializations[$source->id])) { - if ($source->processing_specialized_descendants_of) { + $specialized_calls = $source->specialized_calls; + Assert::null($source->specialization_key); + + if ($specialized_calls) { // If processing descendants of a specialized call, accept only descendants. foreach ($this->specializations[$source->id] as $specialization => $specialized_id) { - if (!isset($source->processing_specialized_descendants_of[$specialization])) { + if (!isset($specialized_calls[$specialization])) { continue; } - $new_source = clone $source; - $new_source->is_first_level_and_specialized = false; - $new_source->id = $specialized_id; - unset($new_source->processing_specialized_descendants_of[$specialization]); + $copy = $specialized_calls; + unset($copy[$specialization]); + $new_source = new DataFlowNode( + $specialized_id, + $source->id, + $specialization, + $source->label, + $source->code_location, + $source->taints, + $source->taintSource, + $source->path_types, + $copy, + ); + $this->getChildNodes( $new_sources, $new_source, @@ -310,9 +338,17 @@ public function connectSinksAndSources(Progress $progress): void } else { // If not processing descendants, accept all specializations. foreach ($this->specializations[$source->id] as $specialization => $specialized_id) { - $new_source = clone $source; - $new_source->is_first_level_and_specialized = false; - $new_source->id = $specialized_id; + $new_source = new DataFlowNode( + $specialized_id, + $source->id, + $specialization, + $source->label, + $source->code_location, + $source->taints, + $source->taintSource, + $source->path_types, + $specialized_calls, + ); $this->getChildNodes( $new_sources, @@ -328,16 +364,25 @@ public function connectSinksAndSources(Progress $progress): void } } else { // Process all descendants - foreach ($source->processing_specialized_descendants_of as $map) { + Assert::null($source->specialization_key); + foreach ($source->specialized_calls as $specialization => $map) { if (isset($map[$source->id])) { $specialized_id = $map[$source->id]; if (!isset($this->forward_edges[$specialized_id])) { continue; } - $new_source = clone $source; - $new_source->is_first_level_and_specialized = false; - $new_source->id = $specialized_id; - + $new_source = new DataFlowNode( + $specialized_id, + $source->id, + $specialization, + $source->label, + $source->code_location, + $source->taints, + $source->taintSource, + $source->path_types, + $source->specialized_calls, + ); + $this->getChildNodes( $new_sources, $new_source, @@ -552,20 +597,27 @@ private function getChildNodes( } $key = $to_id . - ' ' . json_encode($generated_source->processing_specialized_descendants_of, JSON_THROW_ON_ERROR) . - ' ' . $new_taints; + ' ' . json_encode($generated_source->specialized_calls, JSON_THROW_ON_ERROR) . + ' ' . $new_taints; if (isset($new_sources[$key])) { continue; } - $new_destination = clone $this->nodes[$to_id]; - $new_destination->is_first_level_and_specialized = false; - $new_destination->taintSource = $generated_source; - $new_destination->taints = $new_taints; - $new_destination->processing_specialized_descendants_of - = $generated_source->processing_specialized_descendants_of; - $new_destination->path_types = [...$generated_source->path_types, $path_type]; + $old = $this->nodes[$to_id]; + $path_types = $generated_source->path_types; + $path_types []= $path_type; + $new_destination = new DataFlowNode( + $old->id, + $old->unspecialized_id, + $old->specialization_key, + $old->label, + $old->code_location, + $new_taints, + $generated_source, + $path_types, + $generated_source->specialized_calls + ); $new_sources[$key] = $new_destination; } diff --git a/src/Psalm/Internal/Codebase/VariableUseGraph.php b/src/Psalm/Internal/Codebase/VariableUseGraph.php index 08ad5ea6628..1fd4cbd872a 100644 --- a/src/Psalm/Internal/Codebase/VariableUseGraph.php +++ b/src/Psalm/Internal/Codebase/VariableUseGraph.php @@ -183,8 +183,18 @@ private function getChildNodes( continue; } - $new_destination = new DataFlowNode($to_id, $to_id, null); - $new_destination->path_types = [...$generated_source->path_types, ...[$path_type]]; + $path_types = $generated_source->path_types; + $path_types []= $path_type; + $new_destination = new DataFlowNode( + $to_id, + null, + null, + $to_id, + null, + 0, + null, + $path_types + ); $new_child_nodes[$to_id] = $new_destination; } diff --git a/src/Psalm/Internal/DataFlow/DataFlowNode.php b/src/Psalm/Internal/DataFlow/DataFlowNode.php index 18f17a783ee..c0ea0fa8063 100644 --- a/src/Psalm/Internal/DataFlow/DataFlowNode.php +++ b/src/Psalm/Internal/DataFlow/DataFlowNode.php @@ -6,6 +6,7 @@ use Override; use Psalm\CodeLocation; +use Psalm\Storage\ImmutableNonCloneableTrait; use Stringable; use function strtolower; @@ -14,48 +15,59 @@ * @psalm-consistent-constructor * @internal */ -class DataFlowNode implements Stringable +final readonly class DataFlowNode implements Stringable { - public ?string $unspecialized_id = null; - - public ?string $specialization_key = null; - - public bool $is_first_level_and_specialized = false; - - /** @var ?self */ - public ?DataFlowNode $taintSource = null; - - /** @var list */ - public array $path_types = []; + public function __construct( + public readonly string $id, + public readonly ?string $unspecialized_id, + public readonly ?string $specialization_key, + public readonly string $label, + public readonly ?CodeLocation $code_location = null, + public readonly int $taints = 0, + public readonly ?self $taintSource = null, + /** @var list */ + public readonly array $path_types = [], + /** + * @var array> + */ + public readonly array $specialized_calls = [], + ) { + } - /** - * @var array> - */ - public array $processing_specialized_descendants_of = []; + private function __clone() + { + } - public function __construct( - public string $id, - public string $label, - public ?CodeLocation $code_location, + public static function make( + string $id, + string $label, + ?CodeLocation $code_location, ?string $specialization_key = null, - public int $taints = 0, - ) { - if ($specialization_key) { - $this->unspecialized_id = $id; - $this->id .= '-' . $specialization_key; + int $taints = 0 + ): self { + if ($specialization_key === null) { + $unspecialized_id = null; + } else { + $unspecialized_id = $id; + $id .= '-' . $specialization_key; } - $this->specialization_key = $specialization_key; + return new self( + $id, + $unspecialized_id, + $specialization_key, + $label, + $code_location, + $taints + ); } - /** - * @return static - */ - final public static function getForMethodArgument( + public static function getForMethodArgument( string $method_id, string $cased_method_id, int $argument_offset, ?CodeLocation $arg_location, ?CodeLocation $code_location = null, + int $taints = 0, ): self { $arg_id = strtolower($method_id) . '#' . ($argument_offset + 1); @@ -67,18 +79,16 @@ final public static function getForMethodArgument( $specialization_key = strtolower($code_location->file_name) . ':' . $code_location->raw_file_start; } - return new static( + return self::make( $arg_id, $label, $arg_location, $specialization_key, + $taints, ); } - /** - * @return static - */ - final public static function getForAssignment( + public static function getForAssignment( string $var_id, CodeLocation $assignment_location, ?string $specialization_key = null, @@ -88,16 +98,15 @@ final public static function getForAssignment( . ':' . $assignment_location->raw_file_start . '-' . $assignment_location->raw_file_end; - return new static($id, $var_id, $assignment_location, $specialization_key); + return self::make($id, $var_id, $assignment_location, $specialization_key); } - /** - * @return static - */ - final public static function getForMethodReturn( + + public static function getForMethodReturn( string $method_id, string $cased_method_id, ?CodeLocation $code_location, ?CodeLocation $function_location = null, + int $taints = 0 ): self { $specialization_key = null; @@ -105,40 +114,48 @@ final public static function getForMethodReturn( $specialization_key = strtolower($function_location->file_name) . ':' . $function_location->raw_file_start; } - return new static( + return self::make( strtolower($method_id), $cased_method_id, $code_location, $specialization_key, + $taints, ); } - /** - * @return static - */ - final public static function getForVariableUse(): self + public static function getForVariableUse(): self { - return new static('variable-use', 'variable use', null); + return new self('variable-use', null, null, 'variable use'); } - - /** - * @return static - */ - final public static function getForUnknownOrigin(): self + public static function getForUnknownOrigin(): self { - return new static('unknown-origin', 'unknown origin', null); + return new self('unknown-origin', null, null, 'unknown origin'); } + public static function getForClosureUse(): self + { + return new self('closure-use', null, null, 'closure use'); + } - /** - * @return static - */ - final public static function getForClosureUse(): self + public function setTaints(int $taints): self { - return new static('closure-use', 'closure use', null); + if ($this->taints === $taints) { + return $this; + } + return new self( + $this->id, + $this->unspecialized_id, + $this->specialization_key, + $this->label, + $this->code_location, + $taints, + $this->taintSource, + $this->path_types, + $this->specialized_calls + ); } #[Override] diff --git a/src/Psalm/Internal/DataFlow/TaintSink.php b/src/Psalm/Internal/DataFlow/TaintSink.php deleted file mode 100644 index 42a75b2f4c8..00000000000 --- a/src/Psalm/Internal/DataFlow/TaintSink.php +++ /dev/null @@ -1,12 +0,0 @@ -unspecialized_id ?? $node->id, - $node->label, - $node->code_location, - $node->specialization_key, - $node->taints, - ); - $v->taints = $taints; - return $v; - } -} diff --git a/src/Psalm/Internal/PreloaderList.php b/src/Psalm/Internal/PreloaderList.php index 6c66d9d5d85..71f378d171d 100644 --- a/src/Psalm/Internal/PreloaderList.php +++ b/src/Psalm/Internal/PreloaderList.php @@ -761,8 +761,6 @@ final class PreloaderList { \Psalm\Internal\Composer::class, \Psalm\Internal\DataFlow\DataFlowNode::class, \Psalm\Internal\DataFlow\Path::class, - \Psalm\Internal\DataFlow\TaintSink::class, - \Psalm\Internal\DataFlow\TaintSource::class, \Psalm\Internal\Diff\AstDiffer::class, \Psalm\Internal\Diff\ClassStatementsDiffer::class, \Psalm\Internal\Diff\DiffElem::class, @@ -1591,6 +1589,7 @@ final class PreloaderList { \Psalm\Progress\DebugProgress::class, \Psalm\Progress\DefaultProgress::class, \Psalm\Progress\LongProgress::class, + \Psalm\Progress\Phase::class, \Psalm\Progress\Progress::class, \Psalm\Progress\VoidProgress::class, \Psalm\Report::class, @@ -1773,6 +1772,7 @@ final class PreloaderList { \Symfony\Component\Console\Output\BufferedOutput::class, \Symfony\Component\Console\Style\SymfonyStyle::class, \Symfony\Component\Filesystem\Path::class, + \Webmozart\Assert\Assert::class, \XdgBaseDir\Xdg::class, ]; From aa2327932b0a25ee9f74756194ea2e3189d3f420 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 5 Mar 2025 20:05:45 +0100 Subject: [PATCH 25/42] Fixes --- .../Statements/Expression/Fetch/VariableFetchAnalyzer.php | 4 ++-- .../Analyzer/Statements/Expression/PrintAnalyzer.php | 1 - src/Psalm/Internal/Codebase/TaintFlowGraph.php | 6 +++++- src/Psalm/Internal/DataFlow/DataFlowNode.php | 7 +++---- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index e6c215a56c1..f846f5cfdf7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -529,10 +529,10 @@ private static function taintVariable( $taint_location = new CodeLocation($statements_analyzer->getSource(), $stmt); $taint_source = DataFlowNode::make( - $var_name . ':' . $taint_location->file_name . ':' . $taint_location->raw_file_start, + $var_name, $var_name, null, - null, + $taint_location->file_name . ':' . $taint_location->raw_file_start, $taints, ); $statements_analyzer->data_flow_graph->addSource($taint_source); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php index 0a48a1ccd12..d01c35e4c46 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php @@ -51,7 +51,6 @@ public static function analyze( | TaintKind::SYSTEM_SECRET ); - $statements_analyzer->data_flow_graph->addSink($print_param_sink); } diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index a08f5192762..872a6801768 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -236,11 +236,14 @@ public function connectSinksAndSources(Progress $progress): void } } unset($map);*/ + $stack = []; // reprocess resolved descendants up to a maximum nesting level of 40 for ($i = 0; count($sinks) && count($sources) && $i < 40; $i++) { $new_sources = []; ksort($sources); + $stack []= $sources; + $progress->expand(count($sources)); foreach ($sources as $source) { @@ -264,7 +267,7 @@ public function connectSinksAndSources(Progress $progress): void continue; } - // If this is a specialized node AND it was added using addNode, de-specialize; + // If this is a specialized node, de-specialize; // Then, if we have one or more edges starting at the de-specialized node, // process destinations of those edges. if ($source->specialization_key !== null && isset($this->specialized_calls[$source->specialization_key])) { @@ -301,6 +304,7 @@ public function connectSinksAndSources(Progress $progress): void // process them all } elseif (isset($this->specializations[$source->id])) { $specialized_calls = $source->specialized_calls; + // Assert that we're unspecialized. Assert::null($source->specialization_key); if ($specialized_calls) { diff --git a/src/Psalm/Internal/DataFlow/DataFlowNode.php b/src/Psalm/Internal/DataFlow/DataFlowNode.php index c0ea0fa8063..a2d4d58db9c 100644 --- a/src/Psalm/Internal/DataFlow/DataFlowNode.php +++ b/src/Psalm/Internal/DataFlow/DataFlowNode.php @@ -91,14 +91,13 @@ public static function getForMethodArgument( public static function getForAssignment( string $var_id, CodeLocation $assignment_location, - ?string $specialization_key = null, + string $specialization_key = '', ): self { - $id = $var_id - . '-' . $assignment_location->file_name + $specialization_key .= '-' . $assignment_location->file_name . ':' . $assignment_location->raw_file_start . '-' . $assignment_location->raw_file_end; - return self::make($id, $var_id, $assignment_location, $specialization_key); + return self::make($var_id, $var_id, $assignment_location, $specialization_key); } public static function getForMethodReturn( From bda7a57cc5236286bab8375821b3d03a78adf25a Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 5 Mar 2025 20:46:51 +0100 Subject: [PATCH 26/42] cs-fix --- .../Analyzer/Statements/EchoAnalyzer.php | 2 +- .../Expression/Call/StaticCallAnalyzer.php | 2 +- .../Statements/Expression/ExitAnalyzer.php | 2 +- .../Statements/Expression/IncludeAnalyzer.php | 2 +- .../Statements/Expression/PrintAnalyzer.php | 2 +- .../Internal/Analyzer/StatementsAnalyzer.php | 34 +++++++++++++------ .../Internal/Codebase/TaintFlowGraph.php | 4 +-- .../Internal/Codebase/VariableUseGraph.php | 2 +- src/Psalm/Internal/DataFlow/DataFlowNode.php | 25 ++++++++------ 9 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php index ae39e946ef9..786eb1f25d6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/EchoAnalyzer.php @@ -66,7 +66,7 @@ public static function analyze( TaintKind::INPUT_HTML | TaintKind::INPUT_HAS_QUOTES | TaintKind::USER_SECRET - | TaintKind::SYSTEM_SECRET + | TaintKind::SYSTEM_SECRET, ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index be418b6ecc2..5efb9f17f7a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -375,7 +375,7 @@ public static function taintReturnType( $cased_method_id, $method_storage->signature_return_type_location ?: $method_storage->location, null, - $method_storage->taint_source_types + $method_storage->taint_source_types, ); $statements_analyzer->data_flow_graph->addSource($method_node); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php index 4e6197a0040..9acd1299216 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php @@ -78,7 +78,7 @@ public static function analyze( TaintKind::INPUT_HTML | TaintKind::INPUT_HAS_QUOTES | TaintKind::USER_SECRET - | TaintKind::SYSTEM_SECRET + | TaintKind::SYSTEM_SECRET, ); $statements_analyzer->data_flow_graph->addSink($echo_param_sink); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php index 1488e9f9184..453684a163d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php @@ -122,7 +122,7 @@ public static function analyze( 0, $arg_location, $arg_location, - TaintKind::INPUT_INCLUDE + TaintKind::INPUT_INCLUDE, ); $statements_analyzer->data_flow_graph->addSink($include_param_sink); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php index d01c35e4c46..ccc65107481 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php @@ -48,7 +48,7 @@ public static function analyze( TaintKind::INPUT_HTML | TaintKind::INPUT_HAS_QUOTES | TaintKind::USER_SECRET - | TaintKind::SYSTEM_SECRET + | TaintKind::SYSTEM_SECRET, ); $statements_analyzer->data_flow_graph->addSink($print_param_sink); diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 071b7efeba9..ee1ff90ab51 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -39,6 +39,7 @@ 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; @@ -154,11 +155,29 @@ public function __construct(protected SourceAnalyzer $source, public NodeDataPro $this->file_analyzer = $source->getFileAnalyzer(); $this->codebase = $source->getCodebase(); - if (!$this->codebase->taint_flow_graph && $this->codebase->find_unused_variables) { + if ($this->codebase->taint_flow_graph) { + $this->initTaintFlowGraph(true); + } elseif ($this->codebase->find_unused_variables) { $this->data_flow_graph = new VariableUseGraph(); } } + private function initTaintFlowGraph(bool $enable): ?TaintFlowGraph + { + $old = $this->data_flow_graph; + + if ($enable + && $this->codebase->taint_flow_graph + && $this->codebase->config->trackTaintsInPath($this->getFilePath()) + ) { + $this->data_flow_graph = $this->codebase->taint_flow_graph; + } else { + $this->data_flow_graph = null; + } + + return $old; + } + /** * Checks an array of statements for validity * @@ -177,16 +196,9 @@ public function analyze( // hoist functions to the top $this->hoistFunctions($stmts, $context); - $project_analyzer = $this->getFileAnalyzer()->project_analyzer; - $codebase = $project_analyzer->getCodebase(); + $codebase = $this->codebase; - if ($this->codebase->taint_flow_graph) { - if ($root_scope && $codebase->config->trackTaintsInPath($this->getFilePath())) { - $this->data_flow_graph = $this->codebase->taint_flow_graph; - } else { - $this->data_flow_graph = null; - } - } + $prev = $this->initTaintFlowGraph($root_scope); if ($codebase->config->hoist_constants) { self::hoistConstants($this, $stmts, $context); @@ -219,6 +231,8 @@ public function analyze( } } + $this->data_flow_graph = $prev; + return null; } diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 872a6801768..a5abb132ffe 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -286,7 +286,7 @@ public function connectSinksAndSources(Progress $progress): void $source->taints, $source->taintSource, $source->path_types, - $specialized_calls + $specialized_calls, ); $this->getChildNodes( @@ -620,7 +620,7 @@ private function getChildNodes( $new_taints, $generated_source, $path_types, - $generated_source->specialized_calls + $generated_source->specialized_calls, ); $new_sources[$key] = $new_destination; diff --git a/src/Psalm/Internal/Codebase/VariableUseGraph.php b/src/Psalm/Internal/Codebase/VariableUseGraph.php index 1fd4cbd872a..23de22698f0 100644 --- a/src/Psalm/Internal/Codebase/VariableUseGraph.php +++ b/src/Psalm/Internal/Codebase/VariableUseGraph.php @@ -193,7 +193,7 @@ private function getChildNodes( null, 0, null, - $path_types + $path_types, ); $new_child_nodes[$to_id] = $new_destination; diff --git a/src/Psalm/Internal/DataFlow/DataFlowNode.php b/src/Psalm/Internal/DataFlow/DataFlowNode.php index a2d4d58db9c..d4b51547f8c 100644 --- a/src/Psalm/Internal/DataFlow/DataFlowNode.php +++ b/src/Psalm/Internal/DataFlow/DataFlowNode.php @@ -6,7 +6,6 @@ use Override; use Psalm\CodeLocation; -use Psalm\Storage\ImmutableNonCloneableTrait; use Stringable; use function strtolower; @@ -30,7 +29,7 @@ public function __construct( /** * @var array> */ - public readonly array $specialized_calls = [], + public readonly array $specialized_calls = [], ) { } @@ -43,7 +42,7 @@ public static function make( string $label, ?CodeLocation $code_location, ?string $specialization_key = null, - int $taints = 0 + int $taints = 0, ): self { if ($specialization_key === null) { $unspecialized_id = null; @@ -57,7 +56,7 @@ public static function make( $specialization_key, $label, $code_location, - $taints + $taints, ); } @@ -91,11 +90,17 @@ public static function getForMethodArgument( public static function getForAssignment( string $var_id, CodeLocation $assignment_location, - string $specialization_key = '', + ?string $specialization_key = null, ): self { - $specialization_key .= '-' . $assignment_location->file_name - . ':' . $assignment_location->raw_file_start - . '-' . $assignment_location->raw_file_end; + if ($specialization_key === null) { + $specialization_key = $assignment_location->file_name + . ':' . $assignment_location->raw_file_start + . '-' . $assignment_location->raw_file_end; + } else { + $specialization_key .= '-' . $assignment_location->file_name + . ':' . $assignment_location->raw_file_start + . '-' . $assignment_location->raw_file_end; + } return self::make($var_id, $var_id, $assignment_location, $specialization_key); } @@ -105,7 +110,7 @@ public static function getForMethodReturn( string $cased_method_id, ?CodeLocation $code_location, ?CodeLocation $function_location = null, - int $taints = 0 + int $taints = 0, ): self { $specialization_key = null; @@ -153,7 +158,7 @@ public function setTaints(int $taints): self $taints, $this->taintSource, $this->path_types, - $this->specialized_calls + $this->specialized_calls, ); } From b513b32ef4a8f5a46c4412133f345182e34db54c Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 7 Mar 2025 17:09:10 +0100 Subject: [PATCH 27/42] Tmp --- src/Psalm/Internal/Codebase/TaintFlowGraph.php | 6 ++++++ src/Psalm/Internal/DataFlow/DataFlowNode.php | 15 +++++---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index a5abb132ffe..98e5e7bd908 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -310,6 +310,9 @@ public function connectSinksAndSources(Progress $progress): void if ($specialized_calls) { // If processing descendants of a specialized call, accept only descendants. foreach ($this->specializations[$source->id] as $specialization => $specialized_id) { + if (!isset($this->forward_edges[$specialized_id])) { + continue; + } if (!isset($specialized_calls[$specialization])) { continue; } @@ -342,6 +345,9 @@ public function connectSinksAndSources(Progress $progress): void } else { // If not processing descendants, accept all specializations. foreach ($this->specializations[$source->id] as $specialization => $specialized_id) { + if (!isset($this->forward_edges[$specialized_id])) { + continue; + } $new_source = new DataFlowNode( $specialized_id, $source->id, diff --git a/src/Psalm/Internal/DataFlow/DataFlowNode.php b/src/Psalm/Internal/DataFlow/DataFlowNode.php index d4b51547f8c..7492d89cbd5 100644 --- a/src/Psalm/Internal/DataFlow/DataFlowNode.php +++ b/src/Psalm/Internal/DataFlow/DataFlowNode.php @@ -92,17 +92,12 @@ public static function getForAssignment( CodeLocation $assignment_location, ?string $specialization_key = null, ): self { - if ($specialization_key === null) { - $specialization_key = $assignment_location->file_name - . ':' . $assignment_location->raw_file_start - . '-' . $assignment_location->raw_file_end; - } else { - $specialization_key .= '-' . $assignment_location->file_name - . ':' . $assignment_location->raw_file_start - . '-' . $assignment_location->raw_file_end; - } + $label = $var_id; + $var_id .= ':' . $assignment_location->file_name + . ':' . $assignment_location->raw_file_start + . '-' . $assignment_location->raw_file_end; - return self::make($var_id, $var_id, $assignment_location, $specialization_key); + return self::make($var_id, $label, $assignment_location, $specialization_key); } public static function getForMethodReturn( From 53d411397e5c1e894cd72d6c3a12a900ae8d2903 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 7 Mar 2025 17:46:04 +0100 Subject: [PATCH 28/42] Fix --- src/Psalm/Internal/Codebase/TaintFlowGraph.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 98e5e7bd908..7024fcd93a6 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -479,7 +479,8 @@ private function getChildNodes( $path = $this->getPredecessorPath($generated_source) . ' -> ' . $this->getSuccessorPath($sink); - for ($x = $codebase->taint_count-1; $x >= 0; $x--) { + $max = $codebase->taint_count; + for ($x = 0; $x < $max; $x++) { $t = 1 << $x; if (!($matching_taints & $t)) { continue; From fd5d04217aa7283f041a5a4e0f5a94ac8b7fef8b Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 7 Mar 2025 17:53:15 +0100 Subject: [PATCH 29/42] fix --- src/Psalm/Internal/DataFlow/DataFlowNode.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/DataFlow/DataFlowNode.php b/src/Psalm/Internal/DataFlow/DataFlowNode.php index 7492d89cbd5..8d3aa9cb5d3 100644 --- a/src/Psalm/Internal/DataFlow/DataFlowNode.php +++ b/src/Psalm/Internal/DataFlow/DataFlowNode.php @@ -14,7 +14,7 @@ * @psalm-consistent-constructor * @internal */ -final readonly class DataFlowNode implements Stringable +final class DataFlowNode implements Stringable { public function __construct( public readonly string $id, From e04dda943e46eeff7e0d291500076b21741cd697 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Mar 2025 16:30:06 +0100 Subject: [PATCH 30/42] Fixes --- examples/TemplateChecker.php | 1 + .../Internal/Analyzer/AttributesAnalyzer.php | 1 + src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 4 +-- src/Psalm/Internal/Analyzer/FileAnalyzer.php | 4 +-- .../Analyzer/FunctionLikeAnalyzer.php | 4 +-- .../Internal/Analyzer/InterfaceAnalyzer.php | 4 +-- .../Internal/Analyzer/NamespaceAnalyzer.php | 4 +-- .../Internal/Analyzer/StatementsAnalyzer.php | 32 ++++--------------- tests/AlgebraTest.php | 2 +- tests/TypeReconciliation/ReconcilerTest.php | 1 + 10 files changed, 21 insertions(+), 36 deletions(-) diff --git a/examples/TemplateChecker.php b/examples/TemplateChecker.php index e830403b46b..d92dae96779 100644 --- a/examples/TemplateChecker.php +++ b/examples/TemplateChecker.php @@ -178,6 +178,7 @@ private function checkWithViewClass(Context $context, array $stmts): void $statements_source = new StatementsAnalyzer( $view_method_analyzer, new NodeDataProvider(), + false, ); $statements_source->analyze($pseudo_method_stmts, $context); diff --git a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php index 7ac69ca8a63..571e9bb5ece 100644 --- a/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/AttributesAnalyzer.php @@ -212,6 +212,7 @@ private static function analyzeAttributeConstruction( $statements_analyzer = new StatementsAnalyzer( $source, new NodeDataProvider(), + false, ); $statements_analyzer->addSuppressedIssues(array_values($suppressed_issues)); diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 40b374a8224..4e40730140e 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -561,8 +561,8 @@ public function analyze( } } - $statements_analyzer = new StatementsAnalyzer($this, new NodeDataProvider()); - $statements_analyzer->analyze($member_stmts, $class_context, $global_context, true); + $statements_analyzer = new StatementsAnalyzer($this, new NodeDataProvider(), true); + $statements_analyzer->analyze($member_stmts, $class_context, $global_context); ClassConstAnalyzer::analyze($storage, $this->getCodebase()); diff --git a/src/Psalm/Internal/Analyzer/FileAnalyzer.php b/src/Psalm/Internal/Analyzer/FileAnalyzer.php index 7967b32c10d..bd8bc7caccb 100644 --- a/src/Psalm/Internal/Analyzer/FileAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FileAnalyzer.php @@ -162,7 +162,7 @@ public function analyze( $leftover_stmts = $this->populateCheckers($stmts); $this->node_data = new NodeDataProvider(); - $statements_analyzer = new StatementsAnalyzer($this, $this->node_data); + $statements_analyzer = new StatementsAnalyzer($this, $this->node_data, true); foreach ($file_storage->docblock_issues as $docblock_issue) { IssueBuffer::maybeAdd($docblock_issue); @@ -171,7 +171,7 @@ public function analyze( // 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); + $statements_analyzer->analyze($leftover_stmts, $this->context, $global_context); foreach ($leftover_stmts as $leftover_stmt) { if ($leftover_stmt instanceof PhpParser\Node\Stmt\Return_) { diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 5f7c3197cb2..21b13a322d9 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -240,7 +240,7 @@ public function analyze( $this->is_static = true; } - $statements_analyzer = new StatementsAnalyzer($this, $type_provider); + $statements_analyzer = new StatementsAnalyzer($this, $type_provider, true); $byref_uses = []; if ($this instanceof ClosureAnalyzer && $this->function instanceof Closure) { @@ -505,7 +505,7 @@ public function analyze( $statements_analyzer->addSuppressedIssues(['NoValue']); } - $statements_analyzer->analyze($function_stmts, $context, $global_context, true); + $statements_analyzer->analyze($function_stmts, $context, $global_context); if ($codebase->alter_code && isset($project_analyzer->getIssuesToFix()['MissingPureAnnotation']) diff --git a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php index 5e3f4151fb7..4779acde01f 100644 --- a/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/InterfaceAnalyzer.php @@ -221,8 +221,8 @@ public function analyze(): void MethodComparator::comparePseudoMethods($pseudo_methods, $this->fq_class_name, $codebase, $class_storage); - $statements_analyzer = new StatementsAnalyzer($this, new NodeDataProvider()); - $statements_analyzer->analyze($member_stmts, $interface_context, null, true); + $statements_analyzer = new StatementsAnalyzer($this, new NodeDataProvider(), true); + $statements_analyzer->analyze($member_stmts, $interface_context, null); ClassConstAnalyzer::analyze($this->storage, $this->getCodebase()); } diff --git a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php index 5cf7863367e..859c992b26e 100644 --- a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php @@ -77,7 +77,7 @@ public function collectAnalyzableInformation(): void } if ($leftover_stmts) { - $statements_analyzer = new StatementsAnalyzer($this, new NodeDataProvider()); + $statements_analyzer = new StatementsAnalyzer($this, new NodeDataProvider(), true); $file_context = $this->source->context; if ($file_context !== null) { @@ -88,7 +88,7 @@ public function collectAnalyzableInformation(): void $context->defineGlobals(); $context->collect_exceptions = $codebase->config->check_for_throws_in_global_scope; } - $statements_analyzer->analyze($leftover_stmts, $context, null, true); + $statements_analyzer->analyze($leftover_stmts, $context); } } diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index ee1ff90ab51..d99e297312e 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -150,32 +150,19 @@ final class StatementsAnalyzer extends SourceAnalyzer */ public array $foreach_var_locations = []; - public function __construct(protected SourceAnalyzer $source, public NodeDataProvider $node_data) + public function __construct(protected SourceAnalyzer $source, public NodeDataProvider $node_data, private readonly bool $root_scope) { $this->file_analyzer = $source->getFileAnalyzer(); $this->codebase = $source->getCodebase(); - if ($this->codebase->taint_flow_graph) { - $this->initTaintFlowGraph(true); - } elseif ($this->codebase->find_unused_variables) { - $this->data_flow_graph = new VariableUseGraph(); - } - } - - private function initTaintFlowGraph(bool $enable): ?TaintFlowGraph - { - $old = $this->data_flow_graph; - - if ($enable - && $this->codebase->taint_flow_graph + if ($this->codebase->taint_flow_graph + && $root_scope && $this->codebase->config->trackTaintsInPath($this->getFilePath()) ) { $this->data_flow_graph = $this->codebase->taint_flow_graph; - } else { - $this->data_flow_graph = null; + } elseif ($this->codebase->find_unused_variables) { + $this->data_flow_graph = new VariableUseGraph(); } - - return $old; } /** @@ -188,7 +175,6 @@ public function analyze( array $stmts, Context $context, ?Context $global_context = null, - bool $root_scope = false, ): ?bool { if (!$stmts) { return null; @@ -198,8 +184,6 @@ public function analyze( $codebase = $this->codebase; - $prev = $this->initTaintFlowGraph($root_scope); - if ($codebase->config->hoist_constants) { self::hoistConstants($this, $stmts, $context); } @@ -210,7 +194,7 @@ public function analyze( } } - if ($root_scope + if ($this->root_scope && !$context->collect_initializations && !$context->collect_mutations && $codebase->find_unused_variables @@ -219,7 +203,7 @@ public function analyze( $this->checkUnreferencedVars($stmts, $context); } - if ($codebase->alter_code && $root_scope && $this->vars_to_initialize) { + if ($codebase->alter_code && $this->root_scope && $this->vars_to_initialize) { $file_contents = $codebase->getFileContents($this->getFilePath()); foreach ($this->vars_to_initialize as $var_id => $branch_point) { @@ -231,8 +215,6 @@ public function analyze( } } - $this->data_flow_graph = $prev; - return null; } diff --git a/tests/AlgebraTest.php b/tests/AlgebraTest.php index 8ba5f869f72..d2dc4f26f1b 100644 --- a/tests/AlgebraTest.php +++ b/tests/AlgebraTest.php @@ -113,7 +113,7 @@ public function testCombinatorialExpansion(): void $file_analyzer = new FileAnalyzer($this->project_analyzer, 'somefile.php', 'somefile.php'); $file_analyzer->context = new Context(); - $statements_analyzer = new StatementsAnalyzer($file_analyzer, new NodeDataProvider()); + $statements_analyzer = new StatementsAnalyzer($file_analyzer, new NodeDataProvider(), false); $dnf_clauses = FormulaGenerator::getFormula( spl_object_id($dnf_stmt->expr), diff --git a/tests/TypeReconciliation/ReconcilerTest.php b/tests/TypeReconciliation/ReconcilerTest.php index ad2b1a4bf6d..7a48a57358d 100644 --- a/tests/TypeReconciliation/ReconcilerTest.php +++ b/tests/TypeReconciliation/ReconcilerTest.php @@ -56,6 +56,7 @@ public function setUp(): void $this->statements_analyzer = new StatementsAnalyzer( $this->file_analyzer, new NodeDataProvider(), + false, ); $this->addFile('newfile.php', ' From abcfaabfda36d26a93d9b6533ba5315075fb4f4a Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Mar 2025 20:02:12 +0100 Subject: [PATCH 31/42] Fixes --- src/Psalm/Internal/Codebase/TaintFlowGraph.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 7024fcd93a6..c57b5f42914 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -374,7 +374,6 @@ public function connectSinksAndSources(Progress $progress): void } } else { // Process all descendants - Assert::null($source->specialization_key); foreach ($source->specialized_calls as $specialization => $map) { if (isset($map[$source->id])) { $specialized_id = $map[$source->id]; From 0e7c2d49160e3226c560595cd8d783aeab1232d0 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Mar 2025 20:06:59 +0100 Subject: [PATCH 32/42] Fix --- src/Psalm/Internal/Analyzer/StatementsAnalyzer.php | 8 +++++--- src/Psalm/Internal/Codebase/TaintFlowGraph.php | 14 +++++--------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index d99e297312e..cb21490d685 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -39,7 +39,6 @@ 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; @@ -150,8 +149,11 @@ final class StatementsAnalyzer extends SourceAnalyzer */ public array $foreach_var_locations = []; - public function __construct(protected SourceAnalyzer $source, public NodeDataProvider $node_data, private readonly bool $root_scope) - { + public function __construct( + protected SourceAnalyzer $source, + public NodeDataProvider $node_data, + private readonly bool $root_scope, + ) { $this->file_analyzer = $source->getFileAnalyzer(); $this->codebase = $source->getCodebase(); diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index c57b5f42914..bdb89874832 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -225,7 +225,7 @@ public function connectSinksAndSources(Progress $progress): void $codebase = $project_analyzer->getCodebase(); // Remove all specializations without an outgoing edge - /*foreach ($this->specializations as $k => &$map) { + foreach ($this->specializations as $k => &$map) { foreach ($map as $kk => $specialized_id) { if (!isset($this->forward_edges[$specialized_id])) { unset($map[$kk]); @@ -234,7 +234,7 @@ public function connectSinksAndSources(Progress $progress): void if (!$map) { unset($this->specializations[$k]); } - } unset($map);*/ + } unset($map); $stack = []; // reprocess resolved descendants up to a maximum nesting level of 40 @@ -270,7 +270,9 @@ public function connectSinksAndSources(Progress $progress): void // If this is a specialized node, de-specialize; // Then, if we have one or more edges starting at the de-specialized node, // process destinations of those edges. - if ($source->specialization_key !== null && isset($this->specialized_calls[$source->specialization_key])) { + if ($source->specialization_key !== null + && isset($this->specialized_calls[$source->specialization_key]) + ) { /** @var string $source->unspecialized_id */ if (!isset($this->forward_edges[$source->unspecialized_id])) { continue; @@ -310,9 +312,6 @@ public function connectSinksAndSources(Progress $progress): void if ($specialized_calls) { // If processing descendants of a specialized call, accept only descendants. foreach ($this->specializations[$source->id] as $specialization => $specialized_id) { - if (!isset($this->forward_edges[$specialized_id])) { - continue; - } if (!isset($specialized_calls[$specialization])) { continue; } @@ -345,9 +344,6 @@ public function connectSinksAndSources(Progress $progress): void } else { // If not processing descendants, accept all specializations. foreach ($this->specializations[$source->id] as $specialization => $specialized_id) { - if (!isset($this->forward_edges[$specialized_id])) { - continue; - } $new_source = new DataFlowNode( $specialized_id, $source->id, From 0f32350bf168428a6191ca66251533487e29202c Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Mar 2025 20:18:00 +0100 Subject: [PATCH 33/42] Bump upgrading --- UPGRADING.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/UPGRADING.md b/UPGRADING.md index f237259cb1d..9ded9b873b1 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -2,6 +2,22 @@ ## Changed +- [BC] Taints are now *internally* represented by a bitmap (an integer), instead of an array of strings. Users can still use the usual string taint identifiers (including custom ones, which will be automatically registered by Psalm), but internally, the type of `Psalm\Type\TaintKind` taint types is now an integer. + +- [BC] The maximum number of usable taint *types* (including both native taints and custom taints) is now equal to 32 on 32-bit systems and 64 on 64-bit systems: this should be enough for the vast majority of usecases, if more taint types are needed, consider merging some taint types or using some native taint types. + +- [BC] `Psalm\Plugin\EventHandler\AddTaintsInterface::addTaints` and `Psalm\Plugin\EventHandler\RemoveTaintsInterface::removeTaints` now must return an integer taint instead of an array of strings (see the new [taint documentation](https://psalm.dev/docs/security_analysis/custom_taint_sources/) for more info). + +- [BC] The type of the `$taints` parameter of `Psalm\Codebase::addTaintSource` and `Psalm\Codebase::addTaintSink` was changed to an integer + +- [BC] Type of property `Psalm\Storage\FunctionLikeParameter::$sinks` changed from `array|null` to `int` + +- [BC] Type of property `Psalm\Storage\FunctionLikeStorage::$taint_source_types` changed from `array` to `int` + +- [BC] Type of property `Psalm\Storage\FunctionLikeStorage::$added_taints` changed from `array` to `int` + +- [BC] Type of property `Psalm\Storage\FunctionLikeStorage::$removed_taints` changed from `array` to `int` + - [BC] The `startScanningFiles`, `startAnalyzingFiles`, `startAlteringFiles` of `Psalm\Progress\Progress` and subclasses were removed and replaced with a new `startPhase` method, taking a `Psalm\Progress\Phase` enum case. - [BC] The `start` method was removed, use `expand`, instead; the progress is reset to 0 when changing the current phase. From f78967ca03556f909299138fe71e44086ae87bea Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Mar 2025 20:50:55 +0100 Subject: [PATCH 34/42] Fix --- .../Internal/Analyzer/StatementsAnalyzer.php | 70 +++++++++++-------- .../Internal/Codebase/TaintFlowGraph.php | 2 - 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index cb21490d685..d35aece9269 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -149,6 +149,8 @@ final class StatementsAnalyzer extends SourceAnalyzer */ public array $foreach_var_locations = []; + private int $depth = 0; + public function __construct( protected SourceAnalyzer $source, public NodeDataProvider $node_data, @@ -181,43 +183,53 @@ public function analyze( if (!$stmts) { return null; } - // hoist functions to the top - $this->hoistFunctions($stmts, $context); - - $codebase = $this->codebase; + $this->depth++; + try { + // hoist functions to the top + $this->hoistFunctions($stmts, $context); - if ($codebase->config->hoist_constants) { - self::hoistConstants($this, $stmts, $context); - } + $codebase = $this->codebase; - foreach ($stmts as $stmt) { - if (self::analyzeStatement($this, $stmt, $context, $global_context) === false) { - return false; + if ($codebase->config->hoist_constants) { + self::hoistConstants($this, $stmts, $context); } - } - if ($this->root_scope - && !$context->collect_initializations - && !$context->collect_mutations - && $codebase->find_unused_variables - && $context->check_variables - ) { - $this->checkUnreferencedVars($stmts, $context); - } + foreach ($stmts as $stmt) { + if (self::analyzeStatement($this, $stmt, $context, $global_context) === false) { + return false; + } + } - if ($codebase->alter_code && $this->root_scope && $this->vars_to_initialize) { - $file_contents = $codebase->getFileContents($this->getFilePath()); + if ($this->root_scope + && $this->depth === 1 + && !$context->collect_initializations + && !$context->collect_mutations + && $codebase->find_unused_variables + && $context->check_variables + ) { + $this->checkUnreferencedVars($stmts, $context); + } - 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 ($codebase->alter_code + && $this->root_scope + && $this->depth === 1 + && $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), + ]); + } } - } - return null; + return null; + } finally { + $this->depth--; + } } /** diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index bdb89874832..147ddfed96d 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -236,13 +236,11 @@ public function connectSinksAndSources(Progress $progress): void } } unset($map); - $stack = []; // reprocess resolved descendants up to a maximum nesting level of 40 for ($i = 0; count($sinks) && count($sources) && $i < 40; $i++) { $new_sources = []; ksort($sources); - $stack []= $sources; $progress->expand(count($sources)); From 1cf251147c7462f43e9df01e61ff8377144deea2 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Mar 2025 20:54:21 +0100 Subject: [PATCH 35/42] Fixes --- examples/plugins/SafeArrayKeyChecker.php | 2 +- examples/plugins/TaintActiveRecords.php | 2 +- psalm-baseline.xml | 16 ++-------------- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/examples/plugins/SafeArrayKeyChecker.php b/examples/plugins/SafeArrayKeyChecker.php index 8713f15a9a1..5cfb6e3d99a 100644 --- a/examples/plugins/SafeArrayKeyChecker.php +++ b/examples/plugins/SafeArrayKeyChecker.php @@ -13,7 +13,7 @@ final class SafeArrayKeyChecker implements RemoveTaintsInterface /** * Called to see what taints should be removed * - * @return int-mask-of + * @return int */ #[\Override] public static function removeTaints(AddRemoveTaintsEvent $event): int diff --git a/examples/plugins/TaintActiveRecords.php b/examples/plugins/TaintActiveRecords.php index a1da7623158..f0ade9645d4 100644 --- a/examples/plugins/TaintActiveRecords.php +++ b/examples/plugins/TaintActiveRecords.php @@ -28,7 +28,7 @@ final class TaintActiveRecords implements AddTaintsInterface /** * Called to see what taints should be added * - * @return int-mask-of + * @return int */ #[Override] public static function addTaints(AddRemoveTaintsEvent $event): int diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 173bc553133..7f688a409e5 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -911,7 +911,6 @@ calling_method_id]]> calling_method_id]]> calling_method_id]]> - sinks]]> @@ -1041,7 +1040,6 @@ - specialization_key]]> @@ -1621,12 +1619,7 @@ - specialization_key]]> - unspecialized_id]]> - escaped_taints]]> - unescaped_taints]]> - specialization_key]]> - path_types)]]> + @@ -1634,11 +1627,6 @@ - - - - - name]]> From e26e62d1206188d674908cd18b27144b91756d64 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Mar 2025 21:01:32 +0100 Subject: [PATCH 36/42] Fix --- tests/CommentAnalyzerTest.php | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/CommentAnalyzerTest.php b/tests/CommentAnalyzerTest.php index b9a7ae2ebf0..96a9fdde97f 100644 --- a/tests/CommentAnalyzerTest.php +++ b/tests/CommentAnalyzerTest.php @@ -8,17 +8,32 @@ use PHPUnit\Framework\TestCase as BaseTestCase; use PhpParser\Comment\Doc; use Psalm\Aliases; +use Psalm\Codebase; use Psalm\Internal\Analyzer\CommentAnalyzer; use Psalm\Internal\Analyzer\ProjectAnalyzer; +use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\RuntimeCaches; use Psalm\Internal\Scanner\FileScanner; +use Psalm\Internal\Provider\Providers; +use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; final class CommentAnalyzerTest extends BaseTestCase { + private Codebase $codebase; + #[Override] public function setUp(): void { RuntimeCaches::clearAll(); + + $file_provider = new FakeFileProvider(); + $this->codebase = (new ProjectAnalyzer( + new TestConfig(), + new Providers( + $file_provider, + new FakeParserCacheProvider(), + ), + ))->getCodebase(); } public function testDocblockVarDescription(): void @@ -28,7 +43,7 @@ public function testDocblockVarDescription(): void */ '; $php_parser_doc = new Doc($doc); - $comment_docblock = CommentAnalyzer::getTypeFromComment(ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); + $comment_docblock = CommentAnalyzer::getTypeFromComment($this->codebase, $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); $this->assertSame('Some Description', $comment_docblock[0]->description); } @@ -39,7 +54,7 @@ public function testDocblockVarDescriptionWithVarId(): void */ '; $php_parser_doc = new Doc($doc); - $comment_docblock = CommentAnalyzer::getTypeFromComment(ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); + $comment_docblock = CommentAnalyzer::getTypeFromComment($this->codebase, $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); $this->assertSame('Some Description', $comment_docblock[0]->description); } @@ -51,7 +66,7 @@ public function testDocblockVarDescriptionMultiline(): void */ '; $php_parser_doc = new Doc($doc); - $comment_docblock = CommentAnalyzer::getTypeFromComment(ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); + $comment_docblock = CommentAnalyzer::getTypeFromComment($this->codebase, $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); $this->assertSame('Some Description with a long description.', $comment_docblock[0]->description); } @@ -64,7 +79,7 @@ public function testDocblockDescription(): void */ '; $php_parser_doc = new Doc($doc); - $comment_docblock = CommentAnalyzer::getTypeFromComment(ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); + $comment_docblock = CommentAnalyzer::getTypeFromComment($this->codebase, $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); $this->assertSame('Some Description', $comment_docblock[0]->description); } @@ -77,7 +92,7 @@ public function testDocblockDescriptionWithVarDescription(): void */ '; $php_parser_doc = new Doc($doc); - $comment_docblock = CommentAnalyzer::getTypeFromComment(ProjectAnalyzer::getInstance()->getCodebase(), $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); + $comment_docblock = CommentAnalyzer::getTypeFromComment($this->codebase, $php_parser_doc, new FileScanner('somefile.php', 'somefile.php', false), new Aliases); $this->assertSame('Use a string', $comment_docblock[0]->description); } From 399383b4bdb14690baa84fb6348dabfc340e0862 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Mar 2025 21:18:27 +0100 Subject: [PATCH 37/42] Fix --- src/Psalm/Internal/DataFlow/DataFlowNode.php | 4 +-- tests/fixtures/expected_taint_graph.dot | 36 ++++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Psalm/Internal/DataFlow/DataFlowNode.php b/src/Psalm/Internal/DataFlow/DataFlowNode.php index 8d3aa9cb5d3..fadde77bd13 100644 --- a/src/Psalm/Internal/DataFlow/DataFlowNode.php +++ b/src/Psalm/Internal/DataFlow/DataFlowNode.php @@ -48,7 +48,7 @@ public static function make( $unspecialized_id = null; } else { $unspecialized_id = $id; - $id .= '-' . $specialization_key; + $id .= ' specialized in ' . $specialization_key; } return new self( $id, @@ -93,7 +93,7 @@ public static function getForAssignment( ?string $specialization_key = null, ): self { $label = $var_id; - $var_id .= ':' . $assignment_location->file_name + $var_id .= ' from ' . strtolower($assignment_location->file_name) . ':' . $assignment_location->raw_file_start . '-' . $assignment_location->raw_file_end; diff --git a/tests/fixtures/expected_taint_graph.dot b/tests/fixtures/expected_taint_graph.dot index 26cf0ac54ae..f5334d43943 100644 --- a/tests/fixtures/expected_taint_graph.dot +++ b/tests/fixtures/expected_taint_graph.dot @@ -1,20 +1,20 @@ digraph Taints { - "$_GET:src/FileWithErrors.php:414" -> "$_GET['abc']-src/FileWithErrors.php:414-418" - "$_GET:src/FileWithErrors.php:441" -> "$_GET['abc']-src/FileWithErrors.php:441-445" - "$_GET:src/FileWithErrors.php:457" -> "$_GET['abc']-src/FileWithErrors.php:457-461" - "$_GET['abc']-src/FileWithErrors.php:441-445" -> "call to is_string-src/FileWithErrors.php:441-452" - "$_GET['abc']-src/FileWithErrors.php:457-461" -> "call to echo-src/FileWithErrors.php:408-474" - "$s-src/FileWithErrors.php:110-111" -> "variable-use" -> "acme\sampleproject\bar" - "$s-src/FileWithErrors.php:163-164" -> "variable-use" -> "acme\sampleproject\baz" - "$s-src/FileWithErrors.php:216-217" -> "variable-use" -> "acme\sampleproject\bat" - "$s-src/FileWithErrors.php:270-271" -> "variable-use" -> "acme\sampleproject\bang" - "acme\sampleproject\bang#1" -> "$s-src/FileWithErrors.php:270-271" - "acme\sampleproject\bar#1" -> "$s-src/FileWithErrors.php:110-111" - "acme\sampleproject\bat#1" -> "$s-src/FileWithErrors.php:216-217" - "acme\sampleproject\baz#1" -> "$s-src/FileWithErrors.php:163-164" - "acme\sampleproject\foo#1" -> "$_s-src/FileWithErrors.php:57-59" - "call to echo-src/FileWithErrors.php:336-368" -> "echo#1-src/filewitherrors.php:331" - "call to echo-src/FileWithErrors.php:408-474" -> "echo#1-src/filewitherrors.php:403" - "call to is_string-src/FileWithErrors.php:441-452" -> "is_string#1-src/filewitherrors.php:431" - "coalesce-src/FileWithErrors.php:346-367" -> "call to echo-src/FileWithErrors.php:336-368" + "$_GET specialized in src/FileWithErrors.php:414" -> "$_GET['abc'] from src/filewitherrors.php:414-418" + "$_GET specialized in src/FileWithErrors.php:441" -> "$_GET['abc'] from src/filewitherrors.php:441-445" + "$_GET specialized in src/FileWithErrors.php:457" -> "$_GET['abc'] from src/filewitherrors.php:457-461" + "$_GET['abc'] from src/filewitherrors.php:441-445" -> "call to is_string from src/filewitherrors.php:441-452" + "$_GET['abc'] from src/filewitherrors.php:457-461" -> "call to echo from src/filewitherrors.php:408-474" + "$s from src/filewitherrors.php:110-111" -> "variable-use" -> "acme\sampleproject\bar" + "$s from src/filewitherrors.php:163-164" -> "variable-use" -> "acme\sampleproject\baz" + "$s from src/filewitherrors.php:216-217" -> "variable-use" -> "acme\sampleproject\bat" + "$s from src/filewitherrors.php:270-271" -> "variable-use" -> "acme\sampleproject\bang" + "acme\sampleproject\bang#1" -> "$s from src/filewitherrors.php:270-271" + "acme\sampleproject\bar#1" -> "$s from src/filewitherrors.php:110-111" + "acme\sampleproject\bat#1" -> "$s from src/filewitherrors.php:216-217" + "acme\sampleproject\baz#1" -> "$s from src/filewitherrors.php:163-164" + "acme\sampleproject\foo#1" -> "$_s from src/filewitherrors.php:57-59" + "call to echo from src/filewitherrors.php:336-368" -> "echo#1 specialized in src/filewitherrors.php:331" + "call to echo from src/filewitherrors.php:408-474" -> "echo#1 specialized in src/filewitherrors.php:403" + "call to is_string from src/filewitherrors.php:441-452" -> "is_string#1 specialized in src/filewitherrors.php:431" + "coalesce from src/filewitherrors.php:346-367" -> "call to echo from src/filewitherrors.php:336-368" } From 89f91125c71cc40df567109d43693fe105c96057 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Mar 2025 21:41:08 +0100 Subject: [PATCH 38/42] Improvement --- .../Internal/Codebase/VariableUseGraph.php | 25 ++++++++----------- src/Psalm/Internal/DataFlow/DataFlowNode.php | 9 ++++--- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/Psalm/Internal/Codebase/VariableUseGraph.php b/src/Psalm/Internal/Codebase/VariableUseGraph.php index 23de22698f0..29cf07a5949 100644 --- a/src/Psalm/Internal/Codebase/VariableUseGraph.php +++ b/src/Psalm/Internal/Codebase/VariableUseGraph.php @@ -74,16 +74,13 @@ public function isVariableUsed(DataFlowNode $assignment_node): bool foreach ($sources as $source) { $visited_source_ids[$source->id] = true; - $child_nodes = $this->getChildNodes( + if ($this->getChildNodes( + $new_child_nodes, $source, $visited_source_ids, - ); - - if ($child_nodes === null) { + )) { return true; } - - $new_child_nodes = [...$new_child_nodes, ...$child_nodes]; } $sources = $new_child_nodes; @@ -138,16 +135,16 @@ public function getOriginLocations(DataFlowNode $assignment_node): array /** * @param array $visited_source_ids - * @return array|null + * @param array $child_nodes + * @param-out array $child_nodes */ private function getChildNodes( + array &$child_nodes, DataFlowNode $generated_source, array $visited_source_ids, - ): ?array { - $new_child_nodes = []; - + ): bool { if (!isset($this->forward_edges[$generated_source->id])) { - return []; + return false; } foreach ($this->forward_edges[$generated_source->id] as $to_id => $path) { @@ -164,7 +161,7 @@ private function getChildNodes( || $path->type === 'arg' || $path->type === 'comparison' ) { - return null; + return true; } if (isset($visited_source_ids[$to_id])) { @@ -196,10 +193,10 @@ private function getChildNodes( $path_types, ); - $new_child_nodes[$to_id] = $new_destination; + $child_nodes[$to_id] = $new_destination; } - return $new_child_nodes; + return false; } /** diff --git a/src/Psalm/Internal/DataFlow/DataFlowNode.php b/src/Psalm/Internal/DataFlow/DataFlowNode.php index fadde77bd13..3175225fc7c 100644 --- a/src/Psalm/Internal/DataFlow/DataFlowNode.php +++ b/src/Psalm/Internal/DataFlow/DataFlowNode.php @@ -123,20 +123,23 @@ public static function getForMethodReturn( } + private static self $forVariableUse; public static function getForVariableUse(): self { - return new self('variable-use', null, null, 'variable use'); + return self::$forVariableUse ??= new self('variable-use', null, null, 'variable use'); } + private static self $forUnknownOrigin; public static function getForUnknownOrigin(): self { - return new self('unknown-origin', null, null, 'unknown origin'); + return self::$forUnknownOrigin ??= new self('unknown-origin', null, null, 'unknown origin'); } + private static self $forClosureUse; public static function getForClosureUse(): self { - return new self('closure-use', null, null, 'closure use'); + return self::$forClosureUse ??= new self('closure-use', null, null, 'closure use'); } public function setTaints(int $taints): self From 1844ba53f32c208939af51af678dbd3d80169700 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 11 Mar 2025 17:50:29 +0100 Subject: [PATCH 39/42] Fix --- .../Analyzer/Statements/Expression/AssignmentAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 1996f1b3800..9824c2894e6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -950,7 +950,7 @@ public static function analyzeAssignmentRef( // Remove old reference parent node so previously referenced variable usage doesn't count as reference usage $old_type = $context->vars_in_scope[$lhs_var_id]; foreach ($old_type->parent_nodes as $old_parent_node_id => $_) { - if (str_starts_with($old_parent_node_id, "$lhs_var_id-")) { + if (str_starts_with($old_parent_node_id, "$lhs_var_id from ")) { unset($old_type->parent_nodes[$old_parent_node_id]); } } From f10a077076cc745d8467e9f8e81ec49ae09358c4 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 11 Mar 2025 18:32:57 +0100 Subject: [PATCH 40/42] Finalize --- psalm-baseline.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7f688a409e5..e49e73e4ef9 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1627,6 +1627,16 @@ + + + + + + + + + + name]]> From 91a16994441ff5419615843254466259a4bfeb02 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 11 Mar 2025 18:33:14 +0100 Subject: [PATCH 41/42] cs-fix --- tests/CommentAnalyzerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CommentAnalyzerTest.php b/tests/CommentAnalyzerTest.php index 96a9fdde97f..1c08495fb22 100644 --- a/tests/CommentAnalyzerTest.php +++ b/tests/CommentAnalyzerTest.php @@ -12,9 +12,9 @@ use Psalm\Internal\Analyzer\CommentAnalyzer; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\Provider\FakeFileProvider; +use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; use Psalm\Internal\Scanner\FileScanner; -use Psalm\Internal\Provider\Providers; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; final class CommentAnalyzerTest extends BaseTestCase From caafc3cece82fd457c40eb1bbd8ee770e769e0ec Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 11 Mar 2025 18:34:37 +0100 Subject: [PATCH 42/42] Revert --- .github/workflows/windows-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 9bed492db43..88696ba49d3 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -59,7 +59,8 @@ jobs: #ini-values: zend.assertions=1, assert.exception=1 tools: composer:v2 coverage: none - extensions: none, curl, dom, filter, intl, json, libxml, mbstring, openssl, opcache, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter + #extensions: none, curl, dom, filter, intl, json, libxml, mbstring, openssl, opcache, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter + extensions: none, curl, dom, filter, intl, json, libxml, mbstring, openssl, pcre, phar, reflection, simplexml, spl, tokenizer, xml, xmlwriter env: fail-fast: true