chengkun
2025-09-05 4822304b63e1bd6327860af7f3db0133cecf167f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<?php declare(strict_types=1);
 
namespace Composer\Pcre\PHPStan;
 
use Composer\Pcre\Preg;
use Composer\Pcre\Regex;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\Native\NativeParameterReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ClosureType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Php\RegexArrayShapeMatcher;
use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
use PHPStan\Type\StringType;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\Type;
 
final class PregReplaceCallbackClosureTypeExtension implements StaticMethodParameterClosureTypeExtension
{
    /**
     * @var RegexArrayShapeMatcher
     */
    private $regexShapeMatcher;
 
    public function __construct(RegexArrayShapeMatcher $regexShapeMatcher)
    {
        $this->regexShapeMatcher = $regexShapeMatcher;
    }
 
    public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
    {
        return in_array($methodReflection->getDeclaringClass()->getName(), [Preg::class, Regex::class], true)
            && in_array($methodReflection->getName(), ['replaceCallback', 'replaceCallbackStrictGroups'], true)
            && $parameter->getName() === 'replacement';
    }
 
    public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type
    {
        $args = $methodCall->getArgs();
        $patternArg = $args[0] ?? null;
        $flagsArg = $args[5] ?? null;
 
        if (
            $patternArg === null
        ) {
            return null;
        }
 
        $flagsType = PregMatchFlags::getType($flagsArg, $scope);
 
        $matchesType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope);
        if ($matchesType === null) {
            return null;
        }
 
        if ($methodReflection->getName() === 'replaceCallbackStrictGroups' && count($matchesType->getConstantArrays()) === 1) {
            $matchesType = $matchesType->getConstantArrays()[0];
            $matchesType = new ConstantArrayType(
                $matchesType->getKeyTypes(),
                array_map(static function (Type $valueType): Type {
                    if (count($valueType->getConstantArrays()) === 1) {
                        $valueTypeArray = $valueType->getConstantArrays()[0];
                        return new ConstantArrayType(
                            $valueTypeArray->getKeyTypes(),
                            array_map(static function (Type $valueType): Type {
                                return TypeCombinator::removeNull($valueType);
                            }, $valueTypeArray->getValueTypes()),
                            $valueTypeArray->getNextAutoIndexes(),
                            [],
                            $valueTypeArray->isList()
                        );
                    }
                    return TypeCombinator::removeNull($valueType);
                }, $matchesType->getValueTypes()),
                $matchesType->getNextAutoIndexes(),
                [],
                $matchesType->isList()
            );
        }
 
        return new ClosureType(
            [
                new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), $matchesType, $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue()),
            ],
            new StringType()
        );
    }
}