From b51f024f2ad5ecf92c23edea219a40c316d918f7 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:53:15 +0000 Subject: [PATCH 1/5] Make `ReflectionObject`, `RecursiveFilterIterator`, `ParentIterator`, `RecursiveCachingIterator` and `RecursiveRegexIterator` generic in stubs * Add `stubs/ReflectionObject.stub` and `stubs/ReflectionObjectWithLazyObjects.stub` declaring `ReflectionObject` as `@extends ReflectionClass` with `@param T $argument` on the constructor, mirroring the covariant/invariant split of the two `ReflectionClass` stub variants. * Register both files in `ReflectionClassStubFilesExtension` next to the matching `ReflectionClass` stub, so the lazy-objects (PHP >= 8.4) and non-lazy variants stay in sync. * Probed the sibling subclass of `ReflectionClass`: `ReflectionEnum` already declares `@extends ReflectionClass` and needed no change. * Swept the same "builtin subclass of a generic stubbed class has no `@extends`, so the type arguments are erased" pattern in `stubs/iterable.stub`: added generic declarations for `RecursiveFilterIterator`, `ParentIterator`, `RecursiveCachingIterator` and `RecursiveRegexIterator`, the recursive counterparts of the already generic `FilterIterator`, `CachingIterator` and `RegexIterator`. * `OverridingMethodRuleTest::testBug9615` expectation updated: the declaring class of `FilterIterator::accept()` seen through `RecursiveFilterIterator` is now `FilterIterator>` instead of the erased `Traversable` bound. --- .../ReflectionClassStubFilesExtension.php | 2 + stubs/ReflectionObject.stub | 15 ++++ stubs/ReflectionObjectWithLazyObjects.stub | 15 ++++ stubs/iterable.stub | 87 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-15032.php | 33 +++++++ .../Analyser/nsrt/recursive-iterators.php | 61 +++++++++++++ .../Methods/OverridingMethodRuleTest.php | 2 +- 7 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 stubs/ReflectionObject.stub create mode 100644 stubs/ReflectionObjectWithLazyObjects.stub create mode 100644 tests/PHPStan/Analyser/nsrt/bug-15032.php create mode 100644 tests/PHPStan/Analyser/nsrt/recursive-iterators.php diff --git a/src/PhpDoc/ReflectionClassStubFilesExtension.php b/src/PhpDoc/ReflectionClassStubFilesExtension.php index f47fe54bbae..a6844801d4a 100644 --- a/src/PhpDoc/ReflectionClassStubFilesExtension.php +++ b/src/PhpDoc/ReflectionClassStubFilesExtension.php @@ -18,11 +18,13 @@ public function getFiles(): array if (!$this->phpVersion->supportsLazyObjects()) { return [ __DIR__ . '/../../stubs/ReflectionClass.stub', + __DIR__ . '/../../stubs/ReflectionObject.stub', ]; } return [ __DIR__ . '/../../stubs/ReflectionClassWithLazyObjects.stub', + __DIR__ . '/../../stubs/ReflectionObjectWithLazyObjects.stub', ]; } diff --git a/stubs/ReflectionObject.stub b/stubs/ReflectionObject.stub new file mode 100644 index 00000000000..6f779116406 --- /dev/null +++ b/stubs/ReflectionObject.stub @@ -0,0 +1,15 @@ + + */ +class ReflectionObject extends ReflectionClass +{ + + /** + * @param T $argument + */ + public function __construct($argument) {} + +} diff --git a/stubs/ReflectionObjectWithLazyObjects.stub b/stubs/ReflectionObjectWithLazyObjects.stub new file mode 100644 index 00000000000..8979b9142c2 --- /dev/null +++ b/stubs/ReflectionObjectWithLazyObjects.stub @@ -0,0 +1,15 @@ + + */ +class ReflectionObject extends ReflectionClass +{ + + /** + * @param T $argument + */ + public function __construct($argument) {} + +} diff --git a/stubs/iterable.stub b/stubs/iterable.stub index baf5ca90837..b2982ba1522 100644 --- a/stubs/iterable.stub +++ b/stubs/iterable.stub @@ -456,6 +456,93 @@ class RegexIterator extends FilterIterator { public function key() {} } +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as RecursiveIterator + * + * @template-extends FilterIterator + * @template-implements RecursiveIterator + */ +abstract class RecursiveFilterIterator extends FilterIterator implements RecursiveIterator { + /** + * @param TIterator $iterator + */ + public function __construct(RecursiveIterator $iterator) {} + + /** + * @return bool + */ + public function hasChildren() {} + + /** + * @return static + */ + public function getChildren() {} +} + +/** + * @template-covariant TKey + * @template-covariant TValue + * @template TIterator as RecursiveIterator + * + * @template-extends RecursiveFilterIterator + */ +class ParentIterator extends RecursiveFilterIterator { +} + +/** + * @template TKey + * @template TValue + * @template TIterator as Iterator + * + * @template-extends CachingIterator + * @template-implements RecursiveIterator + */ +class RecursiveCachingIterator extends CachingIterator implements RecursiveIterator { + /** + * @param TIterator $iterator + * @param int-mask-of $flags + */ + public function __construct(Iterator $iterator, int $flags = CachingIterator::CALL_TOSTRING) {} + + /** + * @return bool + */ + public function hasChildren() {} + + /** + * @return static + */ + public function getChildren() {} +} + +/** + * @template TKey + * @template TValue + * @template TIterator of RecursiveIterator + * + * @template-extends RegexIterator + * @template-implements RecursiveIterator + */ +class RecursiveRegexIterator extends RegexIterator implements RecursiveIterator { + /** + * @param TIterator $iterator + * @param RegexIterator::MATCH|RegexIterator::GET_MATCH|RegexIterator::ALL_MATCHES|RegexIterator::SPLIT|RegexIterator::REPLACE $mode + */ + public function __construct(RecursiveIterator $iterator, string $pattern, int $mode = RegexIterator::MATCH, int $flags = 0, int $preg_flags = 0) {} + + /** + * @return bool + */ + public function hasChildren() {} + + /** + * @return static + */ + public function getChildren() {} +} + /** * @template-implements Iterator */ diff --git a/tests/PHPStan/Analyser/nsrt/bug-15032.php b/tests/PHPStan/Analyser/nsrt/bug-15032.php new file mode 100644 index 00000000000..086402e959e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-15032.php @@ -0,0 +1,33 @@ +', $ref); + + $ret = $ref->newInstance(); + assertType('T of object (function Bug15032\createInstance(), argument)', $ret); + + return $ret; +} + +function concreteObject(\Exception $e): void +{ + $ref = new ReflectionObject($e); + assertType('ReflectionObject', $ref); + assertType('class-string', $ref->getName()); + assertType('class-string', $ref->name); + assertType('Exception', $ref->newInstance()); + assertType('Exception', $ref->newInstanceArgs([])); + assertType('Exception', $ref->newInstanceWithoutConstructor()); +} diff --git a/tests/PHPStan/Analyser/nsrt/recursive-iterators.php b/tests/PHPStan/Analyser/nsrt/recursive-iterators.php new file mode 100644 index 00000000000..119f22ad077 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/recursive-iterators.php @@ -0,0 +1,61 @@ + $parent + * @param RecursiveCachingIterator $caching + * @param RecursiveRegexIterator $regex + */ + public function doFoo($parent, $caching, $regex): void + { + foreach ($parent as $key => $value) { + assertType('string', $key); + assertType('SplFileInfo', $value); + } + foreach ($caching as $key => $value) { + assertType('string', $key); + assertType('SplFileInfo', $value); + } + foreach ($regex as $key => $value) { + assertType('string', $key); + assertType('SplFileInfo', $value); + } + + assertType('ParentIterator', $parent->getChildren()); + assertType('RecursiveCachingIterator', $caching->getChildren()); + assertType('RecursiveRegexIterator', $regex->getChildren()); + + assertType('Iterator', $parent->getInnerIterator()); + } + + /** + * @param RecursiveArrayIterator $it + */ + public function doBar($it): void + { + $caching = new RecursiveCachingIterator($it); + assertType('RecursiveCachingIterator>', $caching); + assertType('string', $caching->current()); + assertType('int', $caching->key()); + + $parent = new ParentIterator($it); + assertType('ParentIterator>', $parent); + + $regex = new RecursiveRegexIterator($it, '~foo~'); + assertType('RecursiveRegexIterator>', $regex); + assertType('string', $regex->current()); + assertType('int', $regex->key()); + } + +} diff --git a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php index 910ea477e02..366c39b9964 100644 --- a/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php +++ b/tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php @@ -673,7 +673,7 @@ public function testBug9615(): void $this->phpVersionId = PHP_VERSION_ID; $this->analyse([__DIR__ . '/data/bug-9615.php'], [ [ - 'Return type mixed of method Bug9615\ExpectComplaintsHere::accept() is not covariant with tentative return type bool of method FilterIterator>::accept().', + 'Return type mixed of method Bug9615\ExpectComplaintsHere::accept() is not covariant with tentative return type bool of method FilterIterator>::accept().', 19, $tipText, ], From 8e25fcdb2ac4a9799b013ea9a4bae63ef71742f4 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 31 Jul 2026 16:20:55 +0000 Subject: [PATCH 2/5] Fill skipCheckGenericClasses from #[RequireGenericsInBleedingEdgeOnly] in stubs A class that only becomes generic in a minor version cannot start requiring type arguments right away - missingType.generics would fire in code that used to be fine - so it is listed in featureToggles.skipCheckGenericClasses and the check is deferred to bleeding edge. Listing the class name in conf/config.neon keeps that decision far away from the stub that makes the class generic in the first place; the attribute puts the two next to each other. The classes are not collected by composer-attribute-collector like the rest of PHPStan's attributes: the classes live in .stub files, which the class map generator does not look at (it scans .php and .inc), and the collector reads a class' attributes through runtime reflection, which for an internal class such as ReflectionObject reports the attributes of the real PHP class and never the ones written in a stub. SkipCheckGenericClassesExtension reads the stub files itself instead, parsing only those that mention the attribute at all, and registers them as container dependencies so that editing a stub rebuilds the compiled container. DOMNamedNodeMap, so far the only entry of the parameter, is marked with the attribute in stubs/dom.stub instead. Co-Authored-By: Claude Opus 5 --- conf/config.neon | 3 +- .../SkipCheckGenericClassesExtension.php | 126 ++++++++++++++++++ .../RequireGenericsInBleedingEdgeOnly.php | 23 ++++ stubs/dom.stub | 1 + ...enericClassesBleedingEdgeExtensionTest.php | 26 ++++ .../SkipCheckGenericClassesExtensionTest.php | 18 +++ 6 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 src/DependencyInjection/SkipCheckGenericClassesExtension.php create mode 100644 src/PhpDoc/RequireGenericsInBleedingEdgeOnly.php create mode 100644 tests/PHPStan/DependencyInjection/SkipCheckGenericClassesBleedingEdgeExtensionTest.php create mode 100644 tests/PHPStan/DependencyInjection/SkipCheckGenericClassesExtensionTest.php diff --git a/conf/config.neon b/conf/config.neon index 1cd6efbf7f9..b9f2e9813aa 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -28,8 +28,7 @@ parameters: bleedingEdge: false checkNonStringableDynamicAccess: false checkParameterCastableToNumberFunctions: false - skipCheckGenericClasses: - - DOMNamedNodeMap + skipCheckGenericClasses: [] stricterFunctionMap: false reportPreciseLineForUnusedFunctionParameter: false checkPrintfParameterTypes: false diff --git a/src/DependencyInjection/SkipCheckGenericClassesExtension.php b/src/DependencyInjection/SkipCheckGenericClassesExtension.php new file mode 100644 index 00000000000..267f8c661e2 --- /dev/null +++ b/src/DependencyInjection/SkipCheckGenericClassesExtension.php @@ -0,0 +1,126 @@ +compiler->addDependencies($stubFiles); + + $builder = $this->getContainerBuilder(); + if ((bool) $builder->parameters['featureToggles']['bleedingEdge']) { + return; + } + + $builder->parameters['featureToggles']['skipCheckGenericClasses'] = array_values(array_unique(array_merge( + $builder->parameters['featureToggles']['skipCheckGenericClasses'], + self::findClasses($stubFiles), + ))); + } + + /** + * @param list $stubFiles + * @return list + */ + private static function findClasses(array $stubFiles): array + { + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + $nodeFinder = new NodeFinder(); + $classes = []; + + foreach ($stubFiles as $stubFile) { + $contents = file_get_contents($stubFile); + if ($contents === false) { + throw new ShouldNotHappenException(sprintf('Could not read stub file %s.', $stubFile)); + } + + if (!str_contains($contents, RequireGenericsInBleedingEdgeOnly::class)) { + continue; + } + + $traverser = new NodeTraverser(new NameResolver()); + $nodes = $traverser->traverse($parser->parse($contents) ?? []); + + foreach ($nodeFinder->findInstanceOf($nodes, ClassLike::class) as $classLike) { + $className = $classLike->namespacedName; + if ($className === null) { + continue; + } + + foreach ($classLike->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if ($attr->name->toString() !== RequireGenericsInBleedingEdgeOnly::class) { + continue; + } + + $classes[] = $className->toString(); + } + } + } + } + + sort($classes); + + return $classes; + } + + /** + * @return list + */ + private static function findStubFiles(): array + { + $files = []; + $directoryIterator = new RecursiveDirectoryIterator(__DIR__ . '/../../stubs', RecursiveDirectoryIterator::SKIP_DOTS); + foreach (new RecursiveIteratorIterator($directoryIterator) as $file) { + if ($file->getExtension() !== 'stub') { + continue; + } + + $files[] = $file->getPathname(); + } + + sort($files); + + return $files; + } + +} diff --git a/src/PhpDoc/RequireGenericsInBleedingEdgeOnly.php b/src/PhpDoc/RequireGenericsInBleedingEdgeOnly.php new file mode 100644 index 00000000000..b18a3cd5188 --- /dev/null +++ b/src/PhpDoc/RequireGenericsInBleedingEdgeOnly.php @@ -0,0 +1,23 @@ +getParameter('featureToggles'); + $this->assertSame([], $featureToggles['skipCheckGenericClasses']); + } + + /** + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + ]; + } + +} diff --git a/tests/PHPStan/DependencyInjection/SkipCheckGenericClassesExtensionTest.php b/tests/PHPStan/DependencyInjection/SkipCheckGenericClassesExtensionTest.php new file mode 100644 index 00000000000..11ba33b40ef --- /dev/null +++ b/tests/PHPStan/DependencyInjection/SkipCheckGenericClassesExtensionTest.php @@ -0,0 +1,18 @@ +getParameter('featureToggles'); + $this->assertSame([ + 'DOMNamedNodeMap', + ], $featureToggles['skipCheckGenericClasses']); + } + +} From 799c6564484b68fb2fdd7b263e9b6e9f45650ade Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 31 Jul 2026 16:21:19 +0000 Subject: [PATCH 3/5] Mark the classes made generic here with #[RequireGenericsInBleedingEdgeOnly] ReflectionObject, RecursiveFilterIterator, ParentIterator, RecursiveCachingIterator and RecursiveRegexIterator all gained type parameters in the previous commit's parent. Code that already mentions them without type arguments was written when they had none, so requiring the arguments now would report missingType.generics on code that could not have been written any other way. The check is deferred to bleeding edge, like it is for DOMNamedNodeMap. Co-Authored-By: Claude Opus 5 --- stubs/ReflectionObject.stub | 1 + stubs/ReflectionObjectWithLazyObjects.stub | 1 + stubs/iterable.stub | 4 ++++ .../SkipCheckGenericClassesExtensionTest.php | 5 +++++ 4 files changed, 11 insertions(+) diff --git a/stubs/ReflectionObject.stub b/stubs/ReflectionObject.stub index 6f779116406..6bed64cc204 100644 --- a/stubs/ReflectionObject.stub +++ b/stubs/ReflectionObject.stub @@ -4,6 +4,7 @@ * @template-covariant T of object * @extends ReflectionClass */ +#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] class ReflectionObject extends ReflectionClass { diff --git a/stubs/ReflectionObjectWithLazyObjects.stub b/stubs/ReflectionObjectWithLazyObjects.stub index 8979b9142c2..124d849ffa9 100644 --- a/stubs/ReflectionObjectWithLazyObjects.stub +++ b/stubs/ReflectionObjectWithLazyObjects.stub @@ -4,6 +4,7 @@ * @template T of object * @extends ReflectionClass */ +#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] class ReflectionObject extends ReflectionClass { diff --git a/stubs/iterable.stub b/stubs/iterable.stub index b2982ba1522..ee806c9e2bf 100644 --- a/stubs/iterable.stub +++ b/stubs/iterable.stub @@ -464,6 +464,7 @@ class RegexIterator extends FilterIterator { * @template-extends FilterIterator * @template-implements RecursiveIterator */ +#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] abstract class RecursiveFilterIterator extends FilterIterator implements RecursiveIterator { /** * @param TIterator $iterator @@ -488,6 +489,7 @@ abstract class RecursiveFilterIterator extends FilterIterator implements Recursi * * @template-extends RecursiveFilterIterator */ +#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] class ParentIterator extends RecursiveFilterIterator { } @@ -499,6 +501,7 @@ class ParentIterator extends RecursiveFilterIterator { * @template-extends CachingIterator * @template-implements RecursiveIterator */ +#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] class RecursiveCachingIterator extends CachingIterator implements RecursiveIterator { /** * @param TIterator $iterator @@ -525,6 +528,7 @@ class RecursiveCachingIterator extends CachingIterator implements RecursiveItera * @template-extends RegexIterator * @template-implements RecursiveIterator */ +#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] class RecursiveRegexIterator extends RegexIterator implements RecursiveIterator { /** * @param TIterator $iterator diff --git a/tests/PHPStan/DependencyInjection/SkipCheckGenericClassesExtensionTest.php b/tests/PHPStan/DependencyInjection/SkipCheckGenericClassesExtensionTest.php index 11ba33b40ef..42a296ef7c4 100644 --- a/tests/PHPStan/DependencyInjection/SkipCheckGenericClassesExtensionTest.php +++ b/tests/PHPStan/DependencyInjection/SkipCheckGenericClassesExtensionTest.php @@ -12,6 +12,11 @@ public function testClassesMarkedInStubsAreSkipped(): void $featureToggles = self::getContainer()->getParameter('featureToggles'); $this->assertSame([ 'DOMNamedNodeMap', + 'ParentIterator', + 'RecursiveCachingIterator', + 'RecursiveFilterIterator', + 'RecursiveRegexIterator', + 'ReflectionObject', ], $featureToggles['skipCheckGenericClasses']); } From 57f6c3d8feaac9dbef0a471256a1b50832b6c9f2 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 31 Jul 2026 17:50:39 +0000 Subject: [PATCH 4/5] Document what composer-attribute-collector needs to collect stub attributes Names the two things missing in the collector - the class map only knows .php and .inc files, and attributes are read through runtime reflection on PHP >= 8, which for an internal class such as ReflectionObject describes the real class instead of the stub - so that this extension can be replaced with a call to Attributes::findTargetClasses() once a release supporting both is out. --- .../SkipCheckGenericClassesExtension.php | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/DependencyInjection/SkipCheckGenericClassesExtension.php b/src/DependencyInjection/SkipCheckGenericClassesExtension.php index 267f8c661e2..27a99560f06 100644 --- a/src/DependencyInjection/SkipCheckGenericClassesExtension.php +++ b/src/DependencyInjection/SkipCheckGenericClassesExtension.php @@ -27,11 +27,18 @@ * generic does not start requiring type arguments before the next major version. * * The classes are not collected by composer-attribute-collector like the rest of PHPStan's - * attributes are: the classes live in .stub files, which are neither part of the class map nor - * loadable, and the collector reads the attributes of a class through runtime reflection - which - * for an internal class such as ReflectionObject reports the attributes of the real PHP class, - * never the ones written in a stub. The stub files are read here instead; parsing is limited to - * the files that mention the attribute at all. + * attributes are, because the classes live in .stub files. Two things are missing there: + * + * 1) the collector scans the included paths for .php and .inc files only, so a .stub file never + * enters the class map, + * 2) on PHP >= 8 it reads the attributes of a class through runtime reflection, which for an + * internal class such as ReflectionObject reports the attributes of the real PHP class, never + * the ones written in a stub. Its AST-based collector, used on PHP 7.4, reports them correctly. + * + * Until a release of the collector supporting both, the stub files are read here instead; parsing + * is limited to the files that mention the attribute at all. Once it does support them, this class + * becomes a call to Attributes::findTargetClasses() - the container cache key already accounts for + * vendor/attributes.php, which is what addDependencies() below achieves for the stub files. */ #[ContainerExtension(name: 'skipCheckGenericClasses')] final class SkipCheckGenericClassesExtension extends CompilerExtension From 089ed2d82a4739789101e058e10d4274bc101c24 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 31 Jul 2026 18:04:20 +0000 Subject: [PATCH 5/5] List the classes made generic here in skipCheckGenericClasses Reverts the #[RequireGenericsInBleedingEdgeOnly] attribute and the container extension reading it out of the stub files, as requested in review. The five classes that became generic in this PR are named in conf/config.neon next to DOMNamedNodeMap instead, so missingType.generics for them is only reported with bleeding edge enabled. Co-Authored-By: Claude Opus 5 --- conf/config.neon | 8 +- .../SkipCheckGenericClassesExtension.php | 133 ------------------ .../RequireGenericsInBleedingEdgeOnly.php | 23 --- stubs/ReflectionObject.stub | 1 - stubs/ReflectionObjectWithLazyObjects.stub | 1 - stubs/dom.stub | 1 - stubs/iterable.stub | 4 - ...enericClassesBleedingEdgeExtensionTest.php | 26 ---- .../SkipCheckGenericClassesExtensionTest.php | 23 --- 9 files changed, 7 insertions(+), 213 deletions(-) delete mode 100644 src/DependencyInjection/SkipCheckGenericClassesExtension.php delete mode 100644 src/PhpDoc/RequireGenericsInBleedingEdgeOnly.php delete mode 100644 tests/PHPStan/DependencyInjection/SkipCheckGenericClassesBleedingEdgeExtensionTest.php delete mode 100644 tests/PHPStan/DependencyInjection/SkipCheckGenericClassesExtensionTest.php diff --git a/conf/config.neon b/conf/config.neon index b9f2e9813aa..8fc949c9361 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -28,7 +28,13 @@ parameters: bleedingEdge: false checkNonStringableDynamicAccess: false checkParameterCastableToNumberFunctions: false - skipCheckGenericClasses: [] + skipCheckGenericClasses: + - DOMNamedNodeMap + - ParentIterator + - RecursiveCachingIterator + - RecursiveFilterIterator + - RecursiveRegexIterator + - ReflectionObject stricterFunctionMap: false reportPreciseLineForUnusedFunctionParameter: false checkPrintfParameterTypes: false diff --git a/src/DependencyInjection/SkipCheckGenericClassesExtension.php b/src/DependencyInjection/SkipCheckGenericClassesExtension.php deleted file mode 100644 index 27a99560f06..00000000000 --- a/src/DependencyInjection/SkipCheckGenericClassesExtension.php +++ /dev/null @@ -1,133 +0,0 @@ -= 8 it reads the attributes of a class through runtime reflection, which for an - * internal class such as ReflectionObject reports the attributes of the real PHP class, never - * the ones written in a stub. Its AST-based collector, used on PHP 7.4, reports them correctly. - * - * Until a release of the collector supporting both, the stub files are read here instead; parsing - * is limited to the files that mention the attribute at all. Once it does support them, this class - * becomes a call to Attributes::findTargetClasses() - the container cache key already accounts for - * vendor/attributes.php, which is what addDependencies() below achieves for the stub files. - */ -#[ContainerExtension(name: 'skipCheckGenericClasses')] -final class SkipCheckGenericClassesExtension extends CompilerExtension -{ - - #[Override] - public function loadConfiguration(): void - { - $stubFiles = self::findStubFiles(); - - // the compiled container holds the result of reading the stub files, - // so it has to be rebuilt when any of them changes - $this->compiler->addDependencies($stubFiles); - - $builder = $this->getContainerBuilder(); - if ((bool) $builder->parameters['featureToggles']['bleedingEdge']) { - return; - } - - $builder->parameters['featureToggles']['skipCheckGenericClasses'] = array_values(array_unique(array_merge( - $builder->parameters['featureToggles']['skipCheckGenericClasses'], - self::findClasses($stubFiles), - ))); - } - - /** - * @param list $stubFiles - * @return list - */ - private static function findClasses(array $stubFiles): array - { - $parser = (new ParserFactory())->createForNewestSupportedVersion(); - $nodeFinder = new NodeFinder(); - $classes = []; - - foreach ($stubFiles as $stubFile) { - $contents = file_get_contents($stubFile); - if ($contents === false) { - throw new ShouldNotHappenException(sprintf('Could not read stub file %s.', $stubFile)); - } - - if (!str_contains($contents, RequireGenericsInBleedingEdgeOnly::class)) { - continue; - } - - $traverser = new NodeTraverser(new NameResolver()); - $nodes = $traverser->traverse($parser->parse($contents) ?? []); - - foreach ($nodeFinder->findInstanceOf($nodes, ClassLike::class) as $classLike) { - $className = $classLike->namespacedName; - if ($className === null) { - continue; - } - - foreach ($classLike->attrGroups as $attrGroup) { - foreach ($attrGroup->attrs as $attr) { - if ($attr->name->toString() !== RequireGenericsInBleedingEdgeOnly::class) { - continue; - } - - $classes[] = $className->toString(); - } - } - } - } - - sort($classes); - - return $classes; - } - - /** - * @return list - */ - private static function findStubFiles(): array - { - $files = []; - $directoryIterator = new RecursiveDirectoryIterator(__DIR__ . '/../../stubs', RecursiveDirectoryIterator::SKIP_DOTS); - foreach (new RecursiveIteratorIterator($directoryIterator) as $file) { - if ($file->getExtension() !== 'stub') { - continue; - } - - $files[] = $file->getPathname(); - } - - sort($files); - - return $files; - } - -} diff --git a/src/PhpDoc/RequireGenericsInBleedingEdgeOnly.php b/src/PhpDoc/RequireGenericsInBleedingEdgeOnly.php deleted file mode 100644 index b18a3cd5188..00000000000 --- a/src/PhpDoc/RequireGenericsInBleedingEdgeOnly.php +++ /dev/null @@ -1,23 +0,0 @@ - */ -#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] class ReflectionObject extends ReflectionClass { diff --git a/stubs/ReflectionObjectWithLazyObjects.stub b/stubs/ReflectionObjectWithLazyObjects.stub index 124d849ffa9..8979b9142c2 100644 --- a/stubs/ReflectionObjectWithLazyObjects.stub +++ b/stubs/ReflectionObjectWithLazyObjects.stub @@ -4,7 +4,6 @@ * @template T of object * @extends ReflectionClass */ -#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] class ReflectionObject extends ReflectionClass { diff --git a/stubs/dom.stub b/stubs/dom.stub index fbabcd5f2e8..4a4b41a1fe8 100644 --- a/stubs/dom.stub +++ b/stubs/dom.stub @@ -126,7 +126,6 @@ class DOMProcessingInstruction * * @property-read int $length */ -#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] class DOMNamedNodeMap implements IteratorAggregate, Countable { /** diff --git a/stubs/iterable.stub b/stubs/iterable.stub index ee806c9e2bf..b2982ba1522 100644 --- a/stubs/iterable.stub +++ b/stubs/iterable.stub @@ -464,7 +464,6 @@ class RegexIterator extends FilterIterator { * @template-extends FilterIterator * @template-implements RecursiveIterator */ -#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] abstract class RecursiveFilterIterator extends FilterIterator implements RecursiveIterator { /** * @param TIterator $iterator @@ -489,7 +488,6 @@ abstract class RecursiveFilterIterator extends FilterIterator implements Recursi * * @template-extends RecursiveFilterIterator */ -#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] class ParentIterator extends RecursiveFilterIterator { } @@ -501,7 +499,6 @@ class ParentIterator extends RecursiveFilterIterator { * @template-extends CachingIterator * @template-implements RecursiveIterator */ -#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] class RecursiveCachingIterator extends CachingIterator implements RecursiveIterator { /** * @param TIterator $iterator @@ -528,7 +525,6 @@ class RecursiveCachingIterator extends CachingIterator implements RecursiveItera * @template-extends RegexIterator * @template-implements RecursiveIterator */ -#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] class RecursiveRegexIterator extends RegexIterator implements RecursiveIterator { /** * @param TIterator $iterator diff --git a/tests/PHPStan/DependencyInjection/SkipCheckGenericClassesBleedingEdgeExtensionTest.php b/tests/PHPStan/DependencyInjection/SkipCheckGenericClassesBleedingEdgeExtensionTest.php deleted file mode 100644 index 13c4fba7976..00000000000 --- a/tests/PHPStan/DependencyInjection/SkipCheckGenericClassesBleedingEdgeExtensionTest.php +++ /dev/null @@ -1,26 +0,0 @@ -getParameter('featureToggles'); - $this->assertSame([], $featureToggles['skipCheckGenericClasses']); - } - - /** - * @return string[] - */ - public static function getAdditionalConfigFiles(): array - { - return [ - __DIR__ . '/../../../conf/bleedingEdge.neon', - ]; - } - -} diff --git a/tests/PHPStan/DependencyInjection/SkipCheckGenericClassesExtensionTest.php b/tests/PHPStan/DependencyInjection/SkipCheckGenericClassesExtensionTest.php deleted file mode 100644 index 42a296ef7c4..00000000000 --- a/tests/PHPStan/DependencyInjection/SkipCheckGenericClassesExtensionTest.php +++ /dev/null @@ -1,23 +0,0 @@ -getParameter('featureToggles'); - $this->assertSame([ - 'DOMNamedNodeMap', - 'ParentIterator', - 'RecursiveCachingIterator', - 'RecursiveFilterIterator', - 'RecursiveRegexIterator', - 'ReflectionObject', - ], $featureToggles['skipCheckGenericClasses']); - } - -}