Skip to content

Commit

Permalink
Add psalm-flow for string functions from sscanf to wordwrap (#4591)
Browse files Browse the repository at this point in the history
* Add string functions from sscanf to wordwrap

This should conclude all string functions from https://www.php.net/manual/en/book.strings.php

Continuation of #4576

Ref #3636

* Add StrTrReturnTypeProvider

* Fix psalm error

* phpcs

* Line length

* Ignore false return on vsprintf

Co-authored-by: Matthew Brown <github@muglug.com>
  • Loading branch information
LukasReschke and muglug committed Nov 21, 2020
1 parent bf873b2 commit 3943b55
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php
Expand Up @@ -48,6 +48,7 @@ public function __construct()
$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);
Expand Down
@@ -0,0 +1,69 @@
<?php
namespace Psalm\Internal\Provider\ReturnTypeProvider;

use PhpParser;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Internal\DataFlow\DataFlowNode;

class StrTrReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTypeProviderInterface
{
public static function getFunctionIds() : array
{
return [
'strtr',
];
}

/**
* @param list<PhpParser\Node\Arg> $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;
}
}
149 changes: 147 additions & 2 deletions stubs/CoreGenericFunctions.phpstub
Expand Up @@ -484,6 +484,20 @@ function strtolower(string $str) : string {}
*/
function strtoupper(string $str) : string {}

/**
* @psalm-pure
*
* @param string|array<string> $string
* @param string|array<string> $replacement
* @param int|array<int> $start
* @param null|int|array<int> $length
*
* @return ($string is array<string> ? array<string> : string)
*
* @psalm-flow ($string, $replacement) -> return
*/
function substr_replace($string, $replacement, $start, $length) {}

/**
* @psalm-pure
*
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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<string|int|float> $search
* @param string|array<string|int|float> $replace
* @param string|array<string|int|float> $subject
* @param int $count
* @return ($subject is array ? array<string> : 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<string>
*
* @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
*
Expand All @@ -681,7 +807,7 @@ function preg_filter($pattern, $replacement, $subject, int $limit = -1, &$count
* @param string|array<string|int|float> $replace
* @param string|array<string|int|float> $subject
* @param int $count
* @return ($subject is array ? array<string> : string)
* @return ($subject is array ? array<string>|null : string|null)
*
* @psalm-flow ($replace, $subject) -> return
*/
Expand All @@ -692,7 +818,7 @@ function preg_replace($search, $replace, $subject, int $limit = -1, &$count = nu
* @param callable(array<int, string>):string $replace
* @param string|array<string|int|float> $subject
* @param int $count
* @return ($subject is array ? array<string> : string)
* @return ($subject is array ? array<string>|null : string|null)
*
* @psalm-taint-specialize
* @psalm-flow ($subject) -> return
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -802,6 +945,8 @@ function strval ($var): string {}
/**
* @return ($input is non-empty-string ? non-empty-list<string> : non-empty-list<string>|array{null})
* @psalm-pure
*
* @psalm-flow ($input) -> return
*/
function str_getcsv(string $input, string $delimiter = ',', string $enclosure = '"', string $escape = '\\\\')
{
Expand Down
11 changes: 11 additions & 0 deletions tests/TaintTest.php
Expand Up @@ -557,6 +557,11 @@ public function getTaint() : string {
echo $file;'
],
'strTrNotTainted' => [
'<?php
$input = strtr(\'data\', \'data\', \'data\');
setcookie($input, \'value\');',
],
];
}

Expand Down Expand Up @@ -1862,6 +1867,12 @@ public function getTaint() : string {
echo $a->getTaint();',
'error_message' => 'TaintedHtml',
],
'strTrReturnTypeTaint' => [
'<?php
$input = strtr(\'data\', $_GET[\'taint\'], \'data\');
setcookie($input, \'value\');',
'error_message' => '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.
Expand Down

0 comments on commit 3943b55

Please sign in to comment.