diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index 059ecb4d4d3..622eed45916 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -6,6 +6,7 @@ * @param mixed $search_value * @param bool $strict * + * @psalm-flow ($array) -> return * @return (TArray is non-empty-array ? non-empty-list> : list>) * @psalm-pure */ @@ -20,6 +21,7 @@ function array_keys(array $array, $search_value = null, bool $strict = false) * @param array $array * @param array ...$arrays * + * @psalm-flow ($array) -> return * @return array * @psalm-pure */ @@ -34,6 +36,7 @@ function array_intersect(array $array, array ...$arrays) * @param array $array * @param array ...$arrays * + * @psalm-flow ($array) -> return * @return array * @psalm-pure */ @@ -48,6 +51,7 @@ function array_intersect_key(array $array, array ...$arrays) * @param array $array * @param array ...$arrays * + * @psalm-flow ($array) -> return * @return array * @psalm-pure */ @@ -55,6 +59,22 @@ function array_intersect_assoc(array $array, array ...$arrays) { } +/** + * @psalm-flow ($array) -> return + * @psalm-pure + */ +function array_intersect_uassoc(array $array, array ...$arrays, callable $key_compare_func): array +{ +} + +/** + * @psalm-flow ($array) -> return + * @psalm-pure + */ +function array_intersect_ukey(array $array, array ...$arrays, callable $key_compare_func): array +{ +} + /** * @psalm-template TKey as array-key * @psalm-template TValue @@ -62,6 +82,7 @@ function array_intersect_assoc(array $array, array ...$arrays) * @param array $keys * @param array $values * + * @psalm-flow ($keys, $values) -> return * @return ( * PHP_MAJOR_VERSION is 8 ? * ($keys is non-empty-array ? non-empty-array : array) : @@ -81,6 +102,7 @@ function array_combine(array $keys, array $values) * @param array $array * @param array ...$arrays * + * @psalm-flow ($array) -> return * @return array * @psalm-pure */ @@ -95,6 +117,7 @@ function array_diff(array $array, array ...$arrays) * @param array $array * @param array ...$arrays * + * @psalm-flow ($array) -> return * @return array * @psalm-pure */ @@ -102,6 +125,22 @@ function array_diff_key(array $array, array ...$arrays) { } +/** + * @psalm-flow ($array) -> return + * @psalm-pure + */ +function array_diff_uassoc(array $array, array ...$arrays, callable $key_compare_func) +{ +} + +/** + * @psalm-flow ($array) -> return + * @psalm-pure + */ +function array_diff_ukey(array $array, array ...$arrays, callable $key_compare_func) +{ +} + /** * @psalm-template TKey as array-key * @psalm-template TValue @@ -109,6 +148,7 @@ function array_diff_key(array $array, array ...$arrays) * @param array $array * @param array ...$arrays * + * @psalm-flow ($array) -> return * @return array * @psalm-pure */ @@ -122,6 +162,7 @@ function array_diff_assoc(array $array, array ...$arrays) * * @param array $array * + * @psalm-flow ($array) -> return * @return ($array is non-empty-array ? non-empty-array : array) * @psalm-pure */ @@ -136,6 +177,7 @@ function array_flip(array $array) * * @param TArray $array * + * @psalm-flow ($array) -> return * @return (TArray is non-empty-array ? non-empty-array : array) * @psalm-pure */ @@ -203,6 +245,7 @@ function end(&$array) * * @param TArray $array * + * @psalm-flow ($array) -> return * @return (TArray is array ? null : (TArray is non-empty-array ? key-of : key-of|null)) * @psalm-pure */ @@ -215,6 +258,7 @@ function array_key_first($array) * * @param TArray $array * + * @psalm-flow ($array) -> return * @return (TArray is array ? null : (TArray is non-empty-array ? key-of : key-of|null)) * @psalm-pure */ @@ -222,11 +266,20 @@ function array_key_last($array) { } +/** + * @psalm-flow ($array, $arrays) -> return + * @psalm-pure + */ +function array_map(?callable $callback, array $array, array ...$arrays): array +{ +} + /** * @psalm-template TArray as array * * @param TArray $array * + * @psalm-flow ($array) -> return * @return (TArray is non-empty-array ? non-empty-list> : list>) * @psalm-pure */ @@ -241,6 +294,7 @@ function array_values($array) * @param array $haystack * @param bool $strict * + * @psalm-flow ($haystack) -> return * @return T|false * @psalm-pure */ @@ -327,12 +381,31 @@ function uksort(array &$array, callable $callback): bool * * @param array $array * + * @psalm-flow ($array) -> return * @return array */ function array_change_key_case(array $array, int $case = CASE_LOWER) { } +/** + * @psalm-pure + * + * @psalm-flow ($array) -> return + */ +function array_chunk(array $array, int $length) +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array) -> return + */ +function array_column(array $array, int|string|null $column_key) +{ +} + /** * @psalm-pure * @@ -357,6 +430,7 @@ function array_key_exists($key, array $array) : bool * * @param array ...$arrays * + * @psalm-flow ($arrays) -> return * @return array */ function array_merge_recursive(array ...$arrays) @@ -366,6 +440,152 @@ function array_merge_recursive(array ...$arrays) /** * @psalm-pure * + * @psalm-flow ($array, $value) -> return + */ +function array_pad(array $array, int $length, mixed $value): array +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array) -> return + */ +function array_rand(array $array, int $num = 1): int|string|array +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array, $initial) -> return + */ +function array_reduce(array $array, callable $callback, mixed $initial = null): mixed +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array, $replacements) -> return + */ +function array_replace(array $array, array ...$replacements): array +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array, $replacements) -> return + */ +function array_replace_recursive(array $array, array ...$replacements): array +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array) -> return + */ +function array_reverse(array $array, bool $preserve_keys = false): array +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array) -> return + */ +function array_slice( + array $array, + int $offset, + ?int $length = null, + bool $preserve_keys = false +): array +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array, $replacement) -> return + */ +function array_splice( + array &$array, + int $offset, + ?int $length = null, + mixed $replacement = [] +): array +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array) -> return + */ +function array_udiff(array $array, array ...$arrays, callable $value_compare_func): array +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array) -> return + */ +function array_udiff_assoc(array $array, array ...$arrays, callable $value_compare_func): array +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array) -> return + */ +function array_udiff_uassoc( + array $array, + array ...$arrays, + callable $value_compare_func, + callable $key_compare_func +): array +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array) -> return + */ +function array_uintersect(array $array, array ...$arrays, callable $value_compare_func): array +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array) -> return + */ +function array_uintersect_assoc(array $array, array ...$arrays, callable $value_compare_func): array +{ +} + +/** + * @psalm-pure + * + * @psalm-flow ($array1) -> return + */ +function array_uintersect_uassoc( + array $array1, + array ...$arrays, + callable $value_compare_func, + callable $key_compare_func +): array +{ +} + +/** + * @psalm-pure + * * @no-named-arguments * * @psalm-template TKey as array-key @@ -373,12 +593,21 @@ function array_merge_recursive(array ...$arrays) * * @param array ...$arrays * + * @psalm-flow ($arrays) -> return * @return array */ function array_merge(array ...$arrays) { } +/** + * @psalm-pure + * @psalm-flow ($value) -> return + */ +function array_fill(int $start_index, int $count, mixed $value): array +{ +} + /** * @psalm-pure * @@ -388,12 +617,21 @@ function array_merge(array ...$arrays) * @param array $keys * @param TValue $value * + * @psalm-flow ($value) -> return * @return array */ function array_fill_keys(array $keys, $value): array { } +/** + * @psalm-pure + * @psalm-flow ($array) -> return + */ +function array_filter(array $array, ?callable $callback = null, int $mode = 0): array +{ +} + /** * @psalm-pure * @@ -1390,6 +1628,7 @@ function str_getcsv(string $string, string $separator = ',', string $enclosure = * * @param TArray $array * + * @psalm-flow ($array) -> return * @return (TArray is non-empty-array ? non-empty-array> : array>) * * @psalm-pure diff --git a/tests/TaintTest.php b/tests/TaintTest.php index edaedce57ca..2affadabfb6 100644 --- a/tests/TaintTest.php +++ b/tests/TaintTest.php @@ -753,12 +753,133 @@ function bar(array $arr): void { ]; } + private function getLastFunctionName($lines) + { + $i = count($lines); + do{ + $i--; + $parts = explode('(', $lines[$i]); + }while(count($parts) === 1); + + $functionName = trim(str_replace('return ', '', $parts[0])); + if(empty($functionName)){ + throw new \Exception("Function name detection failed for code: " . implode("\n", $lines)); + } + + return $functionName; + } + + public function buildDataSets($dataSetNamePrefix, $taintType, $codeBlocks) + { + $dataSets = []; + + foreach($codeBlocks as $code){ + $lines = explode("\n", $code); + if(count($lines) > 1){ + $code = "(function(){{$code}})()"; + } + + $functionName = $this->getLastFunctionName($lines); + $dataSetNumber = 0; + do{ + $dataSetNumber++; + $dataSetName = "$dataSetNamePrefix-$functionName-$dataSetNumber"; + }while(isset($items[$dataSetName])); + + $dataSets[$dataSetName] = [ + 'code' => " $taintType, + ]; + }; + + return $dataSets; + } + /** * @return array */ public function providerInvalidCodeParse(): array { - return [ + $arrayFunctionTaintFlowDataSets = $this->buildDataSets('taintFlow', 'TaintedHtml', [ + 'array_change_key_case([$_GET["a"]])', + 'array_chunk([$_GET["a"]], 1)', + 'array_column([[$_GET["a"]]], 0)', + 'array_combine([0], [$_GET["a"]])', + 'array_combine([$_GET["a"]], [0])', + 'array_count_values([$_GET["a"]])', + 'array_diff([$_GET["a"]], [])', + 'array_diff_assoc([$_GET["a"] => 0], [])', + 'array_diff_key([$_GET["a"] => 0], [])', + 'array_diff_uassoc([$_GET["a"]], [], function(){return false;})', + 'array_diff_ukey([$_GET["a"]], [], function(){return false;})', + 'array_fill(0, 1, $_GET["a"])', + 'array_fill_keys([0], $_GET["a"])', + 'array_filter([$_GET["a"]])', + 'array_flip([$_GET["a"]])', + 'array_intersect([$_GET["a"]], [$_GET["a"]])', + 'array_intersect_assoc([$_GET["a"]], [$_GET["a"]])', + 'array_intersect_key([$_GET["a"]], [0])', + 'array_intersect_uassoc([0 => $_GET["a"]], [1 => $_GET["a"]], function(){return 0;})', + 'array_intersect_ukey([0 => $_GET["a"]], [1 => 2], function(){return 0;})', + 'array_keys([$_GET["a"] => 0])', + 'array_key_first([$_GET["a"] => 0])', + 'array_key_last([$_GET["a"] => 0])', + 'array_map(function($a){return $a;}, [$_GET["a"]])', + 'array_map(function($a, $b){return $b;}, [0], [$_GET["a"]])', + 'array_merge([$_GET["a"]])', + 'array_merge_recursive(["b" => ["c" => $_GET["a"]]], ["b" => ["c" => "d"]])', + 'array_pad([$_GET["a"]], 2, 1)', + 'array_pad([], 1, $_GET["a"])', + ' + $a = [$_GET["a"]]; + return array_pop($a); + ', + ' + $a = []; + array_push($a, $_GET["a"]); + return $a; + ', + 'array_rand([$_GET["a"] => 0])', + 'array_reduce([$_GET["a"]], function($carry, $item){return $item;})', + 'array_reduce([], function(){}, $_GET["a"])', + 'array_replace([$_GET["a"]], [])', + 'array_replace([], [$_GET["a"]])', + 'array_replace_recursive([$_GET["a"]], [])', + 'array_replace_recursive([], [$_GET["a"]])', + 'array_reverse([$_GET["a"]])', + 'array_search(0, [$_GET["a"] => 0])', + ' + $a = [$_GET["a"]]; + return array_shift($a); + ', + 'array_slice([$_GET["a"]], 0)', + ' + $a = [$_GET["a"]]; + return array_splice($a, 0); + ', + // This dataset is currently broken, but mmcev106 plans to fix it in an upcoming PR. + // ' + // $a = []; + // array_splice($a, 0, 0, [$_GET["a"]]); + // return $a; + // ', + 'array_udiff([$_GET["a"]], [], function(){return false;})', + 'array_udiff_assoc([$_GET["a"]], [], function(){return false;})', + 'array_udiff_uassoc([$_GET["a"]], [], function(){return false;}, function(){return false;})', + 'array_uintersect([$_GET["a"]], [$_GET["a"]], function(){return 0;})', + 'array_uintersect_assoc([$_GET["a"]], [$_GET["a"]], function(){return 0;})', + 'array_uintersect_uassoc([$_GET["a"]], [$_GET["a"]], function(){return 0;}, function(){return 0;})', + 'array_unique([$_GET["a"]])', + // This dataset is currently broken, but mmcev106 plans to fix it in an upcoming PR. + // ' + // $a = []; + // array_unshift($a, $_GET["a"]); + // return $a; + // ', + 'array_values([$_GET["a"]])', + ]); + + $otherDataSets = [ 'taintedInputFromMethodReturnTypeSimple' => [ 'code' => ' 'TaintedCallable', ], ]; + + $dataSetsArrays = [ + $otherDataSets, + $arrayFunctionTaintFlowDataSets + ]; + + $expectedCount = 0; + foreach($dataSetsArrays as $array){ + $expectedCount += count($array); + } + + $mergedDataSets = array_merge(...$dataSetsArrays); + $this->assertSame($expectedCount, count($mergedDataSets), "Please make sure none of the datasets have duplicate keys!"); + + return $mergedDataSets; } /**