Add TypeSpecifierContext::getConditionType() and move count/strlen narrowing to extensions#5781
Closed
ondrejmirtes wants to merge 6 commits into
Closed
Add TypeSpecifierContext::getConditionType() and move count/strlen narrowing to extensions#5781ondrejmirtes wants to merge 6 commits into
ondrejmirtes wants to merge 6 commits into
Conversation
…en narrowing to extensions Type-specifying extensions could only see the truthy/falsey/true/false bucket of the evaluated condition. They now also get getNarrowedReturnType(): ?Type, the constraint on the analyzed function's return value for the branch being evaluated (e.g. IntegerRangeType(2, max) for `count($x) >= 2`). The bitmask is kept untouched; the narrowed return type is an additive, one-directional hint that negate() drops. As a proof of concept, the hardcoded count()/sizeof() and strlen()/mb_strlen() comparison narrowing moves out of TypeSpecifier into their extensions: - the count array-size narrowing moves into the new CountFuncCallTypeSpecifier service, used by CountFunctionTypeSpecifyingExtension and TypeSpecifier; - TypeSpecifier keeps only the generic size-constraint computation (resolveResultSizeType) and dispatches via withConditionType(); - StrlenFunctionTypeSpecifyingExtension derives non-empty-string/non-falsy-string from the narrowed return type. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tension TypeSpecifier dispatches a function call compared against a value to its type-specifying extensions, passing the compared type as the narrowed return type (TypeSpecifierContext:: getNarrowedReturnType()). Only extensions that require the narrowed return type (they would not support the bare call) take part, so plain truthy/falsey extensions are untouched - no marker interface needed. get_class()/get_debug_type() === class-string narrowing now lives in GetClassFunctionTypeSpecifyingExtension.
gettype($a) === 'string' (and the other gettype names) narrowing now lives in GettypeFunctionTypeSpecifyingExtension, dispatched via the narrowed return type from specifyTypesForConstantStringBinaryExpression instead of being hardcoded in TypeSpecifier.
get_parent_class($a) === Foo::class narrowing now lives in GetParentClassFunctionTypeSpecifyingExtension, dispatched via the narrowed return type instead of being hardcoded in TypeSpecifier.
trim($s) !== '' (and ltrim/rtrim/chop/mb_*) non-empty-string narrowing now lives in TrimFunctionTypeSpecifyingExtension, dispatched via the narrowed return type instead of being hardcoded in TypeSpecifier.
…rowing to an extension array_key_first($a) !== null etc. non-empty-array narrowing now lives in ArrayKeyFirstLastFunctionTypeSpecifyingExtension, dispatched via the narrowed return type instead of being hardcoded in TypeSpecifier.
6905f0c to
ca566c3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Type-specifying extensions (
FunctionTypeSpecifyingExtensionetc.) could only learn the boolean shape of the evaluated condition viaTypeSpecifierContext—true()/truthy()/false()/falsey()/null(). They could not react to a more specific numeric condition likecount($x) >= 2, so that richer narrowing had to live hardcoded insideTypeSpecifier.What this does
New
TypeSpecifierContext::getConditionType(): ?Type(+withConditionType())Reports the constraint on the analyzed function's return value for the branch being evaluated — e.g. for
count($x) >= 2thecount()extension now receivesIntegerRangeType(2, max).?intbitmask is untouched; a parallel?Type $conditionTypefield is added.true()/truthy()/false()/falsey()/null()/negate()and the@apiCONTEXT_*constants behave identically.(bitmask, type)pair handed to an extension;negate()drops it (so extensions that negate fall back to bitmask-only behavior).Proof of concept: move hardcoded narrowing out of
TypeSpecifier#[AutowiredService]CountFuncCallTypeSpecifier, used byCountFunctionTypeSpecifyingExtension(driven bygetConditionType()) and byTypeSpecifierfor the residual sibling-offset inference andcount($a) === Ncases. Internals are not exposed aspublic @internalonTypeSpecifier.StrlenFunctionTypeSpecifyingExtension, driven bygetConditionType().TypeSpecifierkeeps only the generic "compute the integer size constraint and dispatch" logic (resolveResultSizeType()+withConditionType()). Net ~210 lines removed fromTypeSpecifier.preg_match — intentionally not migrated
preg_matchhas no hardcoded type specification inTypeSpecifier— only acomparison → === 1normalization rewrite; its$matchesnarrowing already lives entirely in its extension. Routing it throughgetConditionType()changed the$matchesshape in the not-matched/error branches (becausepreg_matchreturnsint|false), so it was left as-is. Can be revisited as a follow-up.Verification
NodeScopeResolverTest(all nsrt assertType files),TypeSpecifierTest,TypeSpecifierContextTest(with new tests for the new methods).make phpstan(self-analysis) andmake cs: clean.assertTypeexpectation churn — the count/strlen migrations are behavior-preserving refactors.🤖 Generated with Claude Code