Repository navigation
[6.x] Throw TypeParseTreeException for an incomplete conditional type - #11962
Conversation
…if-this-is types The self_out and if_this_is blocks in FunctionLikeDocblockScanner were the only TypeParser::parseTokens() call sites in that file without a surrounding catch (TypeParseTreeException), so an unparseable type there escaped as an uncaught throwable during the scan phase, killing the whole run. Fixes vimeo#11958
TypeParser::getTypeFromTree() fell through to the 'Unrecognised parse tree type' InvalidArgumentException when a parenthesised `is` expression was not completed into a ternary, e.g. `($key is null)`. None of the docblock parse sites catch InvalidArgumentException, so such a type aborted the whole run with an uncaught throwable - including through the already-guarded @return and @param-out paths. TemplateIsTree is now rejected with a TypeParseTreeException, so every caller degrades to InvalidDocblock. Handling it in getTypeFromTree() rather than ParseTreeCreator covers nested occurrences such as `list<($key is null)>` through the existing recursion.
TypeParseTreeException for an incomplete conditional type
@self-out / @if-this-is types
#11960
`(T is ? int : string)` crashed with an uncaught "Undefined array key 0" instead of a clean parse error. The `?` after a childless TemplateIsTree promotes it into a ConditionalTree, so the existing top-level TemplateIsTree guard never runs, and getTypeFromTree read condition->children[0] unchecked. Drops the baseline entry the guard makes redundant. Refs vimeo#11958
|
Pushed a second commit, 94eca63, that closes a sibling hole Copilot spotted on #11964 (#11964 (review)). It is pre-existing on /** @return ($key is ? int : string) */crashes the scan with |
There was a problem hiding this comment.
🔵 Needs a closer look
Add regression coverage for nested incomplete conditional types to verify recursive handling.
Pull request overview
Fixes incomplete conditional-type parsing so invalid docblocks report InvalidDocblock instead of aborting analysis.
Changes:
- Rejects dangling
TemplateIsTreenodes withTypeParseTreeException. - Adds parser and docblock regression coverage.
- Removes the obsolete baseline suppression.
File summaries
| File | Description |
|---|---|
tests/TypeParseTest.php |
Tests malformed conditional types. |
tests/ThisOutTest.php |
Tests incomplete this-out annotations. |
tests/Template/ConditionalReturnTypeTest.php |
Tests invalid conditional return types. |
tests/IfThisIsTest.php |
Tests invalid if-this-is annotations. |
src/Psalm/Internal/Type/TypeParser.php |
Handles incomplete conditional parse trees. |
src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php |
Converts parse failures into invalid-docblock reports. |
psalm-baseline.xml |
Removes the resolved baseline entry. |
Review details
Suppressed comments (1)
src/Psalm/Internal/Type/TypeParser.php:408
- The new branch is specifically intended to make incomplete
isexpressions fail correctly when they are nested under another tree, but the added unit coverage only exercises a root-levelTemplateIsTree. Please add a regression case such aslist<(T is string)>(and ideally the affected docblock tags) so a future change to the recursive dispatch cannot reintroduce the uncaughtInvalidArgumentExceptionfor nested occurrences.
if ($parse_tree instanceof TemplateIsTree) {
throw new TypeParseTreeException('Invalid conditional, expected ? after is');
}
- Files reviewed: 7/7 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Note
Stacked on #11960. The base here is
6.xbecause a fork branch cannot be used as a PR base, so the diff currently shows both commits. Only the second one —Throw TypeParseTreeException for an incomplete conditional type— belongs to this PR. I'll rebase once #11960 lands. Happy to fold it into #11960 instead if you prefer a single PR.Problem
Follow-up to the gap I noted in #11960 (comment).
TypeParser::getTypeFromTree()handlesConditionalTreebut has no branch forTemplateIsTree, the nodeParseTreeCreatorbuilds for anistoken before a?upgrades it into aConditionalTree. Anisexpression that never gets its ternary therefore falls through to the invariant guard at the bottom:None of the docblock parse sites catch
InvalidArgumentException, so this killed the entire run:This was not specific to
@self-out—@return,@param-outand@psalm-if-this-isall died on the same input, even though all three already have acatch (TypeParseTreeException).Change
Reject
TemplateIsTreeingetTypeFromTree()with aTypeParseTreeException, so every one of the eight docblock call sites degrades toInvalidDocblockas intended:Handling it in
getTypeFromTree()rather than inParseTreeCreator::create()is deliberate:getTypeFromTree()already recurses, so nested occurrences such aslist<($key is null)>are covered for free, whereas a tail check increate()would only see the root node and would need a full tree walk.The
InvalidArgumentExceptionat the bottom ofgetTypeFromTree()is left alone. I probed the otherParseTreesubclasses that reach it (Root,FieldEllipsis,CallableParamTree,MethodParamTree,KeyedArrayPropertyTree) with malformed docblocks —array{a: int, ...},list<...>,callable(int...),callable(int):,array{a:},Foo::bar(),int...,...— and none of them are reachable from user input; they are either consumed by their parent node or rejected earlier with aTypeParseTreeException.TemplateIsTreewas the only real hole, so the guard stays as a genuine internal invariant.