22
33namespace PHPStan \Type \Php ;
44
5+ use PhpParser \Node \Expr ;
6+ use PhpParser \Node \Expr \BinaryOp \Concat ;
7+ use PhpParser \Node \Expr \ClassConstFetch ;
58use PhpParser \Node \Expr \FuncCall ;
9+ use PhpParser \Node \Identifier ;
10+ use PhpParser \Node \InterpolatedStringPart ;
11+ use PhpParser \Node \Scalar \InterpolatedString ;
612use PHPStan \Analyser \Scope ;
713use PHPStan \Analyser \SpecifiedTypes ;
814use PHPStan \Analyser \TypeSpecifier ;
1319use PHPStan \Type \FunctionTypeSpecifyingExtension ;
1420use PHPStan \Type \MixedType ;
1521use function count ;
22+ use function preg_match ;
23+ use function strtolower ;
1624
1725#[AutowiredService]
1826final class DefinedConstantTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
@@ -50,11 +58,16 @@ public function specifyTypes(
5058 $ args = $ node ->getArgs ();
5159 $ constantNames = $ scope ->getType ($ args [0 ]->value )->getConstantStrings ();
5260
53- if (count ($ constantNames ) !== 1 || $ constantNames [0 ]->getValue () === '' ) {
54- return new SpecifiedTypes ([], []);
61+ if (count ($ constantNames ) === 1 ) {
62+ if ($ constantNames [0 ]->getValue () === '' ) {
63+ return new SpecifiedTypes ([], []);
64+ }
65+
66+ $ expr = $ this ->constantHelper ->createExprFromConstantName ($ constantNames [0 ]->getValue ());
67+ } else {
68+ $ expr = $ this ->createClassConstFetchFromDynamicName ($ args [0 ]->value , $ scope );
5569 }
5670
57- $ expr = $ this ->constantHelper ->createExprFromConstantName ($ constantNames [0 ]->getValue ());
5871 if ($ expr === null ) {
5972 return new SpecifiedTypes ([], []);
6073 }
@@ -67,4 +80,44 @@ public function specifyTypes(
6780 );
6881 }
6982
83+ /**
84+ * Recognizes `$class . '::NAME'`, `"$class::NAME"` and `"{$class}::NAME"`
85+ * so that the guarded `$class::NAME` fetch is narrowed.
86+ */
87+ private function createClassConstFetchFromDynamicName (Expr $ nameExpr , Scope $ scope ): ?ClassConstFetch
88+ {
89+ if ($ nameExpr instanceof Concat) {
90+ $ classExpr = $ nameExpr ->left ;
91+ $ suffixes = $ scope ->getType ($ nameExpr ->right )->getConstantStrings ();
92+ if (count ($ suffixes ) !== 1 ) {
93+ return null ;
94+ }
95+ $ suffix = $ suffixes [0 ]->getValue ();
96+ } elseif (
97+ $ nameExpr instanceof InterpolatedString
98+ && count ($ nameExpr ->parts ) === 2
99+ && $ nameExpr ->parts [0 ] instanceof Expr
100+ && $ nameExpr ->parts [1 ] instanceof InterpolatedStringPart
101+ ) {
102+ $ classExpr = $ nameExpr ->parts [0 ];
103+ $ suffix = $ nameExpr ->parts [1 ]->value ;
104+ } else {
105+ return null ;
106+ }
107+
108+ if (!$ scope ->getType ($ classExpr )->isString ()->yes ()) {
109+ return null ;
110+ }
111+
112+ if (preg_match ('/^::([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)$/ ' , $ suffix , $ matches ) !== 1 ) {
113+ return null ;
114+ }
115+
116+ if (strtolower ($ matches [1 ]) === 'class ' ) {
117+ return null ;
118+ }
119+
120+ return new ClassConstFetch ($ classExpr , new Identifier ($ matches [1 ]));
121+ }
122+
70123}
0 commit comments