Conversation
|
You've opened the pull request against the latest branch 2.2.x. PHPStan 2.2 is not going to be released for months. If your code is relevant on 2.1.x and you want it to be released sooner, please rebase your pull request and change its target to 2.1.x. |
3dad6af to
8fca348
Compare
…plateType behavior
…guard in getTypeAliasName
…ks templateTypes)
…eon (affects all PHP versions)
8fca348 to
7f5b84d
Compare
|
@ondrejmirtes I know you have a lot going on, but I spent the last few weeks going around in circles trying to plug generics more deeply into my codebase, before finally realizing that the problem was that they weren't really fully-implemented. This PR should get you most of the way there. Wanted to note that a change like this will require a few small changes to the phpdoc parser; I didn't want to start on that PR without getting some buy-in on this one first, so for now I accomplished it with patch files. Anyway, please let me know what you think, and if I should proceed with the parser changes. |
|
I wouldn't hijack template types for this. First I'd try late-resolvable types. Look at LateResolvableType and its implementations. They all share |
Updated. Here's the draft for the necessary changes to the phpdoc parser: phpstan/phpdoc-parser#297 |
Fix for phpstan/phpstan#6099 phpstan/phpstan#12730
Generic
@phpstan-typeAliasesSummary
This PR adds support for parameterised (generic)
@phpstan-typealiases — local type aliases that accept template type arguments, much like how@templateworks on classes and functions today.A generic alias lets you write a reusable structural type once and instantiate it with different concrete types at each use site, without repeating the full shape definition everywhere.
Motivation
PHPStan already supports template types on classes (
@template T) and on individual functions/methods. But@phpstan-typealiases have always been monomorphic — every use site gets the exact same concrete type. That makes it impossible to express patterns like:You can't make
itemswork generically; you end up duplicating the alias for every element type, or reaching for a full class +@template.This PR closes that gap.
Syntax
Template parameters on an alias follow the same
@templatesyntax you already know:Each parameter may optionally carry an upper-bound constraint (
of …) and/or a default type (= …), matching PHPStan's existing@templatesemantics.Usage
Once defined, a generic alias is used the same way generic classes are — with angle-bracket arguments:
Type arguments are substituted into the alias body, so the full type flows through the analyser as if you had written it inline.
Imports
Generic aliases can be imported via
@phpstan-import-typeand then used with type arguments at the import site, just like locally-defined aliases:Error detection
Three new categories of error are reported, all on the existing missing-typehint and incompatible-phpdoc rule levels:
1. Bare usage of a generic alias (missing required type arguments)
When a generic alias that has at least one required parameter (no default) is used without any type arguments, PHPStan reports a missing-typehint error — analogous to using
arrayinstead ofarray<int, string>.Aliases whose every parameter has a default may be used bare without error:
2. Too many type arguments
3. Too few required type arguments
Implementation overview
TypeAliasTag— template parameter storageTypeAliasTagnow stores an array ofTemplateTagValueNode[]alongside the existingTypeNodeandNameScope.PhpDocNodeResolver::resolveTypeAliasTagsforwards thetemplateTypesfield from the phpdoc-parser AST node.TypeAlias— resolution engineTypeAliasgains three resolution strategies on top of the existingresolve():resolve()TemplateTypeplaceholders intact (used internally as the base for both strategies below)resolveWithArgs(TypeNodeResolver, Type[])resolveGenericTypeNodewhen the alias is written asAlias<T1, T2>resolveWithDefaults(TypeNodeResolver)TemplateTypeplaceholders — called when the alias is written bare (Alias) so that error detection can find the unresolved placeholdersTemplate type objects are created with a dedicated
TemplateTypeScope::createWithTypeAlias(className, aliasName), giving them a distinct function name (__typeAlias_<aliasName>) so thatMissingTypehintCheckcan identify them as belonging to an alias rather than a class or function template.TypeNodeResolver— dispatchresolveGenericTypeNode(Alias<T1, T2>syntax): before falling through to the standard class-based generic resolution, it calls the newfindGenericTypeAlias()helper. If the alias is found it validates the argument count and callsresolveWithArgs. Wrong counts produceErrorType(which is what causes the incompatible-phpdoc error fromIncompatiblePhpDocTypeRule).resolveIdentifierTypeNode(bareAliassyntax): intercepts before delegating toUsefulTypeAliasResolver— if the identifier names a generic alias, it callsresolveWithDefaultsso that required-parameter placeholders survive into the type system forMissingTypehintCheckto detect.MissingTypehintCheck::getRawGenericTypeAliasesUsage()New traversal method. Walks a
Typetree looking forTemplateTypeinstances whoseTemplateTypeScopewas created for a type alias (identified by the__typeAlias_prefix). Any such placeholder that has no default is evidence of a bare required parameter → reported as a missing-typehint.Rule-level integration
getRawGenericTypeAliasesUsageis wired into all the places that already callgetMissingTypesorcheckMissingTypehints:MissingMethodParameterTypehintRuleMissingMethodReturnTypehintRuleMissingFunctionParameterTypehintRule/MissingFunctionReturnTypehintRuleMissingPropertyTypehintRuleMissingClassConstantTypehintRuleMissingMethodSelfOutTypeRuleLocalTypeAliasesCheck(with a self-skip guard so an alias's own body params are not reported)AssertRuleHelper,InvalidPhpDocVarTagTypeRule,SetPropertyHookParameterRule,PropertyTagCheck,MethodTagCheck,MixinCheckBug fix:
NameScope-poisoning regressionAdding the
findGenericTypeAliascall inresolveIdentifierTypeNodeexposed a subtle re-entrancy bug. WhenFileTypeMapper::getNameScopeis building a class's NameScope (specifically, resolving the bound of a@phpstan-templatetag),findGenericTypeAliaswas callingClassReflection::getTypeAliases(). This in turn calledClassReflection::getResolvedPhpDoc()→fileTypeMapper->getResolvedPhpDoc()→getNameScope()for the same class — which was still in$inProcess. TheNameScopeAlreadyBeingCreatedExceptionwas caught, an emptyResolvedPhpDocBlockwas cached, andClassReflection::$typeAliaseswas permanently set to[], wiping out all the class's type aliases.The fix mirrors the guard already present in
UsefulTypeAliasResolver::resolveLocalTypeAlias: call$nameScope->hasTypeAlias($name)first; if the name is not registered as a type alias in the current scope, returnnullimmediately — no reflection lookup needed. This preventsfindGenericTypeAliasfrom ever triggering the re-entrant path for identifiers that aren't type aliases (e.g. template bounds likeGotoTarget).Tests
tests/PHPStan/Analyser/nsrt/generic-type-aliases.phpassertTypechecks for single-param, multi-param, defaulted, and imported generic aliasestests/PHPStan/Rules/Methods/data/generic-type-alias-missing-typehint.phptests/PHPStan/Rules/PhpDoc/data/generic-type-alias-wrong-arg-count.phptests/PHPStan/Rules/PhpDoc/data/bug-11033.phpGotoRouteDataTypes[value-of<T>]on a class that has both@phpstan-templateand@phpstan-typealiasesFor people familiar with generics from TypeScript , here's what generic-type-alias.php would look like:
https://jsfiddle.net/qky3r70m/