You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
TypeParser::parse(Peekable $tokens): TypeNode|ParsedToken|null pushes the "was this actually a type?" decision onto every caller. The three-way return means each call site re-derives the same outcome and invents its own wording for the two failure shapes:
null → nothing was there (end of input)
ParsedToken → a token that can't start a type
TypeNode → success
There are 7 callers today, and 6 of them repeat this decode-and-throw dance with slightly different messages:
The only caller that legitimately needs "maybe there's no type here" is parseTypeList(), which abuses the non-TypeNode return purely as a loop terminator.
That awkward return type is why the type-parsing call sites keep growing near-duplicate error branches — it was the root cause behind the E2eCase finding in #58.
Proposed direction
Make parse() throw SyntaxError on a missing/invalid type (it already throws via expect()) and return TypeNode unconditionally. Give parseTypeList() an explicit terminator (peek for the closing token / CloseAngle / CloseParen) instead of relying on a non-TypeNode return. That collapses roughly ten decode branches across the parser package into one convention.
This would also let the six callers drop their bespoke null/ParsedToken handling and just use the returned node.
Caveats
TypeParser is @psalm-internal, so this is not a public BC break — but it does change error messages. e.g. list<int 42> currently reports Expected >, got 42; after the change it would report something like Expected type, got 42. The message-based assertions in TypeParserTest and ExpressionParserTest will need updating, and we should decide which wording we actually want.
Worth doing as its own PR so the message churn is reviewable in isolation.
Follow-up from code review of #58.
Problem
TypeParser::parse(Peekable $tokens): TypeNode|ParsedToken|nullpushes the "was this actually a type?" decision onto every caller. The three-way return means each call site re-derives the same outcome and invents its own wording for the two failure shapes:null→ nothing was there (end of input)ParsedToken→ a token that can't start a typeTypeNode→ successThere are 7 callers today, and 6 of them repeat this decode-and-throw dance with slightly different messages:
TypeParser::parseString()TypeParser::parseFunction()(return type)TypeParser::parseStruct()(field type)ExpressionParser— inline variable type (foo:int)ExpressionParser— lambda return-type annotationE2eCase::parseTypesvia the newparseDeclarations()(added in Make a string entry point consume its whole input #58)The only caller that legitimately needs "maybe there's no type here" is
parseTypeList(), which abuses the non-TypeNodereturn purely as a loop terminator.That awkward return type is why the type-parsing call sites keep growing near-duplicate error branches — it was the root cause behind the
E2eCasefinding in #58.Proposed direction
Make
parse()throwSyntaxErroron a missing/invalid type (it already throws viaexpect()) and returnTypeNodeunconditionally. GiveparseTypeList()an explicit terminator (peek for the closing token /CloseAngle/CloseParen) instead of relying on a non-TypeNodereturn. That collapses roughly ten decode branches across the parser package into one convention.This would also let the six callers drop their bespoke
null/ParsedTokenhandling and just use the returned node.Caveats
TypeParseris@psalm-internal, so this is not a public BC break — but it does change error messages. e.g.list<int 42>currently reportsExpected >, got 42; after the change it would report something likeExpected type, got 42. The message-based assertions inTypeParserTestandExpressionParserTestwill need updating, and we should decide which wording we actually want.Related
parseDeclarations, the 7th caller, and surfaced this)