diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index 7098877fb44..040b3b281be 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -34,3 +34,4 @@ parameters: unusedVariable: true unusedParameters: true reuseCollectedDataRuleErrors: true + disallowClassLevelTemplatesInStaticMethods: true diff --git a/conf/config.neon b/conf/config.neon index 0eb6c0c764b..95d09f63cad 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -66,6 +66,7 @@ parameters: unusedVariable: false unusedParameters: false reuseCollectedDataRuleErrors: false + disallowClassLevelTemplatesInStaticMethods: false fileExtensions: - php checkAdvancedIsset: false diff --git a/conf/parametersSchema.neon b/conf/parametersSchema.neon index 62dab9d9c3b..0fea5c221ef 100644 --- a/conf/parametersSchema.neon +++ b/conf/parametersSchema.neon @@ -63,6 +63,7 @@ parametersSchema: unusedVariable: bool() unusedParameters: bool() reuseCollectedDataRuleErrors: bool() + disallowClassLevelTemplatesInStaticMethods: bool() ]) fileExtensions: listOf(string()) checkAdvancedIsset: bool() diff --git a/conf/services.neon b/conf/services.neon index 1ec53e8e20c..cc5be33af60 100644 --- a/conf/services.neon +++ b/conf/services.neon @@ -138,6 +138,7 @@ services: phpParser: @stubParser resolvedPhpDocBlockCacheCountMax: %cache.resolvedPhpDocBlockCacheCountMax% nameScopeMapMemoryCacheCountMax: %cache.nameScopeMapMemoryCacheCountMax% + disallowClassLevelTemplatesInStaticMethods: %featureToggles.disallowClassLevelTemplatesInStaticMethods% autowired: false fileExcluderAnalyse: diff --git a/src/Type/FileTypeMapper.php b/src/Type/FileTypeMapper.php index a61ad12860e..fa86a52db2d 100644 --- a/src/Type/FileTypeMapper.php +++ b/src/Type/FileTypeMapper.php @@ -91,6 +91,8 @@ public function __construct( private int $resolvedPhpDocBlockCacheCountMax, #[AutowiredParameter(ref: '%cache.nameScopeMapMemoryCacheCountMax%')] int $nameScopeMapMemoryCacheCountMax, + #[AutowiredParameter(ref: '%featureToggles.disallowClassLevelTemplatesInStaticMethods%')] + private bool $disallowClassLevelTemplatesInStaticMethods, ) { // 0 kept one entry here rather than meaning "no limit" as it does for the other bounded @@ -387,7 +389,12 @@ private function getNameScopeMap(string $fileName): array // a map built from the simplified AST of a file outside the analysed paths lacks // the name scopes of classes declared inside function bodies, so it must not be // reused once the file gets analysed, nor the other way around - $cacheKey = sprintf('ftm-%s%s', $this->returnsSimplifiedAst($fileName) ? 'simplified-' : '', $fileName); + $cacheKey = sprintf( + 'ftm-%s%s%s', + $this->returnsSimplifiedAst($fileName) ? 'simplified-' : '', + $this->disallowClassLevelTemplatesInStaticMethods ? 'static-own-scope-' : '', + $fileName, + ); // v6: v5 entries may be poisoned by the turbo comment-loss bug // (https://github.com/phpstan/phpstan/issues/15037) - a parse that // silently dropped every comment cached PHPDoc-less name-scope maps, @@ -553,13 +560,22 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA } if ($node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\ClassMethod || $node instanceof Node\Stmt\Function_ || $node instanceof Node\PropertyHook) { + $parentNameScope = array_last($typeMapStack); + + // static methods exist in their own scope + if ( + $this->disallowClassLevelTemplatesInStaticMethods + && $node instanceof Node\Stmt\ClassMethod + && $node->isStatic() + ) { + $parentNameScope = null; + } + if ($phpDocNode !== null) { if ($node instanceof Node\Stmt\ClassLike) { $typeAliasStack[] = $this->getTypeAliasesMap($phpDocNode); } - $parentNameScope = array_last($typeMapStack); - $typeMapStack[] = new IntermediaryNameScope( $namespaceUsesTracker->getNamespaceUses(), $className, @@ -572,7 +588,6 @@ function (Node $node) use ($fileName, $lookForTrait, &$traitFound, $traitMethodA } elseif ($node instanceof Node\Stmt\ClassLike) { $typeAliasStack[] = []; } else { - $parentNameScope = array_last($typeMapStack); $typeMapStack[] = new IntermediaryNameScope( $namespaceUsesTracker->getNamespaceUses(), $className, diff --git a/tests/PHPStan/Analyser/nsrt/generics.php b/tests/PHPStan/Analyser/nsrt/generics.php index 6e150f33349..4a792501687 100644 --- a/tests/PHPStan/Analyser/nsrt/generics.php +++ b/tests/PHPStan/Analyser/nsrt/generics.php @@ -1410,7 +1410,7 @@ class CarFactoryProcessor { */ public function process($class): void { $car = $class::create(); - assertType(Car::class, $car); + assertType(T::class, $car); // @template T is unknown in static method } } diff --git a/tests/PHPStan/Analyser/nsrt/static-methods-template-types.php b/tests/PHPStan/Analyser/nsrt/static-methods-template-types.php new file mode 100644 index 00000000000..065bd86dfd5 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/static-methods-template-types.php @@ -0,0 +1,49 @@ +analyse([__DIR__ . '/data/method-signature-variance-constructor.php'], []); - $this->analyse([__DIR__ . '/data/method-signature-variance-static.php'], [ - [ - 'Template type X is declared as covariant, but occurs in contravariant position in parameter a of method MethodSignatureVariance\StaticMethod\B::a().', - 43, - ], - [ - 'Template type X is declared as covariant, but occurs in contravariant position in parameter c of method MethodSignatureVariance\StaticMethod\B::a().', - 43, - ], - [ - 'Template type X is declared as covariant, but occurs in contravariant position in return type of method MethodSignatureVariance\StaticMethod\B::c().', - 49, - ], - [ - 'Template type X is declared as contravariant, but occurs in covariant position in parameter b of method MethodSignatureVariance\StaticMethod\C::a().', - 62, - ], - [ - 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\StaticMethod\C::b().', - 65, - ], - [ - 'Template type X is declared as contravariant, but occurs in covariant position in return type of method MethodSignatureVariance\StaticMethod\C::d().', - 71, - ], - ]); + // class-level @template X is unknown in static methods + $this->analyse([__DIR__ . '/data/method-signature-variance-static.php'], []); } public function testBug8880(): void diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index 33f7859302b..de5ef96e6f7 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -541,7 +541,12 @@ public function testBug6249(): void // discussion https://github.com/phpstan/phpstan/discussions/6249 $this->checkThisOnly = false; $this->checkExplicitMixed = true; - $this->analyse([__DIR__ . '/data/bug-6249.php'], []); + $this->analyse([__DIR__ . '/data/bug-6249.php'], [ + [ + 'Parameter #1 $iterable of static method Bug6249N3\Cw<(int|string),mixed>::fromIterable() expects iterable, Bug6249N2\Eii given.', + 145, + ], + ]); } public function testBug5749(): void diff --git a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php index 1a5628f6584..3b1546da5a6 100644 --- a/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php @@ -153,6 +153,10 @@ public function testExistingClassInTypehint(): void 'Template type U of method TestMethodTypehints\TemplateTypeMissingInParameter::doFoo() is not referenced in a parameter.', 130, ], + [ + 'Method TestMethodTypehints\TemplateInStaticMethod::doBar() has invalid return type TestMethodTypehints\T.', + 163, + ], ]); } diff --git a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php index 260fde1475f..86174f2feb0 100644 --- a/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php @@ -1248,7 +1248,29 @@ public function testBug13043(): void public function testBug12739(): void { - $this->analyse([__DIR__ . '/data/bug-12739.php'], []); + // @template TScalar is unknown in static methods + $this->analyse([__DIR__ . '/data/bug-12739.php'], [ + [ + "Method MyInt::getClassIdentify() should return class-string> but returns 'MyInt'.", + 20, + ], + [ + "Method MyString::getClassIdentify() should return class-string> but returns 'MyString'.", + 29, + ], + [ + "Method MyLowerString::getClassIdentify() should return class-string> but returns 'MyLowerString'.", + 38, + ], + [ + "Method MyUpperString::getClassIdentify() should return class-string> but returns 'MyUpperString'.", + 47, + ], + [ + "Method MyClassString::getClassIdentify() should return class-string> but returns 'MyClassString'.", + 56, + ], + ]); } public function testBug12928(): void diff --git a/tests/PHPStan/Rules/Methods/data/bug-6249.php b/tests/PHPStan/Rules/Methods/data/bug-6249.php index 7d2db1ed3e3..dd0535e3a62 100644 --- a/tests/PHPStan/Rules/Methods/data/bug-6249.php +++ b/tests/PHPStan/Rules/Methods/data/bug-6249.php @@ -106,6 +106,18 @@ public static function fromIterable(iterable $iterable): self return new self($iterable); } + /** + * @template UKey of array-key + * @template U + * @param iterable $iterable + * + * @return self + */ + public static function fromIterableCorrect(iterable $iterable): self + { + return new self($iterable); + } + public function count(): int { if (is_array($this->iterable) || $this->iterable instanceof Countable) { @@ -131,6 +143,7 @@ class Foo public function doFoo() { \Bug6249N3\Cw::fromIterable(new \Bug6249N2\Eii([])); + \Bug6249N3\Cw::fromIterableCorrect(new \Bug6249N2\Eii([])); } } diff --git a/tests/PHPStan/Rules/Methods/data/typehints.php b/tests/PHPStan/Rules/Methods/data/typehints.php index f967e44906d..a06bded239e 100644 --- a/tests/PHPStan/Rules/Methods/data/typehints.php +++ b/tests/PHPStan/Rules/Methods/data/typehints.php @@ -142,3 +142,27 @@ public function doBar(string $class): void } } + +/** + * @template T + */ +class TemplateInStaticMethod +{ + + /** + * @return T + */ + public function doFoo() + { + + } + + /** + * @return T + */ + public static function doBar() + { + + } + +} diff --git a/tests/PHPStan/Type/FileTypeMapperTest.php b/tests/PHPStan/Type/FileTypeMapperTest.php index 49658c33707..68168316538 100644 --- a/tests/PHPStan/Type/FileTypeMapperTest.php +++ b/tests/PHPStan/Type/FileTypeMapperTest.php @@ -303,6 +303,7 @@ private function createFileTypeMapper(CacheStorage $storage): FileTypeMapper $container->getByType(FileContentHasher::class), 2048, 128, + false, ); }