Make ReflectionObject, RecursiveFilterIterator, ParentIterator, RecursiveCachingIterator and RecursiveRegexIterator generic in stubs - #6165
Conversation
… `RecursiveCachingIterator` and `RecursiveRegexIterator` generic in stubs * Add `stubs/ReflectionObject.stub` and `stubs/ReflectionObjectWithLazyObjects.stub` declaring `ReflectionObject` as `@extends ReflectionClass<T>` 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<T>` 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<mixed,mixed,RecursiveIterator<mixed, mixed>>` instead of the erased `Traversable<mixed, mixed>` bound.
ondrejmirtes
left a comment
There was a problem hiding this comment.
When a built-in PHP class becomes generic, it needs to be added to skipCheckGenericClasses. So that missingType.generics for this class is only reported with bleeding edge on.
Feel free to:
- Introduce a new attribute
#[\PHPStan\PhpDoc\RequireGenericsInBleedingEdgeOnly] - Configure composer attribute collector to scan
stubs/too - Add a new
ContainerExtensionthat will look for these attributes in stubs and append the skipCheckGenericClasses attribute - Remove DOMNamedNodeMap from skipCheckGenericClasses and add the new attribute above it
- Mark all the classes made generic in this PR with the same attribute
…] 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 <noreply@anthropic.com>
…geOnly] 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 <noreply@anthropic.com>
|
Done — pushed as two commits. What changed1.
2. Marked this PR's newly generic classes ( Deviation from step 2/3 of the reviewI did not configure composer-attribute-collector to scan
So the extension reads the stub files directly instead. It's cheap: it only parses files whose contents contain the attribute name (currently 3 of 40), and only at container-compile time. If you'd rather have this in the collector, it needs a release of Verification
|
ondrejmirtes
left a comment
There was a problem hiding this comment.
I really would like to have this be statically found out by composer attribute collector. It doesn't use runtime reflection, it uses nikic/PHP-Parser so it should be possible. If the problem is the .stub extension, send a PR to https://github.com/ondrejmirtes/simple-downgrader/ that makes it configurable.
…ibutes 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.
|
All background pollers are cleared; the working tree is clean and the branch is pushed at Final state:
|
ondrejmirtes
left a comment
There was a problem hiding this comment.
Alright never mind, remove the atgribute machinery and just leave the stubs and make sure the skipping is in the config
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 <noreply@anthropic.com>
|
Done — pushed as What changedThe attribute machinery is gone; the PR is back to stubs + a config entry. Removed:
Config ( skipCheckGenericClasses:
- DOMNamedNodeMap
- ParentIterator
- RecursiveCachingIterator
- RecursiveFilterIterator
- RecursiveRegexIterator
- ReflectionObjectThe net diff versus Verification of the skippingAnalysed a file taking each of the six classes as a bare parameter type, plus a class extending
|
Summary
ReflectionObjecthad no stub of its own, so PHPStan saw it as a plain, non-generic subclass of the genericReflectionClass<T>. The template argument was therefore erased to its bound, and every inherited method that returnsT(newInstance(),newInstanceArgs(),newInstanceWithoutConstructor(),getName(), the$nameproperty, …) degraded toobject/class-string<object>— producing the reported false positive:The fix adds a generic
ReflectionObjectstub, plus the same treatment for the SPL recursive iterators that sit on the identical axis.Changes
stubs/ReflectionObject.stub—@template-covariant T of object,@extends ReflectionClass<T>, and a redeclared constructor with@param T $argumentsonew ReflectionObject($object)infersTfrom the argument.stubs/ReflectionObjectWithLazyObjects.stub— the same declaration with an invariant@template T of object, matching the invariantReflectionClassWithLazyObjects.stubused on PHP >= 8.4 (the lazy-object methods takeTin parameter position). This mirrors the existingReflectionEnum.stub/ReflectionEnumWithLazyObjects.stubsplit.src/PhpDoc/ReflectionClassStubFilesExtension.php— returns the matchingReflectionObjectstub alongside the matchingReflectionClassstub in both branches.stubs/iterable.stub— added generic declarations for the recursive iterators that were missing them:RecursiveFilterIterator→@extends FilterIterator<TKey, TValue, TIterator>,@implements RecursiveIterator<TKey, TValue>ParentIterator→@extends RecursiveFilterIterator<TKey, TValue, TIterator>RecursiveCachingIterator→@extends CachingIterator<TKey, TValue, TIterator>,@implements RecursiveIterator<TKey, TValue>RecursiveRegexIterator→@extends RegexIterator<TKey, TValue, TIterator>,@implements RecursiveIterator<TKey, TValue>tests/PHPStan/Rules/Methods/OverridingMethodRuleTest.php— expectation fortestBug9615updated to the now-more-precise declaring classFilterIterator<mixed,mixed,RecursiveIterator<mixed, mixed>>.Probed and found already correct (no change needed):
ReflectionEnum— already declares@template-covariant T of UnitEnum/@extends ReflectionClass<T>in both stub variants.ReflectionClass::getAttributes()— already returnslist<ReflectionAttribute<Foo>>when givenFoo::class, via a dynamic return type extension.SplStack,SplQueue,SplMinHeap,SplMaxHeap,RecursiveArrayIterator,RecursiveCallbackFilterIterator— already generic viastubs/spl.stub,stubs/iterable.stubor phpstorm-stubs.RecursiveTreeIterator,DirectoryIterator,FilesystemIterator,SplFileObject,SplTempFileObject,GlobIterator— non-generic on purpose; they implement their iterator interfaces with concrete type arguments.Root cause
The pattern is: a PHP built-in class extends a generic stubbed class but is not itself declared generic with an
@extendstag, so PHPStan resolves the ancestor with the template types erased to their bounds.To enumerate every instance, all internal classes and interfaces were reflected and fed through PHPStan's
missingType.genericscheck to build the set of classes PHPStan considers generic; every non-generic internal class with a generic ancestor was then inspected. Affected locations:ReflectionObject extends ReflectionClass<T>—Terased toobject(the reported bug).RecursiveFilterIterator extends FilterIterator<TKey, TValue, TIterator>—TKey/TValueerased tomixed.ParentIterator extends RecursiveFilterIterator— same, transitively.RecursiveCachingIterator extends CachingIterator<TKey, TValue, TIterator>— same.RecursiveRegexIterator extends RegexIterator<TKey, TValue, TIterator>— same.In each case the fix is the same: declare the subclass generic in the stub and thread the type parameters through
@extends(and@implements RecursiveIterator<TKey, TValue>where the subclass adds recursion), redeclaring the constructor's PHPDoc where it takes a value the type parameters can be inferred from.Test
tests/PHPStan/Analyser/nsrt/bug-15032.php— the reproducer from the issue's playground link. AssertsReflectionObject<T of object>fornew ReflectionObject($object)inside a@template T of objectfunction andTfornewInstance(), plus a concrete-class variant assertingReflectionObject<Exception>,class-string<Exception>forgetName()/$name, andExceptionfornewInstance(),newInstanceArgs()andnewInstanceWithoutConstructor(). Fails before the fix withobject/class-string<object>/ReflectionObject.tests/PHPStan/Analyser/nsrt/recursive-iterators.php— covers the analogous cases:foreachkey/value types,getChildren(),getInnerIterator()andcurrent()/key()onParentIterator,RecursiveCachingIteratorandRecursiveRegexIterator, including template inference from the constructor argument (new RecursiveCachingIterator($recursiveArrayIterator)→RecursiveCachingIterator<int, string, RecursiveArrayIterator<int, string>>). Fails before the fix withmixedand bare class names.Both stub variants were verified by running the reproducer with
phpVersionpinned to 7.4, 8.0 and 8.4, covering the non-lazy and lazy-objectsReflectionClassstubs.make testsandmake phpstanare green.Fixes phpstan/phpstan#15032