2022-01-13 13:02:46 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
declare (strict_types=1);
|
2022-06-06 17:12:56 +00:00
|
|
|
namespace Rector\Php81\Rector\FuncCall;
|
2022-01-13 13:02:46 +00:00
|
|
|
|
2022-06-06 17:12:56 +00:00
|
|
|
use PhpParser\Node;
|
|
|
|
use PhpParser\Node\Arg;
|
|
|
|
use PhpParser\Node\Expr;
|
|
|
|
use PhpParser\Node\Expr\Assign;
|
|
|
|
use PhpParser\Node\Expr\Cast\String_ as CastString_;
|
|
|
|
use PhpParser\Node\Expr\ConstFetch;
|
|
|
|
use PhpParser\Node\Expr\FuncCall;
|
|
|
|
use PhpParser\Node\Expr\MethodCall;
|
|
|
|
use PhpParser\Node\Identifier;
|
2022-08-09 20:47:13 +00:00
|
|
|
use PhpParser\Node\Scalar\Encapsed;
|
2022-06-06 17:12:56 +00:00
|
|
|
use PhpParser\Node\Scalar\String_;
|
|
|
|
use PhpParser\Node\Stmt\Trait_;
|
|
|
|
use PHPStan\Analyser\Scope;
|
|
|
|
use PHPStan\Reflection\Native\NativeFunctionReflection;
|
|
|
|
use PHPStan\Type\ErrorType;
|
|
|
|
use PHPStan\Type\MixedType;
|
|
|
|
use Rector\Core\NodeAnalyzer\ArgsAnalyzer;
|
|
|
|
use Rector\Core\Rector\AbstractRector;
|
|
|
|
use Rector\Core\Reflection\ReflectionResolver;
|
|
|
|
use Rector\Core\ValueObject\PhpVersionFeature;
|
|
|
|
use Rector\NodeTypeResolver\Node\AttributeKey;
|
2022-07-02 21:18:00 +00:00
|
|
|
use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper;
|
2022-06-06 17:12:56 +00:00
|
|
|
use Rector\Php73\NodeTypeAnalyzer\NodeTypeAnalyzer;
|
|
|
|
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
|
2022-06-07 09:18:30 +00:00
|
|
|
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
|
|
|
|
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
|
2022-01-13 13:02:46 +00:00
|
|
|
/**
|
|
|
|
* @see \Rector\Tests\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector\NullToStrictStringFuncCallArgRectorTest
|
|
|
|
*/
|
2022-06-07 08:22:29 +00:00
|
|
|
final class NullToStrictStringFuncCallArgRector extends AbstractRector implements MinPhpVersionInterface
|
2022-01-13 13:02:46 +00:00
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @var array<string, string[]>
|
|
|
|
*/
|
2022-06-21 10:27:51 +00:00
|
|
|
private const ARG_POSITION_NAME_NULL_TO_STRICT_STRING = ['preg_split' => ['subject'], 'preg_match' => ['subject'], 'preg_match_all' => ['subject'], 'explode' => ['string'], 'strlen' => ['string'], 'str_contains' => ['haystack', 'needle'], 'strtotime' => ['datetime'], 'str_replace' => ['subject'], 'substr' => ['string'], 'str_starts_with' => ['haystack', 'needle'], 'strtoupper' => ['string'], 'strtolower' => ['string'], 'strpos' => ['haystack', 'needle'], 'stripos' => ['haystack', 'needle'], 'json_decode' => ['json'], 'urlencode' => ['string'], 'urldecode' => ['string'], 'rawurlencode' => ['string'], 'rawurldecode' => ['string'], 'base64_encode' => ['string'], 'base64_decode' => ['string'], 'utf8_encode' => ['string'], 'utf8_decode' => ['string'], 'bin2hex' => ['string'], 'hex2bin' => ['string'], 'hexdec' => ['hex_string'], 'octdec' => ['octal_string'], 'base_convert' => ['num'], 'htmlspecialchars' => ['string'], 'htmlspecialchars_decode' => ['string'], 'html_entity_decode' => ['string'], 'htmlentities' => ['string'], 'addslashes' => ['string'], 'addcslashes' => ['string', 'characters'], 'stripslashes' => ['string'], 'stripcslashes' => ['string'], 'quotemeta' => ['string'], 'quoted_printable_decode' => ['string'], 'quoted_printable_encode' => ['string'], 'escapeshellarg' => ['arg'], 'curl_escape' => ['string'], 'curl_unescape' => ['string'], 'convert_uuencode' => ['string'], 'zlib_encode' => ['data'], 'gzdeflate' => ['data'], 'gzencode' => ['data'], 'gzcompress' => ['data'], 'gzwrite' => ['data'], 'gzputs' => ['data'], 'deflate_add' => ['data'], 'inflate_add' => ['data'], 'unpack' => ['format', 'string'], 'iconv_mime_encode' => ['field_name', 'field_value'], 'iconv_mime_decode' => ['string'], 'iconv' => ['from_encoding', 'to_encoding', 'string'], 'sodium_bin2hex' => ['string'], 'sodium_hex2bin' => ['string', 'ignore'], 'sodium_bin2base64' => ['string'], 'sodium_base642bin' => ['string', 'ignore'], 'mb_detect_encoding' => ['string'], 'mb_encode_mimeheader' => ['string'], 'mb_decode_mimeheader' => ['string'], 'mb_encode_numericentity' => ['string'], 'mb_decode_numericentity' => ['string'], 'transliterator_transliterate' => ['string'], 'mysqli_real_escape_string' => ['string'], 'mysqli_escape_string' => ['string'], 'ucfirst' => ['string'], 'lcfirst' => ['string'], 'ucwords' => ['string'], 'trim' => ['string'], 'ltrim' => ['string'], 'rtrim' => ['string'], 'chop' => ['string'], 'str_rot13' => ['string'], 'str_shuffle' => ['string'], 'substr_count' => ['haystack', 'needle'], 'strcoll' => ['string1', 'string2'], 'str_split' => ['string'], 'chunk_split' => ['string'], 'wordwrap' => ['string'], 'strrev' => ['string'], 'str_repeat' => ['string'], 'str_pad' => ['string'], 'nl2br' => ['string'], 'strip_tags' => ['string'], 'hebrev' => ['string'], 'iconv_substr' => ['string'], 'mb_strtoupper' => ['string'], 'mb_strtolower' => ['string'], 'mb_convert_case' => ['string'], 'mb_convert_kana' => ['string'], 'mb_scrub' => ['string'], 'mb_substr' => ['string'], 'mb_substr_count' => ['haystack', 'needle'], 'mb_str_split' => ['string'], 'mb_split' => ['pattern', 'string'], 'sodium_pad' => ['string'], 'grapheme_substr' => ['string'], 'strrpos' => ['haystack', 'needle'], 'strripos' => ['haystack', 'needle'], 'iconv_strpos' => ['haystack', 'needle'], 'iconv_strrpos' => ['haystack', 'needle'], 'mb_strpos' => ['haystack', 'needle'], 'mb_strrpos' => ['haystack', 'needle'], 'mb_stripos' => ['haystack', 'needle'], 'mb_strripos' => ['haystack', 'needle'], 'grapheme_strpos' => ['haystack', 'needle'], 'grapheme_strrpos' => ['haystack', 'needle'], 'grapheme_stripos' => ['haystack', 'needle'], 'grapheme_strripos' => ['haystack', 'needle'], 'strcmp' => ['string1', 'string2'], 'strncmp' => ['string1', 'string2'], 'strcasecmp' => ['string1', 'string2'], 'strncasecmp' => ['string1', 'string2'], 'strnatcmp' => ['string1', 'string2'], 'strnatcasecmp' => ['string1', 'string2'], 'substr_compare' => ['haystack', 'needle'], 'str_ends_with' => ['haystack', 'needle'], 'collator_compare' => ['string1', 'string2'], 'collator_get_sort_key' => ['string'], 'metaphone
|
2022-01-13 13:02:46 +00:00
|
|
|
/**
|
|
|
|
* @readonly
|
|
|
|
* @var \Rector\Core\Reflection\ReflectionResolver
|
|
|
|
*/
|
|
|
|
private $reflectionResolver;
|
|
|
|
/**
|
|
|
|
* @readonly
|
|
|
|
* @var \Rector\Core\NodeAnalyzer\ArgsAnalyzer
|
|
|
|
*/
|
|
|
|
private $argsAnalyzer;
|
|
|
|
/**
|
|
|
|
* @readonly
|
|
|
|
* @var \Rector\Php73\NodeTypeAnalyzer\NodeTypeAnalyzer
|
|
|
|
*/
|
|
|
|
private $nodeTypeAnalyzer;
|
2022-06-07 08:22:29 +00:00
|
|
|
public function __construct(ReflectionResolver $reflectionResolver, ArgsAnalyzer $argsAnalyzer, NodeTypeAnalyzer $nodeTypeAnalyzer)
|
2022-01-13 13:02:46 +00:00
|
|
|
{
|
|
|
|
$this->reflectionResolver = $reflectionResolver;
|
|
|
|
$this->argsAnalyzer = $argsAnalyzer;
|
|
|
|
$this->nodeTypeAnalyzer = $nodeTypeAnalyzer;
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
public function getRuleDefinition() : RuleDefinition
|
2022-01-13 13:02:46 +00:00
|
|
|
{
|
2022-06-07 08:22:29 +00:00
|
|
|
return new RuleDefinition('Change null to strict string defined function call args', [new CodeSample(<<<'CODE_SAMPLE'
|
2022-01-13 13:02:46 +00:00
|
|
|
class SomeClass
|
|
|
|
{
|
|
|
|
public function run()
|
|
|
|
{
|
|
|
|
preg_split("#a#", null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
CODE_SAMPLE
|
|
|
|
, <<<'CODE_SAMPLE'
|
|
|
|
class SomeClass
|
|
|
|
{
|
|
|
|
public function run()
|
|
|
|
{
|
|
|
|
preg_split("#a#", '');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
CODE_SAMPLE
|
|
|
|
)]);
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* @return array<class-string<Node>>
|
|
|
|
*/
|
|
|
|
public function getNodeTypes() : array
|
|
|
|
{
|
2022-06-07 08:22:29 +00:00
|
|
|
return [FuncCall::class];
|
2022-01-13 13:02:46 +00:00
|
|
|
}
|
|
|
|
/**
|
|
|
|
* @param FuncCall $node
|
|
|
|
*/
|
2022-06-07 08:22:29 +00:00
|
|
|
public function refactor(Node $node) : ?Node
|
2022-01-13 13:02:46 +00:00
|
|
|
{
|
|
|
|
if ($this->shouldSkip($node)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
$args = $node->getArgs();
|
|
|
|
$positions = $this->argsAnalyzer->hasNamedArg($args) ? $this->resolveNamedPositions($node, $args) : $this->resolveOriginalPositions($node);
|
|
|
|
if ($positions === []) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
$isChanged = \false;
|
|
|
|
foreach ($positions as $position) {
|
|
|
|
$result = $this->processNullToStrictStringOnNodePosition($node, $args, $position);
|
2022-06-07 08:22:29 +00:00
|
|
|
if ($result instanceof Node) {
|
2022-01-13 13:02:46 +00:00
|
|
|
$node = $result;
|
|
|
|
$isChanged = \true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($isChanged) {
|
|
|
|
return $node;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
public function provideMinPhpVersion() : int
|
|
|
|
{
|
2022-06-07 08:22:29 +00:00
|
|
|
return PhpVersionFeature::DEPRECATE_NULL_ARG_IN_STRING_FUNCTION;
|
2022-01-13 13:02:46 +00:00
|
|
|
}
|
|
|
|
/**
|
|
|
|
* @param Arg[] $args
|
|
|
|
* @return int[]|string[]
|
|
|
|
*/
|
2022-06-07 08:22:29 +00:00
|
|
|
private function resolveNamedPositions(FuncCall $funcCall, array $args) : array
|
2022-01-13 13:02:46 +00:00
|
|
|
{
|
|
|
|
$functionName = $this->nodeNameResolver->getName($funcCall);
|
|
|
|
$argNames = self::ARG_POSITION_NAME_NULL_TO_STRICT_STRING[$functionName];
|
|
|
|
$positions = [];
|
|
|
|
foreach ($args as $position => $arg) {
|
2022-06-07 08:22:29 +00:00
|
|
|
if (!$arg->name instanceof Identifier) {
|
2022-01-13 13:02:46 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (!$this->nodeNameResolver->isNames($arg->name, $argNames)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$positions[] = $position;
|
|
|
|
}
|
|
|
|
return $positions;
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* @param Arg[] $args
|
|
|
|
* @param int|string $position
|
|
|
|
*/
|
2022-06-07 08:22:29 +00:00
|
|
|
private function processNullToStrictStringOnNodePosition(FuncCall $funcCall, array $args, $position) : ?FuncCall
|
2022-01-13 13:02:46 +00:00
|
|
|
{
|
2022-08-15 18:34:46 +00:00
|
|
|
if (!isset($args[$position])) {
|
|
|
|
return null;
|
|
|
|
}
|
2022-01-13 13:02:46 +00:00
|
|
|
$argValue = $args[$position]->value;
|
2022-06-07 08:22:29 +00:00
|
|
|
if ($argValue instanceof ConstFetch && $this->valueResolver->isNull($argValue)) {
|
|
|
|
$args[$position]->value = new String_('');
|
2022-01-13 13:02:46 +00:00
|
|
|
$funcCall->args = $args;
|
|
|
|
return $funcCall;
|
|
|
|
}
|
|
|
|
$type = $this->nodeTypeResolver->getType($argValue);
|
|
|
|
if ($this->nodeTypeAnalyzer->isStringyType($type)) {
|
|
|
|
return null;
|
|
|
|
}
|
2022-08-15 18:34:46 +00:00
|
|
|
if (!$type instanceof MixedType || $argValue instanceof Encapsed) {
|
2022-08-09 20:47:13 +00:00
|
|
|
return null;
|
|
|
|
}
|
2022-06-20 14:51:15 +00:00
|
|
|
if ($this->isAnErrorTypeFromParentScope($argValue)) {
|
2022-01-13 13:02:46 +00:00
|
|
|
return null;
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
if ($args[$position]->value instanceof MethodCall) {
|
|
|
|
$trait = $this->betterNodeFinder->findParentType($funcCall, Trait_::class);
|
|
|
|
if ($trait instanceof Trait_) {
|
2022-01-13 13:02:46 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($this->isCastedReassign($argValue)) {
|
|
|
|
return null;
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
$args[$position]->value = new CastString_($argValue);
|
2022-01-13 13:02:46 +00:00
|
|
|
$funcCall->args = $args;
|
|
|
|
return $funcCall;
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
private function isCastedReassign(Expr $expr) : bool
|
2022-01-13 13:02:46 +00:00
|
|
|
{
|
2022-06-07 08:22:29 +00:00
|
|
|
return (bool) $this->betterNodeFinder->findFirstPrevious($expr, function (Node $subNode) use($expr) : bool {
|
|
|
|
if (!$subNode instanceof Assign) {
|
2022-01-13 13:02:46 +00:00
|
|
|
return \false;
|
|
|
|
}
|
|
|
|
if (!$this->nodeComparator->areNodesEqual($subNode->var, $expr)) {
|
|
|
|
return \false;
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
return $subNode->expr instanceof CastString_;
|
2022-01-13 13:02:46 +00:00
|
|
|
});
|
|
|
|
}
|
2022-06-20 14:51:15 +00:00
|
|
|
private function isAnErrorTypeFromParentScope(Expr $expr) : bool
|
2022-01-13 13:02:46 +00:00
|
|
|
{
|
2022-06-07 08:22:29 +00:00
|
|
|
$scope = $expr->getAttribute(AttributeKey::SCOPE);
|
|
|
|
if (!$scope instanceof Scope) {
|
2022-01-13 13:02:46 +00:00
|
|
|
return \false;
|
|
|
|
}
|
|
|
|
$parentScope = $scope->getParentScope();
|
2022-06-07 08:22:29 +00:00
|
|
|
if ($parentScope instanceof Scope) {
|
|
|
|
return $parentScope->getType($expr) instanceof ErrorType;
|
2022-01-13 13:02:46 +00:00
|
|
|
}
|
|
|
|
return \false;
|
|
|
|
}
|
|
|
|
/**
|
|
|
|
* @return int[]|string[]
|
|
|
|
*/
|
2022-06-07 08:22:29 +00:00
|
|
|
private function resolveOriginalPositions(FuncCall $funcCall) : array
|
2022-01-13 13:02:46 +00:00
|
|
|
{
|
|
|
|
$functionReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($funcCall);
|
2022-06-07 08:22:29 +00:00
|
|
|
if (!$functionReflection instanceof NativeFunctionReflection) {
|
2022-01-13 13:02:46 +00:00
|
|
|
return [];
|
|
|
|
}
|
2022-07-02 21:18:00 +00:00
|
|
|
$scope = $funcCall->getAttribute(AttributeKey::SCOPE);
|
|
|
|
if (!$scope instanceof Scope) {
|
|
|
|
return [];
|
|
|
|
}
|
2022-07-05 07:33:24 +00:00
|
|
|
$parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($functionReflection, $funcCall, $scope);
|
2022-01-13 13:02:46 +00:00
|
|
|
$functionName = $this->nodeNameResolver->getName($funcCall);
|
|
|
|
$argNames = self::ARG_POSITION_NAME_NULL_TO_STRICT_STRING[$functionName];
|
|
|
|
$positions = [];
|
|
|
|
foreach ($parametersAcceptor->getParameters() as $position => $parameterReflection) {
|
|
|
|
if (\in_array($parameterReflection->getName(), $argNames, \true)) {
|
|
|
|
$positions[] = $position;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $positions;
|
|
|
|
}
|
2022-06-07 08:22:29 +00:00
|
|
|
private function shouldSkip(FuncCall $funcCall) : bool
|
2022-01-13 13:02:46 +00:00
|
|
|
{
|
|
|
|
$functionNames = \array_keys(self::ARG_POSITION_NAME_NULL_TO_STRICT_STRING);
|
2022-09-07 13:03:58 +00:00
|
|
|
return !$this->nodeNameResolver->isNames($funcCall, $functionNames) || $funcCall->isFirstClassCallable();
|
2022-01-13 13:02:46 +00:00
|
|
|
}
|
|
|
|
}
|