diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php index 5673498f3..fae1d8839 100644 --- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php @@ -48,6 +48,7 @@ class FunctionReturnTypeProvider $this->registerClass(ReturnTypeProvider\IteratorToArrayReturnTypeProvider::class); $this->registerClass(ReturnTypeProvider\ParseUrlReturnTypeProvider::class); $this->registerClass(ReturnTypeProvider\StrReplaceReturnTypeProvider::class); + $this->registerClass(ReturnTypeProvider\StrTrReturnTypeProvider::class); $this->registerClass(ReturnTypeProvider\VersionCompareReturnTypeProvider::class); $this->registerClass(ReturnTypeProvider\MktimeReturnTypeProvider::class); $this->registerClass(ReturnTypeProvider\ExplodeReturnTypeProvider::class); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrTrReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrTrReturnTypeProvider.php new file mode 100644 index 000000000..4913ea330 --- /dev/null +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrTrReturnTypeProvider.php @@ -0,0 +1,69 @@ + $call_args + */ + public static function getFunctionReturnType( + StatementsSource $statements_source, + string $function_id, + array $call_args, + Context $context, + CodeLocation $code_location + ) : Type\Union { + if (!$statements_source instanceof \Psalm\Internal\Analyzer\StatementsAnalyzer) { + throw new \UnexpectedValueException(); + } + + $type = Type::getString(); + + if ($statements_source->data_flow_graph + && !\in_array('TaintedInput', $statements_source->getSuppressedIssues())) { + $function_return_sink = DataFlowNode::getForMethodReturn( + $function_id, + $function_id, + null, + $code_location + ); + + $statements_source->data_flow_graph->addNode($function_return_sink); + foreach ($call_args as $i => $_) { + $function_param_sink = DataFlowNode::getForMethodArgument( + $function_id, + $function_id, + $i, + null, + $code_location + ); + + $statements_source->data_flow_graph->addNode($function_param_sink); + + $statements_source->data_flow_graph->addPath( + $function_param_sink, + $function_return_sink, + 'arg' + ); + } + + $type->parent_nodes = [$function_return_sink->id => $function_return_sink]; + } + + return $type; + } +} diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index b43299af9..ee5db6b81 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -484,6 +484,20 @@ function strtolower(string $str) : string {} */ function strtoupper(string $str) : string {} +/** + * @psalm-pure + * + * @param string|array $string + * @param string|array $replacement + * @param int|array $start + * @param null|int|array $length + * + * @return ($string is array ? array : string) + * + * @psalm-flow ($string, $replacement) -> return + */ +function substr_replace($string, $replacement, $start, $length) {} + /** * @psalm-pure * @@ -610,6 +624,20 @@ function array_product(array $input) {} */ function strip_tags(string $str, ?string $allowable_tags = null) : string {} +/** + * @psalm-pure + * + * @psalm-flow ($str) -> return + */ +function stripcslashes(string $str) : string {} + +/** + * @psalm-pure + * + * @psalm-flow ($str) -> return + */ +function stripslashes(string $str) : string {} + /** * @psalm-pure * @@ -661,6 +689,104 @@ function htmlspecialchars_decode(string $string, ?int $quote_style = null) : str */ function str_replace($search, $replace, $subject, &$count = null) {} +/** + * @psalm-pure + * + * @param string|array $search + * @param string|array $replace + * @param string|array $subject + * @param int $count + * @return ($subject is array ? array : string) + * + * @psalm-flow ($replace, $subject) -> return + */ +function str_ireplace($search, $replace, $subject, &$count = null) {} + +/** + * @psalm-pure + * + * @psalm-flow ($input, $pad_string) -> return + */ +function str_pad(string $input, int $pad_length, $pad_string = '', int $pad_type = STR_PAD_RIGHT): string {} + +/** + * @psalm-pure + * + * @psalm-flow ($input) -> return + */ +function str_repeat(string $input, int $multiplier): string {} + +/** + * @psalm-pure + * + * @psalm-flow ($str) -> return + */ +function str_rot13(string $str): string {} + +/** + * @psalm-pure + * + * @psalm-flow ($str) -> return + */ +function str_shuffle(string $str): string {} + +/** + * @psalm-pure + * @return array + * + * @psalm-flow ($string) -> return + */ +function str_split(string $string, int $split_length = 1) {} + +/** + * @psalm-pure + * + * @psalm-flow ($haystack) -> return + */ +function strstr(string $haystack, string $needle, bool $before_needle = false): string {} + +/** + * @psalm-pure + * + * @psalm-flow ($haystack) -> return + */ +function stristr(string $haystack, string $needle, bool $before_needle = false): string {} + +/** + * @psalm-pure + * + * @psalm-flow ($haystack) -> return + */ +function strchr(string $haystack, string $needle, bool $before_needle = false): string {} + +/** + * @psalm-pure + * + * @psalm-flow ($haystack) -> return + */ +function strpbrk(string $haystack, string $char_list): string {} + +/** + * @psalm-pure + * + * @psalm-flow ($haystack) -> return + */ +function strrchr(string $haystack, string $needle): string {} + +/** + * @psalm-pure + * + * @psalm-flow ($string) -> return + */ +function strrev(string $string): string {} + +/** + * @psalm-pure + * + * @psalm-flow ($string) -> return + */ +function strtok(string $str, string $token): string {} + /** * @psalm-pure * @@ -681,7 +807,7 @@ function preg_filter($pattern, $replacement, $subject, int $limit = -1, &$count * @param string|array $replace * @param string|array $subject * @param int $count - * @return ($subject is array ? array : string) + * @return ($subject is array ? array|null : string|null) * * @psalm-flow ($replace, $subject) -> return */ @@ -692,7 +818,7 @@ function preg_replace($search, $replace, $subject, int $limit = -1, &$count = nu * @param callable(array):string $replace * @param string|array $subject * @param int $count - * @return ($subject is array ? array : string) + * @return ($subject is array ? array|null : string|null) * * @psalm-taint-specialize * @psalm-flow ($subject) -> return @@ -761,6 +887,23 @@ function preg_quote(string $str, ?string $delimiter = null) : string {} */ function sprintf(string $format, ...$args) : string {} +/** + * @psalm-pure + * @return string|false + * @psalm-ignore-falsable-return + * + * @psalm-flow ($format, $args) -> return + */ +function vsprintf(string $format, array $args) {} + +/** + * @psalm-pure + * @return string + * + * @psalm-flow ($str) -> return + */ +function wordwrap(string $str, int $width = 75, string $break = "\n", bool $cut = false) : string {} + /** * @psalm-pure * @@ -802,6 +945,8 @@ function strval ($var): string {} /** * @return ($input is non-empty-string ? non-empty-list : non-empty-list|array{null}) * @psalm-pure + * + * @psalm-flow ($input) -> return */ function str_getcsv(string $input, string $delimiter = ',', string $enclosure = '"', string $escape = '\\\\') { diff --git a/tests/TaintTest.php b/tests/TaintTest.php index 06d7ae688..a512dc755 100644 --- a/tests/TaintTest.php +++ b/tests/TaintTest.php @@ -557,6 +557,11 @@ class TaintTest extends TestCase echo $file;' ], + 'strTrNotTainted' => [ + 'getTaint();', 'error_message' => 'TaintedHtml', ], + 'strTrReturnTypeTaint' => [ + ' 'TaintedCookie', + ], /* // TODO: Stubs do not support this type of inference even with $this->message = $message. // Most uses of getMessage() would be with caught exceptions, so this is not representative of real code.