diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index b0f15a8a69d..9ba71fefc0a 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -1352,12 +1352,6 @@ jobs: OUTPUT=$(../bashunit -a exit_code "0" "../../bin/phpstan analyse -vvv --error-format raw") echo "$OUTPUT" ../bashunit -a not_contains 'not found' "$OUTPUT" - # A reflection error, here a class that extends itself, still keeps the result cache from - # being saved. - cp loop/Loop.php src/Loop.php - OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vvv --error-format raw") - echo "$OUTPUT" - ../bashunit -a contains 'Result cache was not saved because of non-ignorable exception: Reflection error: Circular reference' "$OUTPUT" - script: | cd e2e/result-cache-internal-error composer dump-autoload @@ -1367,6 +1361,12 @@ jobs: ../bashunit -a contains 'Internal error: The rule crashed.' "$OUTPUT" ../bashunit -a contains 'Result cache was not saved' "$OUTPUT" test ! -f tmp/resultCache.php + # An exception about the analysed code other than a parse error, here a class that is not + # found, also keeps the result cache from being saved. + OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -c class-not-found.neon -vvv --error-format raw") + echo "$OUTPUT" + ../bashunit -a contains 'Result cache was not saved because of non-ignorable exception: Class ResultCacheE2EInternalError\Missing was not found' "$OUTPUT" + test ! -f tmp/resultCache.php - script: | cd e2e/result-cache-string-class ../../bin/phpstan analyse diff --git a/e2e/result-cache-internal-error/class-not-found.neon b/e2e/result-cache-internal-error/class-not-found.neon new file mode 100644 index 00000000000..4f32bf4b7e6 --- /dev/null +++ b/e2e/result-cache-internal-error/class-not-found.neon @@ -0,0 +1,8 @@ +parameters: + level: 8 + tmpDir: tmp + paths: + - src + +rules: + - ResultCacheE2EInternalError\ClassNotFoundRule diff --git a/e2e/result-cache-internal-error/rule/ClassNotFoundRule.php b/e2e/result-cache-internal-error/rule/ClassNotFoundRule.php new file mode 100644 index 00000000000..14d529c4e6c --- /dev/null +++ b/e2e/result-cache-internal-error/rule/ClassNotFoundRule.php @@ -0,0 +1,27 @@ + + */ +final class ClassNotFoundRule implements Rule +{ + + public function getNodeType(): string + { + return Function_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + throw new ClassNotFoundException('ResultCacheE2EInternalError\Missing'); + } + +} diff --git a/e2e/result-cache-parse-error/loop/Loop.php b/e2e/result-cache-parse-error/loop/Loop.php deleted file mode 100644 index 048567eb03e..00000000000 --- a/e2e/result-cache-parse-error/loop/Loop.php +++ /dev/null @@ -1,8 +0,0 @@ -changeBaseClass($calledOnTypeReflection)->getStaticObjectType()); + $staticObjectType = $type->changeBaseClass($calledOnTypeReflection)->getStaticObjectType(); + if ($staticObjectType instanceof GenericObjectType) { + // do not carry over the class reflection of the declaring class, + // it would not know the type arguments of the resolved type + $staticObjectType = new GenericObjectType( + $staticObjectType->getClassName(), + $staticObjectType->getTypes(), + $staticObjectType->getSubtractedType(), + variances: $staticObjectType->getVariances(), + ); + } + + return $traverse($staticObjectType); } return $this->calledOnType; diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 22991812fa2..323b5af7278 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = '1cb5faf'; + public const EXPECTED_EXTENSION_VERSION = '529120b'; private static bool $active = false; diff --git a/tests/PHPStan/Analyser/nsrt/generic-static-concrete-arguments.php b/tests/PHPStan/Analyser/nsrt/generic-static-concrete-arguments.php new file mode 100644 index 00000000000..720f2246b7c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/generic-static-concrete-arguments.php @@ -0,0 +1,49 @@ + */ + public function concrete() + { + return new static(); + } + + /** @return static */ + public function withTemplate() + { + return new static(); + } + + /** @return array */ + public function all(): array + { + return []; + } + + /** @return TValue */ + public function first() + { + return 1; + } + +} + +/** + * @param Box $box + */ +function test(Box $box): void +{ + assertType('GenericStaticConcreteArguments\Box', $box->concrete()); + assertType('array', $box->concrete()->all()); + assertType('string', $box->concrete()->first()); + assertType('array', $box->withTemplate()->all()); +} diff --git a/turbo-ext/src/CalledOnTypeUnresolvedMethodPrototypeReflection.cpp b/turbo-ext/src/CalledOnTypeUnresolvedMethodPrototypeReflection.cpp index 57c3986c2c1..ce5c9373118 100644 --- a/turbo-ext/src/CalledOnTypeUnresolvedMethodPrototypeReflection.cpp +++ b/turbo-ext/src/CalledOnTypeUnresolvedMethodPrototypeReflection.cpp @@ -217,6 +217,29 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection } zv::Val staticObjectType = pt_type_call(Z_OBJ_P(changed.raw()), PT_LC("getstaticobjecttype"), 0, NULL); if (UNEXPECTED(staticObjectType.isUndef())) return; + /* a GenericObjectType is rebuilt without the class reflection of the + * declaring class, which would not know the resolved type arguments: + * new GenericObjectType($t->getClassName(), $t->getTypes(), $t->getSubtractedType(), variances: $t->getVariances()) */ + bool isGenericObject; + pt_type_instanceof_ce(staticObjectType.raw(), pt_ce_generic_object_type, isGenericObject); + if (isGenericObject) { + zend_object *objectType = Z_OBJ_P(staticObjectType.raw()); + zv::Val className = pt_type_call(objectType, PT_LC("getclassname"), 0, NULL); + if (UNEXPECTED(className.isUndef())) return; + zv::Val objectTypes = pt_type_call(objectType, PT_LC("gettypes"), 0, NULL); + if (UNEXPECTED(objectTypes.isUndef())) return; + zv::Val subtracted = pt_type_call(objectType, PT_LC("getsubtractedtype"), 0, NULL); + if (UNEXPECTED(subtracted.isUndef())) return; + zv::Val objectVariances = pt_type_call(objectType, PT_LC("getvariances"), 0, NULL); + if (UNEXPECTED(objectVariances.isUndef())) return; + if (UNEXPECTED(Z_TYPE_P(className.raw()) != IS_STRING)) { + zend_type_error("phpstan_turbo: getClassName() must return a string"); + return; + } + zval rebuiltRaw; + if (UNEXPECTED(!pt_generic_object_type_new(&rebuiltRaw, Z_STR_P(className.raw()), objectTypes.raw(), subtracted.raw(), NULL, objectVariances.raw()))) return; + staticObjectType = zv::Val::adopt(rebuiltRaw); + } zv::Val traversed = pt_type_call_callable(&argv[1], 1, staticObjectType.raw()); if (UNEXPECTED(traversed.isUndef())) return; traversed.intoReturnValue(return_value);