From 42764f8b765c105628863147920c2b93b079406d Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Tue, 27 Oct 2026 06:05:20 +0000 Subject: [PATCH 1/4] Do not use PHPDoc type of overridable class constants as their native type Amp-Thread-ID: https://ampcode.com/threads/T-01a179f2-995f-7745-8d8c-4c174ba5c0ce --- .../ExprHandler/ClassConstFetchHandler.php | 1 + .../InitializerExprTypeResolver.php | 28 +++--- .../nsrt/class-constant-native-type.php | 45 +++++++++ turbo-ext/src/ClassConstFetchHandler.cpp | 8 +- turbo-ext/src/InitializerExprTypeResolver.cpp | 47 ++++++---- .../generated/InitializerExprTypeResolver.h | 94 ++++++++++--------- turbo-ext/src/support.h | 2 +- 7 files changed, 147 insertions(+), 78 deletions(-) diff --git a/src/Analyser/ExprHandler/ClassConstFetchHandler.php b/src/Analyser/ExprHandler/ClassConstFetchHandler.php index 7b1a8afb20a..aff729cfbbc 100644 --- a/src/Analyser/ExprHandler/ClassConstFetchHandler.php +++ b/src/Analyser/ExprHandler/ClassConstFetchHandler.php @@ -113,6 +113,7 @@ static function (Expr $e) use ($classResult, $nativeTypesPromoted): Type { return $nativeTypesPromoted ? $classResult->getNativeType() : $classResult->getType(); }, + $nativeTypesPromoted, ); }, specifyTypesCallback: fn (TypeSpecifierContext $context, bool $nativeTypesPromoted) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 44a199ec363..abf5384f2a4 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -2682,9 +2682,12 @@ private static function shiftLeftOverflows(int $value, int $shift): bool } /** + * With $nativeTypesPromoted, PHPDoc types of the constants are ignored: + * a constant that can be overridden in a child class resolves only to its native type. + * * @param callable(Expr): Type $getTypeCallback */ - public function getClassConstFetchTypeByReflection(Name|Expr $class, string $constantName, ?ClassReflection $classReflection, callable $getTypeCallback): Type + public function getClassConstFetchTypeByReflection(Name|Expr $class, string $constantName, ?ClassReflection $classReflection, callable $getTypeCallback, bool $nativeTypesPromoted = false): Type { $isObject = false; if ($class instanceof Name) { @@ -2762,8 +2765,9 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con continue; } - if (!$isObject && array_key_exists($resolvingName, $this->classConstantValueTypeCache)) { - $types[] = $this->classConstantValueTypeCache[$resolvingName]; + $cacheKey = $nativeTypesPromoted ? $resolvingName . '#native' : $resolvingName; + if (!$isObject && array_key_exists($cacheKey, $this->classConstantValueTypeCache)) { + $types[] = $this->classConstantValueTypeCache[$cacheKey]; continue; } @@ -2786,9 +2790,9 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con $constantName, $constantType, $nativeType, - $constantClassReflection->getConstantPhpDocType($constantName), + $nativeTypesPromoted ? null : $constantClassReflection->getConstantPhpDocType($constantName), ), $fetchedOnType); - $this->classConstantValueTypeCache[$resolvingName] = $resolvedType; + $this->classConstantValueTypeCache[$cacheKey] = $resolvedType; $types[] = $resolvedType; unset($this->currentlyResolvingClassConstant[$resolvingName]); continue; @@ -2798,26 +2802,28 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con if ( !$constantClassReflection->isFinal() && !$constantReflection->isFinal() - && !$constantReflection->hasPhpDocType() + && ($nativeTypesPromoted || !$constantReflection->hasPhpDocType()) && !$constantReflection->hasNativeType() ) { unset($this->currentlyResolvingClassConstant[$resolvingName]); return new MixedType(); } - if (!$constantClassReflection->isFinal()) { - $constantType = $constantReflection->getValueType(); - } else { + $nativeType = $constantReflection->getNativeType(); + if ($constantClassReflection->isFinal() || ($nativeTypesPromoted && $constantReflection->isFinal())) { $constantType = $this->getType($constantReflection->getValueExpr(), InitializerExprContext::fromClassReflection($constantReflection->getDeclaringClass())); + } elseif ($nativeTypesPromoted && $nativeType !== null) { + $constantType = $nativeType; + } else { + $constantType = $constantReflection->getValueType(); } - $nativeType = $constantReflection->getNativeType(); $constantType = $this->transformStaticType($this->constantResolver->resolveClassConstantType( $constantClassReflection->getName(), $constantName, $constantType, $nativeType, - $constantClassReflection->getConstantPhpDocType($constantName), + $nativeTypesPromoted ? null : $constantClassReflection->getConstantPhpDocType($constantName), ), $fetchedOnType); unset($this->currentlyResolvingClassConstant[$resolvingName]); $types[] = $constantType; diff --git a/tests/PHPStan/Analyser/nsrt/class-constant-native-type.php b/tests/PHPStan/Analyser/nsrt/class-constant-native-type.php index f8db2259e03..3a669cef825 100644 --- a/tests/PHPStan/Analyser/nsrt/class-constant-native-type.php +++ b/tests/PHPStan/Analyser/nsrt/class-constant-native-type.php @@ -2,6 +2,7 @@ namespace ClassConstantNativeType; +use function PHPStan\Testing\assertNativeType; use function PHPStan\Testing\assertType; class Foo @@ -14,6 +15,9 @@ public function doFoo(): void assertType('1', self::FOO); assertType('int', static::FOO); assertType('int', $this::FOO); + assertNativeType('1', self::FOO); + assertNativeType('int', static::FOO); + assertNativeType('int', $this::FOO); } } @@ -43,6 +47,9 @@ public function doFoo(): void assertType('1', self::FOO); assertType('int<1, max>', static::FOO); assertType('int<1, max>', $this::FOO); + assertNativeType('1', self::FOO); + assertNativeType('int', static::FOO); + assertNativeType('int', $this::FOO); } } @@ -61,3 +68,41 @@ public function doFoo(): void } } + +class FooWithOnlyPhpDoc +{ + + /** @var non-empty-string */ + public const FOO = 'foo'; + + /** @var non-empty-string */ + final public const FINAL_FOO = 'foo'; + + public function doFoo(self $foo): void + { + assertType('\'foo\'', self::FOO); + assertType('non-empty-string', static::FOO); + assertType('non-empty-string', $foo::FOO); + assertNativeType('\'foo\'', self::FOO); + assertNativeType('mixed', static::FOO); + assertNativeType('mixed', $foo::FOO); + + assertType('non-empty-string', static::FINAL_FOO); + assertNativeType('\'foo\'', static::FINAL_FOO); + } + +} + +final class FinalFooWithOnlyPhpDoc +{ + + /** @var non-empty-string */ + public const FOO = 'foo'; + + public function doFoo(): void + { + assertType('\'foo\'', static::FOO); + assertNativeType('\'foo\'', static::FOO); + } + +} diff --git a/turbo-ext/src/ClassConstFetchHandler.cpp b/turbo-ext/src/ClassConstFetchHandler.cpp index 4d28f249fd0..d243cd5c052 100644 --- a/turbo-ext/src/ClassConstFetchHandler.cpp +++ b/turbo-ext/src/ClassConstFetchHandler.cpp @@ -35,10 +35,10 @@ constexpr const char *pt_ccfh_closure_name = "PHPStan\\Analyser\\ExprHandler\\Cl * once they are ported) */ /* $initializerExprTypeResolver->getClassConstFetchTypeByReflection($class, - * $constantName, $classReflection, $getTypeCallback) */ -zv::Val getClassConstFetchTypeByReflection(zval *initializerExprTypeResolver, zval *class_, zval *constantName, zval *classReflection, const pt_ietr_get_type &getTypeCallback) + * $constantName, $classReflection, $getTypeCallback, $nativeTypesPromoted) */ +zv::Val getClassConstFetchTypeByReflection(zval *initializerExprTypeResolver, zval *class_, zval *constantName, zval *classReflection, const pt_ietr_get_type &getTypeCallback, bool nativeTypesPromoted) { - return pt_initializer_expr_type_resolver_get_class_const_fetch_type_by_reflection(initializerExprTypeResolver, class_, constantName, classReflection, getTypeCallback); + return pt_initializer_expr_type_resolver_get_class_const_fetch_type_by_reflection(initializerExprTypeResolver, class_, constantName, classReflection, getTypeCallback, nativeTypesPromoted); } /* }}} */ @@ -317,7 +317,7 @@ class ClassConstFetchHandler ZVAL_COPY_VALUE(&classTypeCaptures[0], &captures[2]); ZVAL_BOOL(&classTypeCaptures[1], zend_is_true(&argv[0])); pt_ietr_get_type getTypeCallback{&classTypeCallback, classTypeCaptures, &classTypeCallable}; - zv::Val type = getClassConstFetchTypeByReflection(OBJ_PROP_NUM(Z_OBJ(captures[0]), slots::initializerExprTypeResolver), class_, constantName, &captures[3], getTypeCallback); + zv::Val type = getClassConstFetchTypeByReflection(OBJ_PROP_NUM(Z_OBJ(captures[0]), slots::initializerExprTypeResolver), class_, constantName, &captures[3], getTypeCallback, zend_is_true(&argv[0])); if (UNEXPECTED(type.isUndef())) return; type.intoReturnValue(return_value); } diff --git a/turbo-ext/src/InitializerExprTypeResolver.cpp b/turbo-ext/src/InitializerExprTypeResolver.cpp index f71e2c9713e..a8dccb753ac 100644 --- a/turbo-ext/src/InitializerExprTypeResolver.cpp +++ b/turbo-ext/src/InitializerExprTypeResolver.cpp @@ -4590,7 +4590,7 @@ class InitializerExprTypeResolver /* Mirrors getClassConstFetchTypeByReflection(); $constantName a string * zval, $classReflection PHP null for null */ - zv::Val getClassConstFetchTypeByReflection(zval *class_, zval *constantName, zval *classReflection, const pt_ietr_get_type &getTypeCallback) const + zv::Val getClassConstFetchTypeByReflection(zval *class_, zval *constantName, zval *classReflection, const pt_ietr_get_type &getTypeCallback, bool nativeTypesPromoted = false) const { zend_string *constantNameString = Z_STR_P(constantName); bool constantNameIsClass = zend_string_equals_literal_ci(constantNameString, "class"); @@ -4733,8 +4733,13 @@ class InitializerExprTypeResolver continue; } + /* $cacheKey = $nativeTypesPromoted ? $resolvingName . '#native' : $resolvingName */ + zend_string *cacheKey = nativeTypesPromoted + ? zend_string_concat2(ZSTR_VAL(resolvingName), ZSTR_LEN(resolvingName), "#native", 7) + : zend_string_copy(resolvingName); + zv::Val cacheKeyHold = zv::Val::adoptString(cacheKey); if (!isObject) { - zval *cached = zend_symtable_find(Z_ARRVAL_P(prop(slots::classConstantValueTypeCache)), resolvingName); + zval *cached = zend_symtable_find(Z_ARRVAL_P(prop(slots::classConstantValueTypeCache)), cacheKey); if (cached != NULL) { types.push(zv::Ref(cached)); continue; @@ -4769,10 +4774,11 @@ class InitializerExprTypeResolver } } IETR_VAL(className, classReflectionName(constantClassReflection.raw())); - IETR_VAL(phpDocType, callOn(constantClassReflection.raw(), mGetConstantPhpDocType, 1, constantName)); + zv::Val phpDocType = nativeTypesPromoted ? zv::Val::null() : callOn(constantClassReflection.raw(), mGetConstantPhpDocType, 1, constantName); + if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); IETR_VAL(resolvedClassConstantType, resolveClassConstantType(prop(slots::constantResolver), className.raw(), constantName, constantType.raw(), nativeType.raw(), phpDocType.raw())); IETR_VAL(resolvedType, transformStaticType(resolvedClassConstantType.raw(), fetchedOnType.raw())); - writeArrayProperty(slots::classConstantValueTypeCache, resolvingName, resolvedType.raw()); + writeArrayProperty(slots::classConstantValueTypeCache, cacheKey, resolvedType.raw()); types.push(zv::Ref(resolvedType.raw())); unsetArrayProperty(slots::currentlyResolvingClassConstant, resolvingName); continue; @@ -4786,7 +4792,7 @@ class InitializerExprTypeResolver IETR_VAL(constantIsFinal, callOn(constantReflection.raw(), mIsFinal)); if (!zend_is_true(constantIsFinal.raw())) { IETR_VAL(hasPhpDocType, callOn(constantReflection.raw(), mHasPhpDocType)); - if (!zend_is_true(hasPhpDocType.raw())) { + if (nativeTypesPromoted || !zend_is_true(hasPhpDocType.raw())) { IETR_VAL(hasNativeType, callOn(constantReflection.raw(), mHasNativeType)); if (!zend_is_true(hasNativeType.raw())) { unsetArrayProperty(slots::currentlyResolvingClassConstant, resolvingName); @@ -4797,24 +4803,32 @@ class InitializerExprTypeResolver } } + IETR_VAL(nativeType, callOn(constantReflection.raw(), mGetNativeType)); zv::Val constantType; { bool classIsFinal; if (UNEXPECTED(!classReflectionIsFinal(constantClassReflection.raw(), classIsFinal))) return zv::Val(); - if (!classIsFinal) { - constantType = callOn(constantReflection.raw(), mGetValueType); - } else { + bool valueIsExact = classIsFinal; + if (!valueIsExact && nativeTypesPromoted) { + IETR_VAL(constantIsFinal, callOn(constantReflection.raw(), mIsFinal)); + valueIsExact = zend_is_true(constantIsFinal.raw()); + } + if (valueIsExact) { IETR_VAL(valueExpr, callOn(constantReflection.raw(), mGetValueExpr)); IETR_VAL(declaringClass, callOn(constantReflection.raw(), mGetDeclaringClass)); IETR_VAL(constantContext, contextFromClassReflection(declaringClass.raw())); constantType = getTypeRecursive(valueExpr.raw(), constantContext.raw()); + } else if (nativeTypesPromoted && Z_TYPE_P(nativeType.raw()) != IS_NULL) { + constantType = zv::Val::copyOf(zv::Ref(nativeType.raw())); + } else { + constantType = callOn(constantReflection.raw(), mGetValueType); } if (UNEXPECTED(constantType.isUndef())) return zv::Val(); } - IETR_VAL(nativeType, callOn(constantReflection.raw(), mGetNativeType)); IETR_VAL(className, classReflectionName(constantClassReflection.raw())); - IETR_VAL(phpDocType, callOn(constantClassReflection.raw(), mGetConstantPhpDocType, 1, constantName)); + zv::Val phpDocType = nativeTypesPromoted ? zv::Val::null() : callOn(constantClassReflection.raw(), mGetConstantPhpDocType, 1, constantName); + if (UNEXPECTED(phpDocType.isUndef())) return zv::Val(); IETR_VAL(resolvedClassConstantType, resolveClassConstantType(prop(slots::constantResolver), className.raw(), constantName, constantType.raw(), nativeType.raw(), phpDocType.raw())); IETR_VAL(resolvedConstantType, transformStaticType(resolvedClassConstantType.raw(), fetchedOnType.raw())); unsetArrayProperty(slots::currentlyResolvingClassConstant, resolvingName); @@ -5569,15 +5583,15 @@ zv::Val pt_initializer_expr_type_resolver_create_first_class_callable(zval *reso return callResolverMethod(site, resolver, PT_LC("createfirstclasscallable"), 3, argv); } -zv::Val pt_initializer_expr_type_resolver_get_class_const_fetch_type_by_reflection(zval *resolver, zval *class_, zval *constantName, zval *classReflection, const pt_ietr_get_type &getTypeCallback) +zv::Val pt_initializer_expr_type_resolver_get_class_const_fetch_type_by_reflection(zval *resolver, zval *class_, zval *constantName, zval *classReflection, const pt_ietr_get_type &getTypeCallback, bool nativeTypesPromoted) { if (EXPECTED(isNativeResolver(resolver) && Z_TYPE_P(constantName) == IS_STRING)) { - return InitializerExprTypeResolver(Z_OBJ_P(resolver)).getClassConstFetchTypeByReflection(class_, constantName, classReflection, getTypeCallback); + return InitializerExprTypeResolver(Z_OBJ_P(resolver)).getClassConstFetchTypeByReflection(class_, constantName, classReflection, getTypeCallback, nativeTypesPromoted); } static pt_method_site site; IETR_VAL(callable, callbackCallable(getTypeCallback)); - zv::Args argv{class_, constantName, classReflection, callable.raw()}; - return callResolverMethod(site, resolver, PT_LC("getclassconstfetchtypebyreflection"), 4, argv); + zv::Args argv{class_, constantName, classReflection, callable.raw(), nativeTypesPromoted}; + return callResolverMethod(site, resolver, PT_LC("getclassconstfetchtypebyreflection"), 5, argv); } zv::Val pt_initializer_expr_type_resolver_get_unary_plus_type(zval *resolver, zval *expr, const pt_ietr_get_type &getTypeCallback) @@ -5967,7 +5981,8 @@ PT_MINIT_REGISTRATION(pt_register_initializer_expr_type_resolver) cls.method(sigs::getClassConstFetchTypeByReflection, [](INTERNAL_FUNCTION_PARAMETERS) { zval *class_, *classReflection, *callable; zend_string *constantName; - if (!zp::parse(execute_data, class_, constantName, classReflection, callable)) RETURN_THROWS(); + bool nativeTypesPromoted = false; + if (!zp::parse>(execute_data, class_, constantName, classReflection, callable, nativeTypesPromoted)) RETURN_THROWS(); if (UNEXPECTED(!ietrArgCallable(callable, 4))) RETURN_THROWS(); zval nullReflection; if (classReflection == NULL) { @@ -5979,7 +5994,7 @@ PT_MINIT_REGISTRATION(pt_register_initializer_expr_type_resolver) zval constantNameZv; ZVAL_STR(&constantNameZv, constantName); pt_ietr_get_type getTypeCallback = callableCallback(callable); - PT_RETURN_VAL(IETR_THIS.getClassConstFetchTypeByReflection(class_, &constantNameZv, classReflection, getTypeCallback)); + PT_RETURN_VAL(IETR_THIS.getClassConstFetchTypeByReflection(class_, &constantNameZv, classReflection, getTypeCallback, nativeTypesPromoted)); }); cls.method(sigs::getClassConstFetchType, [](INTERNAL_FUNCTION_PARAMETERS) { diff --git a/turbo-ext/src/generated/InitializerExprTypeResolver.h b/turbo-ext/src/generated/InitializerExprTypeResolver.h index bf5e14cf515..69ddbf4def1 100644 --- a/turbo-ext/src/generated/InitializerExprTypeResolver.h +++ b/turbo-ext/src/generated/InitializerExprTypeResolver.h @@ -141,29 +141,30 @@ inline constexpr char strings[] = "constantName\0" /* 1880 */ "classReflection\0" /* 1893 */ "PHPStan\\Reflection\\ClassReflection\0" /* 1909 */ - "getClassConstFetchTypeByReflection\0" /* 1944 */ - "fetchedOnType\0" /* 1979 */ - "transformStaticType\0" /* 1993 */ - "getClassConstFetchType\0" /* 2013 */ - "getUnaryPlusType\0" /* 2036 */ - "getUnaryMinusType\0" /* 2053 */ - "getUnaryMinusTypeFromType\0" /* 2071 */ - "getBitwiseNotType\0" /* 2097 */ - "getBitwiseNotTypeFromType\0" /* 2115 */ - "name\0" /* 2141 */ - "PhpParser\\Node\\Name\0" /* 2146 */ - "resolveName\0" /* 2166 */ - "PHPStan\\Type\\TypeWithClassName\0" /* 2178 */ - "resolveTypeByName\0" /* 2209 */ - "classType\0" /* 2227 */ - "methodReflectionCandidate\0" /* 2237 */ - "PHPStan\\Reflection\\MethodReflection\0" /* 2263 */ - "resolveTypeByNameWithLateStaticBinding\0" /* 2299 */ - "toIntBound\0" /* 2338 */ - "getTypeFromValue\0" /* 2349 */ - "PHPStan\\Reflection\\ReflectionProvider\0" /* 2366 */ - "getReflectionProvider\0" /* 2404 */ - "getNeverType"; /* 2426 */ + "false\0" /* 1944 */ + "getClassConstFetchTypeByReflection\0" /* 1950 */ + "fetchedOnType\0" /* 1985 */ + "transformStaticType\0" /* 1999 */ + "getClassConstFetchType\0" /* 2019 */ + "getUnaryPlusType\0" /* 2042 */ + "getUnaryMinusType\0" /* 2059 */ + "getUnaryMinusTypeFromType\0" /* 2077 */ + "getBitwiseNotType\0" /* 2103 */ + "getBitwiseNotTypeFromType\0" /* 2121 */ + "name\0" /* 2147 */ + "PhpParser\\Node\\Name\0" /* 2152 */ + "resolveName\0" /* 2172 */ + "PHPStan\\Type\\TypeWithClassName\0" /* 2184 */ + "resolveTypeByName\0" /* 2215 */ + "classType\0" /* 2233 */ + "methodReflectionCandidate\0" /* 2243 */ + "PHPStan\\Reflection\\MethodReflection\0" /* 2269 */ + "resolveTypeByNameWithLateStaticBinding\0" /* 2305 */ + "toIntBound\0" /* 2344 */ + "getTypeFromValue\0" /* 2355 */ + "PHPStan\\Reflection\\ReflectionProvider\0" /* 2372 */ + "getReflectionProvider\0" /* 2410 */ + "getNeverType"; /* 2432 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 17), /* __construct $constantResolver */ reg::packed(51, 0, 78), /* __construct $reflectionProviderProvider */ @@ -309,9 +310,10 @@ inline constexpr reg::PackedArg args[] = { reg::packed(1880, MAY_BE_STRING), /* getClassConstFetchTypeByReflection $constantName */ reg::packed(1893, MAY_BE_NULL, 1909), /* getClassConstFetchTypeByReflection $classReflection */ reg::packed(647, MAY_BE_CALLABLE), /* getClassConstFetchTypeByReflection $getTypeCallback */ + reg::packed(927, MAY_BE_BOOL, reg::NoString, false, false, 1944), /* getClassConstFetchTypeByReflection $nativeTypesPromoted */ reg::packed(559, 0, 560), /* getClassConstFetchTypeByReflection return */ reg::packed(799, 0, 560), /* transformStaticType $type */ - reg::packed(1979, 0, 560), /* transformStaticType $fetchedOnType */ + reg::packed(1985, 0, 560), /* transformStaticType $fetchedOnType */ reg::packed(559, 0, 560), /* transformStaticType return */ reg::packed(1834, 0, 1840), /* getClassConstFetchType $class */ reg::packed(1880, MAY_BE_STRING), /* getClassConstFetchType $constantName */ @@ -332,21 +334,21 @@ inline constexpr reg::PackedArg args[] = { reg::packed(559, 0, 560), /* getBitwiseNotType return */ reg::packed(772, 0, 560), /* getBitwiseNotTypeFromType $exprType */ reg::packed(559, 0, 560), /* getBitwiseNotTypeFromType return */ - reg::packed(2141, 0, 2146), /* resolveName $name */ + reg::packed(2147, 0, 2152), /* resolveName $name */ reg::packed(1893, MAY_BE_NULL, 1909), /* resolveName $classReflection */ reg::packed(559, MAY_BE_STRING), /* resolveName return */ - reg::packed(2141, 0, 2146), /* resolveTypeByName $name */ + reg::packed(2147, 0, 2152), /* resolveTypeByName $name */ reg::packed(1893, MAY_BE_NULL, 1909), /* resolveTypeByName $classReflection */ - reg::packed(559, 0, 2178), /* resolveTypeByName return */ - reg::packed(1834, 0, 2146), /* resolveTypeByNameWithLateStaticBinding $class */ - reg::packed(2227, 0, 560), /* resolveTypeByNameWithLateStaticBinding $classType */ - reg::packed(2237, 0, 2263), /* resolveTypeByNameWithLateStaticBinding $methodReflectionCandidate */ + reg::packed(559, 0, 2184), /* resolveTypeByName return */ + reg::packed(1834, 0, 2152), /* resolveTypeByNameWithLateStaticBinding $class */ + reg::packed(2233, 0, 560), /* resolveTypeByNameWithLateStaticBinding $classType */ + reg::packed(2243, 0, 2269), /* resolveTypeByNameWithLateStaticBinding $methodReflectionCandidate */ reg::packed(559, 0, 560), /* resolveTypeByNameWithLateStaticBinding return */ reg::packed(1547, MAY_BE_DOUBLE), /* toIntBound $value */ reg::packed(559, MAY_BE_NULL | MAY_BE_LONG), /* toIntBound return */ reg::packed(1547, 0), /* getTypeFromValue $value */ reg::packed(559, 0, 560), /* getTypeFromValue return */ - reg::packed(559, 0, 2366), /* getReflectionProvider return */ + reg::packed(559, 0, 2372), /* getReflectionProvider return */ reg::packed(1151, 0, 560), /* getNeverType $leftType */ reg::packed(1160, 0, 560), /* getNeverType $rightType */ reg::packed(559, 0, 560), /* getNeverType return */ @@ -395,21 +397,21 @@ inline constexpr sigtab::Sig resolveConstantArrayTypeComparison = { { 1691 /* re inline constexpr sigtab::Sig resolveCommonMath = { { 1755 /* resolveCommonMath */, 3, 129, 3, 132, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig integerRangeMath = { { 1792 /* integerRangeMath */, 3, 133, 3, 136, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig shiftLeftOverflows = { { 1815 /* shiftLeftOverflows */, 2, 137, 2, 139, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getClassConstFetchTypeByReflection = { { 1944 /* getClassConstFetchTypeByReflection */, 4, 140, 4, 144, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig transformStaticType = { { 1993 /* transformStaticType */, 2, 145, 2, 147, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getClassConstFetchType = { { 2013 /* getClassConstFetchType */, 4, 148, 4, 152, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getUnaryPlusType = { { 2036 /* getUnaryPlusType */, 2, 153, 2, 155, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getUnaryMinusType = { { 2053 /* getUnaryMinusType */, 2, 156, 2, 158, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getUnaryMinusTypeFromType = { { 2071 /* getUnaryMinusTypeFromType */, 2, 159, 2, 161, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getBitwiseNotType = { { 2097 /* getBitwiseNotType */, 2, 162, 2, 164, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getBitwiseNotTypeFromType = { { 2115 /* getBitwiseNotTypeFromType */, 1, 165, 1, 166, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig resolveName = { { 2166 /* resolveName */, 2, 167, 2, 169, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig resolveTypeByName = { { 2209 /* resolveTypeByName */, 2, 170, 2, 172, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig resolveTypeByNameWithLateStaticBinding = { { 2299 /* resolveTypeByNameWithLateStaticBinding */, 3, 173, 3, 176, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig toIntBound = { { 2338 /* toIntBound */, 1, 177, 1, 178, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig getTypeFromValue = { { 2349 /* getTypeFromValue */, 1, 179, 1, 180, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getReflectionProvider = { { 2404 /* getReflectionProvider */, 0, 181, 0, 181, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getNeverType = { { 2426 /* getNeverType */, 2, 182, 2, 184, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getClassConstFetchTypeByReflection = { { 1950 /* getClassConstFetchTypeByReflection */, 4, 140, 5, 145, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig transformStaticType = { { 1999 /* transformStaticType */, 2, 146, 2, 148, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getClassConstFetchType = { { 2019 /* getClassConstFetchType */, 4, 149, 4, 153, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getUnaryPlusType = { { 2042 /* getUnaryPlusType */, 2, 154, 2, 156, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getUnaryMinusType = { { 2059 /* getUnaryMinusType */, 2, 157, 2, 159, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getUnaryMinusTypeFromType = { { 2077 /* getUnaryMinusTypeFromType */, 2, 160, 2, 162, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getBitwiseNotType = { { 2103 /* getBitwiseNotType */, 2, 163, 2, 165, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getBitwiseNotTypeFromType = { { 2121 /* getBitwiseNotTypeFromType */, 1, 166, 1, 167, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig resolveName = { { 2172 /* resolveName */, 2, 168, 2, 170, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveTypeByName = { { 2215 /* resolveTypeByName */, 2, 171, 2, 173, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveTypeByNameWithLateStaticBinding = { { 2305 /* resolveTypeByNameWithLateStaticBinding */, 3, 174, 3, 177, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig toIntBound = { { 2344 /* toIntBound */, 1, 178, 1, 179, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig getTypeFromValue = { { 2355 /* getTypeFromValue */, 1, 180, 1, 181, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getReflectionProvider = { { 2410 /* getReflectionProvider */, 0, 182, 0, 182, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getNeverType = { { 2432 /* getNeverType */, 2, 183, 2, 185, ZEND_ACC_PRIVATE } }; } // namespace sig } // namespace ptdecl::InitializerExprTypeResolver diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 4e548b663df..043560faaa0 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -4078,7 +4078,7 @@ zv::Val pt_initializer_expr_type_resolver_get_cast_object_type(zval *resolver, z zv::Val pt_initializer_expr_type_resolver_get_function_type(zval *resolver, zval *type, bool isNullable, bool isVariadic, zval *context); zv::Val pt_initializer_expr_type_resolver_get_first_class_callable_type(zval *resolver, zval *expr, zval *context, bool nativeTypesPromoted); zv::Val pt_initializer_expr_type_resolver_create_first_class_callable(zval *resolver, zval *function, zval *variants, bool nativeTypesPromoted); -zv::Val pt_initializer_expr_type_resolver_get_class_const_fetch_type_by_reflection(zval *resolver, zval *class_, zval *constantName, zval *classReflection, const pt_ietr_get_type &getTypeCallback); +zv::Val pt_initializer_expr_type_resolver_get_class_const_fetch_type_by_reflection(zval *resolver, zval *class_, zval *constantName, zval *classReflection, const pt_ietr_get_type &getTypeCallback, bool nativeTypesPromoted = false); zv::Val pt_initializer_expr_type_resolver_get_unary_plus_type(zval *resolver, zval *expr, const pt_ietr_get_type &getTypeCallback); zv::Val pt_initializer_expr_type_resolver_get_unary_minus_type(zval *resolver, zval *expr, const pt_ietr_get_type &getTypeCallback); zv::Val pt_initializer_expr_type_resolver_get_bitwise_not_type(zval *resolver, zval *expr, const pt_ietr_get_type &getTypeCallback); From 61347dc39a64be40b941073cfe34091c6880ac9d Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Tue, 27 Oct 2026 06:05:20 +0000 Subject: [PATCH 2/4] Walk catch blocks that no throw point reaches Amp-Thread-ID: https://ampcode.com/threads/T-01a179f2-995f-7745-8d8c-4c174ba5c0ce --- src/Analyser/StmtHandler/TryCatchHandler.php | 11 +++ tests/PHPStan/Analyser/nsrt/bug-14397.php | 2 +- tests/PHPStan/Analyser/nsrt/dead-catch.php | 95 +++++++++++++++++++ .../Rules/Methods/CallMethodsRuleTest.php | 21 ++++ .../PHPStan/Rules/Methods/data/dead-catch.php | 30 ++++++ .../Variables/DefinedVariableRuleTest.php | 12 +++ .../data/dead-catch-defined-variable.php | 26 +++++ turbo-ext/src/TryCatchHandler.cpp | 53 ++++++++++- 8 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/dead-catch.php create mode 100644 tests/PHPStan/Rules/Methods/data/dead-catch.php create mode 100644 tests/PHPStan/Rules/Variables/data/dead-catch-defined-variable.php diff --git a/src/Analyser/StmtHandler/TryCatchHandler.php b/src/Analyser/StmtHandler/TryCatchHandler.php index 87f0ebe0d8a..c41c2168982 100644 --- a/src/Analyser/StmtHandler/TryCatchHandler.php +++ b/src/Analyser/StmtHandler/TryCatchHandler.php @@ -215,6 +215,17 @@ public function processStmt( if (count($matchingThrowPoints) === 0) { $catchFlows[] = [$originalCatchType, $this->statementsHandler->getVariableMentionFlow($catchNode)]; + + // the catch block is not reachable, but rules still need to see its statements; + // it is entered from where the try block can throw anything, and the result + // of the walk does not flow anywhere + $deadCatchScope = null; + foreach ($branchScopeResult->getThrowPoints() as $throwPoint) { + $deadCatchScope = $deadCatchScope === null ? $throwPoint->getScope() : $deadCatchScope->mergeWith($throwPoint->getScope()); + } + $deadCatchScope = ($deadCatchScope ?? $scope->mergeWith($branchScope))->enterCatchType($catchType instanceof NeverType ? $originalCatchType : $catchType, $catchNode->var !== null && is_string($catchNode->var->name) ? $catchNode->var->name : null); + $deadCatchResult = $nodeScopeResolver->processStmtNodesInternal($catchNode, $catchNode->stmts, $deadCatchScope, $storage, $nodeCallback, $context); + $dependencies[] = $deadCatchResult->getDependencies(); continue; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14397.php b/tests/PHPStan/Analyser/nsrt/bug-14397.php index e607dd8a378..a989786ecf2 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14397.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14397.php @@ -21,7 +21,7 @@ function test2(mixed $mixed): void filter_var($mixed, FILTER_VALIDATE_INT, FILTER_FLAG_GLOBAL_RANGE); $foo = 1; } catch (\Filter\FilterFailedException $e) { - assertVariableCertainty(TrinaryLogic::createYes(), $foo); + assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); } assertType('int|false', filter_var($mixed, FILTER_VALIDATE_INT, FILTER_FLAG_GLOBAL_RANGE)); diff --git a/tests/PHPStan/Analyser/nsrt/dead-catch.php b/tests/PHPStan/Analyser/nsrt/dead-catch.php new file mode 100644 index 00000000000..7d627409404 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/dead-catch.php @@ -0,0 +1,95 @@ +checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/data/dead-catch.php'], [ + [ + 'Call to an undefined method InvalidArgumentException::nonExistentMethod().', + 14, + ], + [ + 'Call to an undefined method CallMethodsDeadCatch\\Foo::nonExistentMethod().', + 15, + ], + [ + 'Call to an undefined method InvalidArgumentException::nonExistentMethod().', + 26, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Methods/data/dead-catch.php b/tests/PHPStan/Rules/Methods/data/dead-catch.php new file mode 100644 index 00000000000..fbcb885bcca --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/dead-catch.php @@ -0,0 +1,30 @@ +getMessage(); + $e->nonExistentMethod(); + $this->nonExistentMethod(); + } + } + + public function doBar(): void + { + try { + $this->doFoo(); + } catch (\Throwable $e) { + + } catch (\InvalidArgumentException $e) { + $e->nonExistentMethod(); + } + } + +} diff --git a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php index a9dfd601ec8..99f580819af 100644 --- a/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php +++ b/tests/PHPStan/Rules/Variables/DefinedVariableRuleTest.php @@ -527,4 +527,16 @@ public function testVariableVariableName(): void ]); } + public function testDeadCatch(): void + { + $this->cliArgumentsVariablesRegistered = true; + $this->checkMaybeUndefinedVariables = true; + $this->analyse([__DIR__ . '/data/dead-catch-defined-variable.php'], [ + [ + 'Undefined variable: $undefined', + 22, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Variables/data/dead-catch-defined-variable.php b/tests/PHPStan/Rules/Variables/data/dead-catch-defined-variable.php new file mode 100644 index 00000000000..26390e0cfbc --- /dev/null +++ b/tests/PHPStan/Rules/Variables/data/dead-catch-defined-variable.php @@ -0,0 +1,26 @@ + $names */ + public function doFoo(array $names): void + { + try { + foreach ($names as $name) { + $this->process($name); + } + } catch (\InvalidArgumentException $e) { + echo $name; + echo $undefined; + } + } + +} diff --git a/turbo-ext/src/TryCatchHandler.cpp b/turbo-ext/src/TryCatchHandler.cpp index 171d0944cc6..78819f6485a 100644 --- a/turbo-ext/src/TryCatchHandler.cpp +++ b/turbo-ext/src/TryCatchHandler.cpp @@ -441,9 +441,60 @@ class TryCatchHandler zv::Val mentionFlow = pt_statements_handler_get_variable_mention_flow(statementsHandler, catchNode); if (UNEXPECTED(mentionFlow.isUndef())) return zv::Val(); zv::Arr catchFlow = zv::Arr::create(2); - catchFlow.push(std::move(originalCatchType)); + catchFlow.push(zv::Ref(originalCatchType.raw())); catchFlow.push(std::move(mentionFlow)); catchFlows.push(zv::Val(std::move(catchFlow))); + + // the catch block is not reachable, but rules still need to see its statements; + // it is entered from where the try block can throw anything, and the result + // of the walk does not flow anywhere + zend_string *deadVariableName = NULL; + { + zval *catchVar = ptsh::readNodeProperty(pt_tch_catch_var_site, catchNode, PT_LC("var")); + if (UNEXPECTED(catchVar == NULL)) return zv::Val(); + if (Z_TYPE_P(catchVar) != IS_NULL) { + zval *name = ptsh::readNodeProperty(pt_tch_variable_name_site, catchVar, PT_LC("name")); + if (UNEXPECTED(name == NULL)) return zv::Val(); + if (Z_TYPE_P(name) == IS_STRING) { + deadVariableName = Z_STR_P(name); + } + } + } + zv::Val deadVariableNameHold = deadVariableName != NULL ? zv::Val::string(deadVariableName) : zv::Val::null(); + zv::Val mergedScope = zv::Val::null(); + { + zv::Val hold; + zval *branchThrowPoints = pt_internal_statement_result_throw_points(branch, hold); + if (UNEXPECTED(branchThrowPoints == NULL)) return zv::Val(); + zv::Val branchThrowPointsHold = zv::Val::copyOf(zv::Ref(branchThrowPoints)); + if (Z_TYPE_P(branchThrowPointsHold.raw()) == IS_ARRAY) { + for (auto throwEntry : zv::ArrRef(branchThrowPointsHold.raw())) { + zv::Val scopeHold; + zval *throwScope = pt_internal_throw_point_scope(throwEntry.value().deref().raw(), scopeHold); + if (UNEXPECTED(throwScope == NULL)) return zv::Val(); + if (mergedScope.isNull()) { + mergedScope = zv::Val::copyOf(zv::Ref(throwScope)); + continue; + } + zv::Val merged = pt_mutating_scope_merge_with(Z_OBJ_P(mergedScope.raw()), throwScope); + if (UNEXPECTED(merged.isUndef())) return zv::Val(); + mergedScope = std::move(merged); + } + } + } + if (mergedScope.isNull()) { + mergedScope = pt_mutating_scope_merge_with(Z_OBJ_P(scope), branchScope.raw()); + if (UNEXPECTED(mergedScope.isUndef())) return zv::Val(); + } + zval *deadCatchType = isNeverType(catchType.raw()) ? originalCatchType.raw() : catchType.raw(); + zv::Val deadCatchScope = pt_mutating_scope_enter_catch_type(Z_OBJ_P(mergedScope.raw()), deadCatchType, deadVariableName != NULL ? Z_STR_P(deadVariableNameHold.raw()) : NULL); + if (UNEXPECTED(deadCatchScope.isUndef())) return zv::Val(); + zval *deadCatchStmts = ptsh::readNodeProperty(pt_tch_catch_stmts_site, catchNode, PT_LC("stmts")); + if (UNEXPECTED(deadCatchStmts == NULL)) return zv::Val(); + zv::Val deadCatchStmtsHold = zv::Val::copyOf(zv::Ref(deadCatchStmts)); + zv::Val deadCatchResult = pt_node_scope_resolver_process_stmt_nodes_internal(nodeScopeResolver, catchNode, deadCatchStmtsHold.raw(), deadCatchScope.raw(), storage, nodeCallback, context); + if (UNEXPECTED(deadCatchResult.isUndef())) return zv::Val(); + if (UNEXPECTED(!pushStatementDependencies(dependencies, deadCatchResult.raw()))) return zv::Val(); continue; } From 6821174e3d0617e097134cce81abfe93c44f546d Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Tue, 27 Oct 2026 06:05:21 +0000 Subject: [PATCH 3/4] Walk attribute groups of closures and arrow functions Amp-Thread-ID: https://ampcode.com/threads/T-01a179f2-995f-7745-8d8c-4c174ba5c0ce --- src/Analyser/ClosureProcessor.php | 32 +- .../Rules/Classes/ClassConstantRuleTest.php | 32 ++ .../data/closure-attribute-class-constant.php | 32 ++ turbo-ext/src/ClosureProcessor.cpp | 40 ++- turbo-ext/src/generated/ClosureProcessor.h | 290 +++++++++--------- 5 files changed, 284 insertions(+), 142 deletions(-) create mode 100644 tests/PHPStan/Rules/Classes/data/closure-attribute-class-constant.php diff --git a/src/Analyser/ClosureProcessor.php b/src/Analyser/ClosureProcessor.php index a838adf6018..e52c33ac024 100644 --- a/src/Analyser/ClosureProcessor.php +++ b/src/Analyser/ClosureProcessor.php @@ -74,6 +74,34 @@ private function getParametersProcessor(): ParametersProcessor return $this->container->getByType(ParametersProcessor::class); } + /** + * Looked up instead of injected for the same reason as getParametersProcessor(). + */ + private function getAttributesHandler(): AttributesHandler + { + return $this->container->getByType(AttributesHandler::class); + } + + /** + * Returns what the attributes and the parameters of the closure or arrow function depend on. + * + * @param callable(Node $node, Scope $scope): void $nodeCallback + */ + private function processAttributeGroupsAndParams( + NodeScopeResolver $nodeScopeResolver, + Node\Stmt $stmt, + Expr\Closure|Expr\ArrowFunction $expr, + MutatingScope $scope, + ExpressionResultStorage $storage, + callable $nodeCallback, + ): ?Dependencies + { + return Dependencies::merge( + $this->getAttributesHandler()->processAttributeGroups($nodeScopeResolver, $stmt, $expr->attrGroups, $scope, $storage, $nodeCallback), + $this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback), + ); + } + /** * @param callable(Node $node, Scope $scope): void $nodeCallback */ @@ -107,7 +135,7 @@ private function processClosureNodeInternal( ?Type $nativePassedToType = null, ): ProcessClosureResult { - $parametersDependencies = $this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback); + $parametersDependencies = $this->processAttributeGroupsAndParams($nodeScopeResolver, $stmt, $expr, $scope, $storage, $nodeCallback); $byRefUses = []; @@ -756,7 +784,7 @@ public function processArrowFunctionNode( ): ProcessArrowFunctionResult { $context ??= ExpressionContext::createTopLevel(); - $parametersDependencies = $this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, $expr->params, $scope, $storage, $nodeCallback); + $parametersDependencies = $this->processAttributeGroupsAndParams($nodeScopeResolver, $stmt, $expr, $scope, $storage, $nodeCallback); if ($this->closureSignatureInference->isObserving($scope)) { foreach (ClosureSignatureInference::getArrowFunctionOuterVariables($expr) as $name) { if (!$scope->hasVariableType($name)->yes()) { diff --git a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php index d8fe7c5c3d5..e6e6d1837bf 100644 --- a/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php +++ b/tests/PHPStan/Rules/Classes/ClassConstantRuleTest.php @@ -604,4 +604,36 @@ public function testConditionallyExecutedClassConstantOnExpression(): void $this->analyse([__DIR__ . '/data/class-constant-on-expr-php-versions.php'], $errors); } + #[RequiresPhp('>= 8.0.0')] + public function testClosureAttributeArguments(): void + { + $tip = 'Learn more at https://phpstan.org/user-guide/discovering-symbols'; + $this->analyse([__DIR__ . '/data/closure-attribute-class-constant.php'], [ + [ + 'Access to constant FOO on an unknown class ClosureAttributeClassConstant\\Missing.', + 26, + $tip, + ], + [ + 'Access to constant FOO on an unknown class ClosureAttributeClassConstant\\Missing.', + 27, + $tip, + ], + [ + 'Access to constant FOO on an unknown class ClosureAttributeClassConstant\\Missing.', + 28, + $tip, + ], + [ + 'Access to undefined constant ClosureAttributeClassConstant\\Foo::BAR.', + 29, + ], + [ + 'Access to constant FOO on an unknown class ClosureAttributeClassConstant\\Missing.', + 29, + $tip, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Classes/data/closure-attribute-class-constant.php b/tests/PHPStan/Rules/Classes/data/closure-attribute-class-constant.php new file mode 100644 index 00000000000..7aebce3dfde --- /dev/null +++ b/tests/PHPStan/Rules/Classes/data/closure-attribute-class-constant.php @@ -0,0 +1,32 @@ += 8.0 + +namespace ClosureAttributeClassConstant; + +#[\Attribute] +class Attr +{ + + public function __construct(public string $value) + { + } + +} + +#[\Attribute] +class AttrWithoutConstructor +{ + +} + +class Foo +{ + + public function doFoo(): void + { + $f = #[Attr(Missing::FOO)] function () {}; + $g = #[Attr(Missing::FOO)] fn () => 1; + $h = #[AttrWithoutConstructor(Missing::FOO)] static function () {}; + $i = #[Attr(self::BAR)] fn () => #[Attr(Missing::FOO)] fn () => 1; + } + +} diff --git a/turbo-ext/src/ClosureProcessor.cpp b/turbo-ext/src/ClosureProcessor.cpp index c299c774332..7bc4b1f2065 100644 --- a/turbo-ext/src/ClosureProcessor.cpp +++ b/turbo-ext/src/ClosureProcessor.cpp @@ -74,6 +74,20 @@ zv::Val getParametersProcessor(zval *container) return service; } +/* $this->container->getByType(AttributesHandler::class) */ +zv::Val getAttributesHandler(zval *container) +{ + if (UNEXPECTED(Z_TYPE_P(container) != IS_OBJECT)) { + zend_throw_error(NULL, "Call to a member function getByType() on %s", zend_zval_value_name(container)); + return zv::Val(); + } + zval className; + ZVAL_STRINGL(&className, "PHPStan\\Analyser\\AttributesHandler", sizeof("PHPStan\\Analyser\\AttributesHandler") - 1); + zv::Val service = pt_call_method_cached(pt_cp_get_by_type_site, Z_OBJ_P(container), PT_LC("getbytype"), 1, &className); + zval_ptr_dtor(&className); + return service; +} + /* $invalidateExprNode->getExpr(): the final node's promoted $expr, the method * of anything else; UNDEF = pending exception */ zv::Val invalidateExprNodeExpr(zval *node) @@ -636,7 +650,7 @@ class ClosureProcessor if (UNEXPECTED(contextHold.isUndef())) return zv::Val(); context = contextHold.raw(); } - zv::Val parametersDependencies = processParams(nodeScopeResolver, stmt, expr, scopeArg, storage, nodeCallback); + zv::Val parametersDependencies = processAttributeGroupsAndParams(nodeScopeResolver, stmt, expr, scopeArg, storage, nodeCallback); if (UNEXPECTED(parametersDependencies.isUndef())) return zv::Val(); zv::Val scopeHold = zv::Val::copyOf(zv::Ref(scopeArg)); bool observing; @@ -847,6 +861,28 @@ class ClosureProcessor Z_PROP_FLAG_P(OBJ_PROP_NUM(self, index)) = 0; } + /* Mirrors processAttributeGroupsAndParams(): Dependencies::merge( + * $this->getAttributesHandler()->processAttributeGroups(..., $expr->attrGroups, ...), + * $this->getParametersProcessor()->processParams(..., $expr->params, ...)); + * UNDEF = pending exception */ + zv::Val processAttributeGroupsAndParams(zval *nodeScopeResolver, zval *stmt, zval *expr, zval *scope, zval *storage, zval *nodeCallback) const + { + zv::Val attributesHandler = getAttributesHandler(slot(slots::container)); + if (UNEXPECTED(attributesHandler.isUndef())) return zv::Val(); + if (UNEXPECTED(!attributesHandler.ref().isObject())) { + zend_throw_error(NULL, "Call to a member function processAttributeGroups() on %s", zend_zval_value_name(attributesHandler.raw())); + return zv::Val(); + } + zval *attrGroups = ptclosure::prop(ptclosure::attrGroupsSite, expr, PT_LC("attrGroups")); + if (UNEXPECTED(attrGroups == NULL)) return zv::Val(); + zv::Val attrGroupsHold = zv::Val::copyOf(zv::Ref(attrGroups)); + zv::Val attributesDependencies = pt_attributes_handler_process_attribute_groups(attributesHandler.raw(), nodeScopeResolver, stmt, attrGroupsHold.raw(), scope, storage, nodeCallback); + if (UNEXPECTED(attributesDependencies.isUndef())) return zv::Val(); + zv::Val parametersDependencies = processParams(nodeScopeResolver, stmt, expr, scope, storage, nodeCallback); + if (UNEXPECTED(parametersDependencies.isUndef())) return zv::Val(); + return pt_dependencies_merge({attributesDependencies.raw(), parametersDependencies.raw()}); + } + /* $this->getParametersProcessor()->processParams($nodeScopeResolver, $stmt, * $expr->params, $scope, $storage, $nodeCallback): what the parameters * depend on; UNDEF = pending exception */ @@ -940,7 +976,7 @@ class ClosureProcessor * gatherer; false = pending exception */ [[nodiscard]] zend_never_inline bool enterClosure(ClosureWalk &w, zval *passedToType, zval *nativePassedToType) const { - w.parametersDependencies = processParams(w.nodeScopeResolver, w.stmt, w.expr, w.scope.raw(), w.storage, w.nodeCallback); + w.parametersDependencies = processAttributeGroupsAndParams(w.nodeScopeResolver, w.stmt, w.expr, w.scope.raw(), w.storage, w.nodeCallback); if (UNEXPECTED(w.parametersDependencies.isUndef())) return false; zval *closureCallArgs = ptclosure::attribute(w.expr, pt_cp_closure_call_args); diff --git a/turbo-ext/src/generated/ClosureProcessor.h b/turbo-ext/src/generated/ClosureProcessor.h index 7c958623e9f..c9b4eef96dd 100644 --- a/turbo-ext/src/generated/ClosureProcessor.h +++ b/turbo-ext/src/generated/ClosureProcessor.h @@ -53,57 +53,61 @@ inline constexpr char strings[] = "\0" /* 471 */ "PHPStan\\Analyser\\ParametersProcessor\0" /* 472 */ "getParametersProcessor\0" /* 509 */ - "nodeScopeResolver\0" /* 532 */ - "PHPStan\\Analyser\\NodeScopeResolver\0" /* 550 */ - "stmt\0" /* 585 */ - "PhpParser\\Node\\Stmt\0" /* 590 */ - "expr\0" /* 610 */ - "PhpParser\\Node\\Expr\\Closure\0" /* 615 */ - "scope\0" /* 643 */ - "PHPStan\\Analyser\\MutatingScope\0" /* 649 */ - "storage\0" /* 680 */ - "PHPStan\\Analyser\\ExpressionResultStorage\0" /* 688 */ - "nodeCallback\0" /* 729 */ - "context\0" /* 742 */ - "PHPStan\\Analyser\\ExpressionContext\0" /* 750 */ - "passedToType\0" /* 785 */ - "PHPStan\\Type\\Type\0" /* 798 */ - "nativePassedToType\0" /* 816 */ - "null\0" /* 835 */ - "PHPStan\\Analyser\\ProcessClosureResult\0" /* 840 */ - "processClosureNode\0" /* 878 */ - "processClosureNodeInternal\0" /* 897 */ - "call\0" /* 924 */ - "PhpParser\\Node\\Expr\0" /* 929 */ - "argumentTypes\0" /* 949 */ - "creationScope\0" /* 963 */ - "untilFixpoint\0" /* 977 */ - "processByRefInvocation\0" /* 991 */ - "entryScope\0" /* 1014 */ - "enter\0" /* 1025 */ - "byRefUses\0" /* 1031 */ - "walkByRefInvocation\0" /* 1041 */ - "byRefEntryTypes\0" /* 1061 */ - "PHPStan\\Dependency\\Dependencies\0" /* 1077 */ - "processDeferredByRefClosureBody\0" /* 1109 */ - "callableParameters\0" /* 1141 */ - "enterWithCapturedUses\0" /* 1160 */ - "source\0" /* 1182 */ - "assignByRefUses\0" /* 1189 */ - "closureScope\0" /* 1205 */ - "gatheredReturnStatementsWithScope\0" /* 1218 */ - "gatheredYieldStatementsWithScope\0" /* 1252 */ - "executionEnds\0" /* 1285 */ - "throwPoints\0" /* 1299 */ - "impurePoints\0" /* 1311 */ - "invalidateExpressions\0" /* 1324 */ - "refineClosureNodeScope\0" /* 1346 */ - "invalidatedExpressions\0" /* 1369 */ - "uses\0" /* 1392 */ - "processImmediatelyCalledCallable\0" /* 1397 */ - "PhpParser\\Node\\Expr\\ArrowFunction\0" /* 1430 */ - "PHPStan\\Analyser\\ProcessArrowFunctionResult\0" /* 1464 */ - "processArrowFunctionNode"; /* 1508 */ + "PHPStan\\Analyser\\AttributesHandler\0" /* 532 */ + "getAttributesHandler\0" /* 567 */ + "nodeScopeResolver\0" /* 588 */ + "PHPStan\\Analyser\\NodeScopeResolver\0" /* 606 */ + "stmt\0" /* 641 */ + "PhpParser\\Node\\Stmt\0" /* 646 */ + "expr\0" /* 666 */ + "PhpParser\\Node\\Expr\\Closure|PhpParser\\Node\\Expr\\ArrowFunction\0" /* 671 */ + "scope\0" /* 733 */ + "PHPStan\\Analyser\\MutatingScope\0" /* 739 */ + "storage\0" /* 770 */ + "PHPStan\\Analyser\\ExpressionResultStorage\0" /* 778 */ + "nodeCallback\0" /* 819 */ + "PHPStan\\Dependency\\Dependencies\0" /* 832 */ + "processAttributeGroupsAndParams\0" /* 864 */ + "PhpParser\\Node\\Expr\\Closure\0" /* 896 */ + "context\0" /* 924 */ + "PHPStan\\Analyser\\ExpressionContext\0" /* 932 */ + "passedToType\0" /* 967 */ + "PHPStan\\Type\\Type\0" /* 980 */ + "nativePassedToType\0" /* 998 */ + "null\0" /* 1017 */ + "PHPStan\\Analyser\\ProcessClosureResult\0" /* 1022 */ + "processClosureNode\0" /* 1060 */ + "processClosureNodeInternal\0" /* 1079 */ + "call\0" /* 1106 */ + "PhpParser\\Node\\Expr\0" /* 1111 */ + "argumentTypes\0" /* 1131 */ + "creationScope\0" /* 1145 */ + "untilFixpoint\0" /* 1159 */ + "processByRefInvocation\0" /* 1173 */ + "entryScope\0" /* 1196 */ + "enter\0" /* 1207 */ + "byRefUses\0" /* 1213 */ + "walkByRefInvocation\0" /* 1223 */ + "byRefEntryTypes\0" /* 1243 */ + "processDeferredByRefClosureBody\0" /* 1259 */ + "callableParameters\0" /* 1291 */ + "enterWithCapturedUses\0" /* 1310 */ + "source\0" /* 1332 */ + "assignByRefUses\0" /* 1339 */ + "closureScope\0" /* 1355 */ + "gatheredReturnStatementsWithScope\0" /* 1368 */ + "gatheredYieldStatementsWithScope\0" /* 1402 */ + "executionEnds\0" /* 1435 */ + "throwPoints\0" /* 1449 */ + "impurePoints\0" /* 1461 */ + "invalidateExpressions\0" /* 1474 */ + "refineClosureNodeScope\0" /* 1496 */ + "invalidatedExpressions\0" /* 1519 */ + "uses\0" /* 1542 */ + "processImmediatelyCalledCallable\0" /* 1547 */ + "PhpParser\\Node\\Expr\\ArrowFunction\0" /* 1580 */ + "PHPStan\\Analyser\\ProcessArrowFunctionResult\0" /* 1614 */ + "processArrowFunctionNode"; /* 1658 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 10), /* __construct $container */ reg::packed(48, 0, 72), /* __construct $expressionResultFactory */ @@ -112,85 +116,93 @@ inline constexpr reg::PackedArg args[] = { reg::packed(275, 0, 310), /* __construct $contextualClosureParameterResolver */ reg::packed(381, 0, 407), /* __construct $closureSignatureInference */ reg::packed(471, 0, 472), /* getParametersProcessor return */ - reg::packed(532, 0, 550), /* processClosureNode $nodeScopeResolver */ - reg::packed(585, 0, 590), /* processClosureNode $stmt */ - reg::packed(610, 0, 615), /* processClosureNode $expr */ - reg::packed(643, 0, 649), /* processClosureNode $scope */ - reg::packed(680, 0, 688), /* processClosureNode $storage */ - reg::packed(729, MAY_BE_CALLABLE), /* processClosureNode $nodeCallback */ - reg::packed(742, 0, 750), /* processClosureNode $context */ - reg::packed(785, MAY_BE_NULL, 798), /* processClosureNode $passedToType */ - reg::packed(816, MAY_BE_NULL, 798, false, false, 835), /* processClosureNode $nativePassedToType */ - reg::packed(471, 0, 840), /* processClosureNode return */ - reg::packed(532, 0, 550), /* processClosureNodeInternal $nodeScopeResolver */ - reg::packed(585, 0, 590), /* processClosureNodeInternal $stmt */ - reg::packed(610, 0, 615), /* processClosureNodeInternal $expr */ - reg::packed(643, 0, 649), /* processClosureNodeInternal $scope */ - reg::packed(680, 0, 688), /* processClosureNodeInternal $storage */ - reg::packed(729, MAY_BE_CALLABLE), /* processClosureNodeInternal $nodeCallback */ - reg::packed(742, 0, 750), /* processClosureNodeInternal $context */ - reg::packed(785, MAY_BE_NULL, 798), /* processClosureNodeInternal $passedToType */ - reg::packed(816, MAY_BE_NULL, 798, false, false, 835), /* processClosureNodeInternal $nativePassedToType */ - reg::packed(471, 0, 840), /* processClosureNodeInternal return */ - reg::packed(532, 0, 550), /* processByRefInvocation $nodeScopeResolver */ - reg::packed(610, 0, 615), /* processByRefInvocation $expr */ - reg::packed(924, 0, 929), /* processByRefInvocation $call */ - reg::packed(643, 0, 649), /* processByRefInvocation $scope */ - reg::packed(680, 0, 688), /* processByRefInvocation $storage */ - reg::packed(949, MAY_BE_ARRAY), /* processByRefInvocation $argumentTypes */ - reg::packed(963, 0, 649), /* processByRefInvocation $creationScope */ - reg::packed(977, MAY_BE_BOOL), /* processByRefInvocation $untilFixpoint */ + reg::packed(471, 0, 532), /* getAttributesHandler return */ + reg::packed(588, 0, 606), /* processAttributeGroupsAndParams $nodeScopeResolver */ + reg::packed(641, 0, 646), /* processAttributeGroupsAndParams $stmt */ + reg::packed(666, 0, 671), /* processAttributeGroupsAndParams $expr */ + reg::packed(733, 0, 739), /* processAttributeGroupsAndParams $scope */ + reg::packed(770, 0, 778), /* processAttributeGroupsAndParams $storage */ + reg::packed(819, MAY_BE_CALLABLE), /* processAttributeGroupsAndParams $nodeCallback */ + reg::packed(471, MAY_BE_NULL, 832), /* processAttributeGroupsAndParams return */ + reg::packed(588, 0, 606), /* processClosureNode $nodeScopeResolver */ + reg::packed(641, 0, 646), /* processClosureNode $stmt */ + reg::packed(666, 0, 896), /* processClosureNode $expr */ + reg::packed(733, 0, 739), /* processClosureNode $scope */ + reg::packed(770, 0, 778), /* processClosureNode $storage */ + reg::packed(819, MAY_BE_CALLABLE), /* processClosureNode $nodeCallback */ + reg::packed(924, 0, 932), /* processClosureNode $context */ + reg::packed(967, MAY_BE_NULL, 980), /* processClosureNode $passedToType */ + reg::packed(998, MAY_BE_NULL, 980, false, false, 1017), /* processClosureNode $nativePassedToType */ + reg::packed(471, 0, 1022), /* processClosureNode return */ + reg::packed(588, 0, 606), /* processClosureNodeInternal $nodeScopeResolver */ + reg::packed(641, 0, 646), /* processClosureNodeInternal $stmt */ + reg::packed(666, 0, 896), /* processClosureNodeInternal $expr */ + reg::packed(733, 0, 739), /* processClosureNodeInternal $scope */ + reg::packed(770, 0, 778), /* processClosureNodeInternal $storage */ + reg::packed(819, MAY_BE_CALLABLE), /* processClosureNodeInternal $nodeCallback */ + reg::packed(924, 0, 932), /* processClosureNodeInternal $context */ + reg::packed(967, MAY_BE_NULL, 980), /* processClosureNodeInternal $passedToType */ + reg::packed(998, MAY_BE_NULL, 980, false, false, 1017), /* processClosureNodeInternal $nativePassedToType */ + reg::packed(471, 0, 1022), /* processClosureNodeInternal return */ + reg::packed(588, 0, 606), /* processByRefInvocation $nodeScopeResolver */ + reg::packed(666, 0, 896), /* processByRefInvocation $expr */ + reg::packed(1106, 0, 1111), /* processByRefInvocation $call */ + reg::packed(733, 0, 739), /* processByRefInvocation $scope */ + reg::packed(770, 0, 778), /* processByRefInvocation $storage */ + reg::packed(1131, MAY_BE_ARRAY), /* processByRefInvocation $argumentTypes */ + reg::packed(1145, 0, 739), /* processByRefInvocation $creationScope */ + reg::packed(1159, MAY_BE_BOOL), /* processByRefInvocation $untilFixpoint */ reg::packed(471, MAY_BE_ARRAY), /* processByRefInvocation return */ - reg::packed(532, 0, 550), /* walkByRefInvocation $nodeScopeResolver */ - reg::packed(610, 0, 615), /* walkByRefInvocation $expr */ - reg::packed(643, 0, 649), /* walkByRefInvocation $scope */ - reg::packed(680, 0, 688), /* walkByRefInvocation $storage */ - reg::packed(1014, 0, 649), /* walkByRefInvocation $entryScope */ - reg::packed(1025, MAY_BE_CALLABLE), /* walkByRefInvocation $enter */ - reg::packed(1031, MAY_BE_ARRAY), /* walkByRefInvocation $byRefUses */ - reg::packed(977, MAY_BE_BOOL), /* walkByRefInvocation $untilFixpoint */ + reg::packed(588, 0, 606), /* walkByRefInvocation $nodeScopeResolver */ + reg::packed(666, 0, 896), /* walkByRefInvocation $expr */ + reg::packed(733, 0, 739), /* walkByRefInvocation $scope */ + reg::packed(770, 0, 778), /* walkByRefInvocation $storage */ + reg::packed(1196, 0, 739), /* walkByRefInvocation $entryScope */ + reg::packed(1207, MAY_BE_CALLABLE), /* walkByRefInvocation $enter */ + reg::packed(1213, MAY_BE_ARRAY), /* walkByRefInvocation $byRefUses */ + reg::packed(1159, MAY_BE_BOOL), /* walkByRefInvocation $untilFixpoint */ reg::packed(471, MAY_BE_ARRAY), /* walkByRefInvocation return */ - reg::packed(532, 0, 550), /* processDeferredByRefClosureBody $nodeScopeResolver */ - reg::packed(610, 0, 615), /* processDeferredByRefClosureBody $expr */ - reg::packed(643, 0, 649), /* processDeferredByRefClosureBody $scope */ - reg::packed(680, 0, 688), /* processDeferredByRefClosureBody $storage */ - reg::packed(729, MAY_BE_CALLABLE), /* processDeferredByRefClosureBody $nodeCallback */ - reg::packed(1061, MAY_BE_ARRAY), /* processDeferredByRefClosureBody $byRefEntryTypes */ - reg::packed(471, MAY_BE_NULL, 1077), /* processDeferredByRefClosureBody return */ - reg::packed(643, 0, 649), /* enterWithCapturedUses $scope */ - reg::packed(610, 0, 615), /* enterWithCapturedUses $expr */ - reg::packed(1141, MAY_BE_ARRAY), /* enterWithCapturedUses $callableParameters */ - reg::packed(963, 0, 649), /* enterWithCapturedUses $creationScope */ - reg::packed(471, 0, 649), /* enterWithCapturedUses return */ - reg::packed(643, 0, 649), /* assignByRefUses $scope */ - reg::packed(1182, 0, 649), /* assignByRefUses $source */ - reg::packed(1031, MAY_BE_ARRAY), /* assignByRefUses $byRefUses */ - reg::packed(471, 0, 649), /* assignByRefUses return */ - reg::packed(1205, 0, 649), /* refineClosureNodeScope $closureScope */ - reg::packed(643, 0, 649), /* refineClosureNodeScope $scope */ - reg::packed(610, 0, 615), /* refineClosureNodeScope $expr */ - reg::packed(1218, MAY_BE_ARRAY), /* refineClosureNodeScope $gatheredReturnStatementsWithScope */ - reg::packed(1252, MAY_BE_ARRAY), /* refineClosureNodeScope $gatheredYieldStatementsWithScope */ - reg::packed(1285, MAY_BE_ARRAY), /* refineClosureNodeScope $executionEnds */ - reg::packed(1299, MAY_BE_ARRAY), /* refineClosureNodeScope $throwPoints */ - reg::packed(1311, MAY_BE_ARRAY), /* refineClosureNodeScope $impurePoints */ - reg::packed(1324, MAY_BE_ARRAY), /* refineClosureNodeScope $invalidateExpressions */ - reg::packed(680, 0, 688), /* refineClosureNodeScope $storage */ - reg::packed(471, 0, 649), /* refineClosureNodeScope return */ - reg::packed(643, 0, 649), /* processImmediatelyCalledCallable $scope */ - reg::packed(1369, MAY_BE_ARRAY), /* processImmediatelyCalledCallable $invalidatedExpressions */ - reg::packed(1392, MAY_BE_ARRAY), /* processImmediatelyCalledCallable $uses */ - reg::packed(471, 0, 649), /* processImmediatelyCalledCallable return */ - reg::packed(532, 0, 550), /* processArrowFunctionNode $nodeScopeResolver */ - reg::packed(585, 0, 590), /* processArrowFunctionNode $stmt */ - reg::packed(610, 0, 1430), /* processArrowFunctionNode $expr */ - reg::packed(643, 0, 649), /* processArrowFunctionNode $scope */ - reg::packed(680, 0, 688), /* processArrowFunctionNode $storage */ - reg::packed(729, MAY_BE_CALLABLE), /* processArrowFunctionNode $nodeCallback */ - reg::packed(785, MAY_BE_NULL, 798), /* processArrowFunctionNode $passedToType */ - reg::packed(816, MAY_BE_NULL, 798, false, false, 835), /* processArrowFunctionNode $nativePassedToType */ - reg::packed(742, MAY_BE_NULL, 750, false, false, 835), /* processArrowFunctionNode $context */ - reg::packed(471, 0, 1464), /* processArrowFunctionNode return */ + reg::packed(588, 0, 606), /* processDeferredByRefClosureBody $nodeScopeResolver */ + reg::packed(666, 0, 896), /* processDeferredByRefClosureBody $expr */ + reg::packed(733, 0, 739), /* processDeferredByRefClosureBody $scope */ + reg::packed(770, 0, 778), /* processDeferredByRefClosureBody $storage */ + reg::packed(819, MAY_BE_CALLABLE), /* processDeferredByRefClosureBody $nodeCallback */ + reg::packed(1243, MAY_BE_ARRAY), /* processDeferredByRefClosureBody $byRefEntryTypes */ + reg::packed(471, MAY_BE_NULL, 832), /* processDeferredByRefClosureBody return */ + reg::packed(733, 0, 739), /* enterWithCapturedUses $scope */ + reg::packed(666, 0, 896), /* enterWithCapturedUses $expr */ + reg::packed(1291, MAY_BE_ARRAY), /* enterWithCapturedUses $callableParameters */ + reg::packed(1145, 0, 739), /* enterWithCapturedUses $creationScope */ + reg::packed(471, 0, 739), /* enterWithCapturedUses return */ + reg::packed(733, 0, 739), /* assignByRefUses $scope */ + reg::packed(1332, 0, 739), /* assignByRefUses $source */ + reg::packed(1213, MAY_BE_ARRAY), /* assignByRefUses $byRefUses */ + reg::packed(471, 0, 739), /* assignByRefUses return */ + reg::packed(1355, 0, 739), /* refineClosureNodeScope $closureScope */ + reg::packed(733, 0, 739), /* refineClosureNodeScope $scope */ + reg::packed(666, 0, 896), /* refineClosureNodeScope $expr */ + reg::packed(1368, MAY_BE_ARRAY), /* refineClosureNodeScope $gatheredReturnStatementsWithScope */ + reg::packed(1402, MAY_BE_ARRAY), /* refineClosureNodeScope $gatheredYieldStatementsWithScope */ + reg::packed(1435, MAY_BE_ARRAY), /* refineClosureNodeScope $executionEnds */ + reg::packed(1449, MAY_BE_ARRAY), /* refineClosureNodeScope $throwPoints */ + reg::packed(1461, MAY_BE_ARRAY), /* refineClosureNodeScope $impurePoints */ + reg::packed(1474, MAY_BE_ARRAY), /* refineClosureNodeScope $invalidateExpressions */ + reg::packed(770, 0, 778), /* refineClosureNodeScope $storage */ + reg::packed(471, 0, 739), /* refineClosureNodeScope return */ + reg::packed(733, 0, 739), /* processImmediatelyCalledCallable $scope */ + reg::packed(1519, MAY_BE_ARRAY), /* processImmediatelyCalledCallable $invalidatedExpressions */ + reg::packed(1542, MAY_BE_ARRAY), /* processImmediatelyCalledCallable $uses */ + reg::packed(471, 0, 739), /* processImmediatelyCalledCallable return */ + reg::packed(588, 0, 606), /* processArrowFunctionNode $nodeScopeResolver */ + reg::packed(641, 0, 646), /* processArrowFunctionNode $stmt */ + reg::packed(666, 0, 1580), /* processArrowFunctionNode $expr */ + reg::packed(733, 0, 739), /* processArrowFunctionNode $scope */ + reg::packed(770, 0, 778), /* processArrowFunctionNode $storage */ + reg::packed(819, MAY_BE_CALLABLE), /* processArrowFunctionNode $nodeCallback */ + reg::packed(967, MAY_BE_NULL, 980), /* processArrowFunctionNode $passedToType */ + reg::packed(998, MAY_BE_NULL, 980, false, false, 1017), /* processArrowFunctionNode $nativePassedToType */ + reg::packed(924, MAY_BE_NULL, 932, false, false, 1017), /* processArrowFunctionNode $context */ + reg::packed(471, 0, 1614), /* processArrowFunctionNode return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -199,16 +211,18 @@ using Sig = reg::Sig; namespace sig { inline constexpr sigtab::Sig __construct = { { 459 /* __construct */, 6, 0, 6, reg::NoArg, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getParametersProcessor = { { 509 /* getParametersProcessor */, 0, 6, 0, 6, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processClosureNode = { { 878 /* processClosureNode */, 8, 7, 9, 16, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig processClosureNodeInternal = { { 897 /* processClosureNodeInternal */, 8, 17, 9, 26, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processByRefInvocation = { { 991 /* processByRefInvocation */, 8, 27, 8, 35, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig walkByRefInvocation = { { 1041 /* walkByRefInvocation */, 8, 36, 8, 44, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processDeferredByRefClosureBody = { { 1109 /* processDeferredByRefClosureBody */, 6, 45, 6, 51, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig enterWithCapturedUses = { { 1160 /* enterWithCapturedUses */, 4, 52, 4, 56, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig assignByRefUses = { { 1189 /* assignByRefUses */, 3, 57, 3, 60, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig refineClosureNodeScope = { { 1346 /* refineClosureNodeScope */, 10, 61, 10, 71, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig processImmediatelyCalledCallable = { { 1397 /* processImmediatelyCalledCallable */, 3, 72, 3, 75, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig processArrowFunctionNode = { { 1508 /* processArrowFunctionNode */, 7, 76, 9, 85, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getAttributesHandler = { { 567 /* getAttributesHandler */, 0, 7, 0, 7, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processAttributeGroupsAndParams = { { 864 /* processAttributeGroupsAndParams */, 6, 8, 6, 14, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processClosureNode = { { 1060 /* processClosureNode */, 8, 15, 9, 24, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig processClosureNodeInternal = { { 1079 /* processClosureNodeInternal */, 8, 25, 9, 34, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processByRefInvocation = { { 1173 /* processByRefInvocation */, 8, 35, 8, 43, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig walkByRefInvocation = { { 1223 /* walkByRefInvocation */, 8, 44, 8, 52, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processDeferredByRefClosureBody = { { 1259 /* processDeferredByRefClosureBody */, 6, 53, 6, 59, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig enterWithCapturedUses = { { 1310 /* enterWithCapturedUses */, 4, 60, 4, 64, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig assignByRefUses = { { 1339 /* assignByRefUses */, 3, 65, 3, 68, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig refineClosureNodeScope = { { 1496 /* refineClosureNodeScope */, 10, 69, 10, 79, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig processImmediatelyCalledCallable = { { 1547 /* processImmediatelyCalledCallable */, 3, 80, 3, 83, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig processArrowFunctionNode = { { 1658 /* processArrowFunctionNode */, 7, 84, 9, 93, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::ClosureProcessor From 62b757bf26b0619f60f55ee1518a6c853aa7f986 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Tue, 27 Oct 2026 06:05:21 +0000 Subject: [PATCH 4/4] Use tentative return types of built-in methods as native return types Amp-Thread-ID: https://ampcode.com/threads/T-01a179f2-995f-7745-8d8c-4c174ba5c0ce --- .../SignatureMap/Php8SignatureMapProvider.php | 58 +++++++++++++++++++ src/Rules/Methods/OverridingMethodRule.php | 3 + .../nsrt/tentative-return-type-native.php | 46 +++++++++++++++ .../IfConstantConditionRuleTest.php | 15 +++-- 4 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/tentative-return-type-native.php diff --git a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php index ad470ff3ecc..ff9bab83e53 100644 --- a/src/Reflection/SignatureMap/Php8SignatureMapProvider.php +++ b/src/Reflection/SignatureMap/Php8SignatureMapProvider.php @@ -195,6 +195,64 @@ public function hasFunctionSignature(string $name): bool } public function getMethodSignatures(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array + { + return $this->applyTentativeReturnType( + $className, + $reflectionMethod, + $this->getMethodSignaturesWithoutTentativeReturnType($className, $methodName, $reflectionMethod), + ); + } + + /** + * PHP checks a tentative return type of a built-in method like a real one, + * and overriding it with an incompatible type is deprecated, + * so it is used as the native return type when the signature does not have one. + * + * @param array{positional: array, named: ?array} $signatures + * @return array{positional: array, named: ?array} + */ + private function applyTentativeReturnType(string $className, ?ReflectionMethod $reflectionMethod, array $signatures): array + { + if ($reflectionMethod === null || !$this->phpVersion->hasTentativeReturnTypes()) { + return $signatures; + } + + $tentativeReturnType = $reflectionMethod->getTentativeReturnType(); + if ($tentativeReturnType === null) { + return $signatures; + } + + $nativeReturnType = TypehintHelper::decideTypeFromReflection( + $tentativeReturnType, + selfClass: $this->reflectionProviderProvider->getReflectionProvider()->getClass($className), + ); + foreach ($signatures as $signatureType => $typeSignatures) { + if ($typeSignatures === null) { + continue; + } + + foreach ($typeSignatures as $i => $signature) { + $signatureNativeReturnType = $signature->getNativeReturnType(); + if (!$signatureNativeReturnType instanceof MixedType || $signatureNativeReturnType->isExplicitMixed()) { + continue; + } + + $signatures[$signatureType][$i] = new FunctionSignature( + $signature->getParameters(), + $signature->getReturnType(), + $nativeReturnType, + $signature->isVariadic(), + ); + } + } + + return $signatures; + } + + /** + * @return array{positional: array, named: ?array} + */ + private function getMethodSignaturesWithoutTentativeReturnType(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array { $lowerClassName = strtolower($className); if (!array_key_exists($lowerClassName, $this->map->classes)) { diff --git a/src/Rules/Methods/OverridingMethodRule.php b/src/Rules/Methods/OverridingMethodRule.php index 770dfe4fd72..698cba9e755 100644 --- a/src/Rules/Methods/OverridingMethodRule.php +++ b/src/Rules/Methods/OverridingMethodRule.php @@ -274,6 +274,9 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE $reportReturnType = !$realPrototype instanceof MethodPrototypeReflection || $realPrototype->getTentativeReturnType() === null || (is_bool($prototype->isBuiltin()) ? !$prototype->isBuiltin() : $prototype->isBuiltin()->no()); + } elseif ($realPrototype instanceof MethodPrototypeReflection && $realPrototype->getTentativeReturnType() !== null) { + // return types that are only tentative are not checked before PHP 8.1 + $reportReturnType = false; } else { if ($realPrototype instanceof MethodPrototypeReflection && $realPrototype->isInternal()) { if ( diff --git a/tests/PHPStan/Analyser/nsrt/tentative-return-type-native.php b/tests/PHPStan/Analyser/nsrt/tentative-return-type-native.php new file mode 100644 index 00000000000..fd1931799de --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/tentative-return-type-native.php @@ -0,0 +1,46 @@ += 8.1 + +namespace TentativeReturnTypeNative; + +use function PHPStan\Testing\assertNativeType; +use function PHPStan\Testing\assertType; + +/** + * @extends \ArrayObject + */ +class MyArrayObject extends \ArrayObject +{ + +} + +/** + * @implements \IteratorAggregate + */ +class MyAggregate implements \IteratorAggregate +{ + + #[\ReturnTypeWillChange] + public function getIterator() + { + return new \ArrayIterator([]); + } + +} + +class Foo +{ + + /** + * @param \ArrayObject $arrayObject + */ + public function doFoo(\ArrayObject $arrayObject, MyArrayObject $myArrayObject, \DateTimeImmutable $date, \SplObjectStorage $storage, MyAggregate $aggregate): void + { + assertType('int<0, max>', $arrayObject->count()); + assertNativeType('int', $arrayObject->count()); + assertNativeType('int', $myArrayObject->count()); + assertNativeType('int', $date->getTimestamp()); + assertNativeType('string', $storage->getHash(new \stdClass())); + assertNativeType('mixed', $aggregate->getIterator()); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php index 10fd1c6a98f..fd0a5a7fa66 100644 --- a/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php @@ -6,6 +6,7 @@ use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; use PHPUnit\Framework\Attributes\RequiresPhp; +use const PHP_VERSION_ID; /** * @extends RuleTestCase @@ -151,12 +152,16 @@ public function testReportPhpDoc(): void public function testBug4043(): void { $this->treatPhpDocTypesAsCertain = true; + $assertError = [ + 'Call to function assert() with true will always evaluate to true.', + 13, + ]; + if (PHP_VERSION_ID < 80100) { + // tentative return types are native only on PHP 8.1+ + $assertError[] = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; + } $this->analyse([__DIR__ . '/data/bug-4043.php'], [ - [ - 'Call to function assert() with true will always evaluate to true.', - 13, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', - ], + $assertError, [ 'If condition is always false.', 43,