fix(preprocessor): validate compound type declarations and class-scope type keywords - #65
Conversation
…e type keywords
resolveTypeDecl now runs a shared well-formedness pass before resolving,
so parameters, returns, properties, class/interface constants, and
closure signatures all obey Zend's compile-time compound-type rules
(each probed on 8.4.13):
- duplicate union members, case-insensitive and after alias/namespace
resolution ("Duplicate type int is redundant", "Duplicate type
App\Sub\Thing is redundant"); iterable is expanded to
array|Traversable first, so iterable|array and iterable|\Traversable
report the overlapping component exactly like Zend, while a
namespace-local Traversable stays legal
- bool with false/true names the literal as the duplicate in either
order; true|false demands bool ("Type contains both true and false,
bool must be used instead")
- mixed/void/never inside a union ("... can only be used as a
standalone type"), ?mixed ("Type mixed cannot be marked as nullable
since mixed already includes null"), ?null, ?void, ?never
- intersection members must be class types ("Type int cannot be part
of an intersection type"); duplicate intersection members are
redundant; self/parent/static keep the established TypeCheckGenerator
diagnostic; redundancy between whole DNF groups is not checked (Zend
uses a distinct "Type X&Y is redundant with type X&Y" pass)
- self/static return types on free functions ("Cannot use \"static\"
when no class scope is active"); closures keep accepting them since
they may be bound to a scope later, matching Zend
- duplicate interfaces in an implements list, for classes and enums
("Class A cannot implement previously implemented interface I");
duplicate trait use stays legal - Zend deduplicates it silently
matyhtf
left a comment
There was a problem hiding this comment.
The focused tests pass (14/14), but three Zend type rules still compile successfully:
objectabsorbs every class type:
class Foo {}
function f(object|Foo $value): void {}Zend: Type Foo|object contains both object and a class type, which is redundant. Track object versus resolved class members in both member orders.
- Whole DNF groups cannot be skipped:
interface A {}
interface B {}
function f((A&B)|(B&A) $value): void {}Zend rejects the second intersection as redundant. It also rejects (A&B)|A because the intersection is more restrictive than A. Canonicalize intersection member sets (order-insensitive) and check exact/restrictive DNF redundancy.
- Class-scope keywords are only checked for a bare return node. They also occur in parameters and nested union/DNF returns:
function f(self $value): void {}
function g(): self|stdClass {}Both must fail with Cannot use "self" when no class scope is active; this PR compiles them. Validate self/static recursively for every declaration context. static must additionally remain return-only.
Please add negative tests for these forms. The current comment explicitly saying whole-DNF redundancy is not checked documents a known PHP incompatibility, so this is not ready to merge as the compound-type validator.
Compound type declarations were not validated for well-formedness — all of these compiled, each a Zend compile fatal (probed individually): duplicate union members after alias/namespace resolution (
int|string|int,Foo|\Foo, iterable expanded soiterable|arraynames the overlapping component); bool/true/false overlaps (bool|false,true|false→ "bool must be used instead");mixed/void/neverinside unions;?mixed,?null,?void,?never; non-class standard types and duplicates inside intersections;self/staticreturn types outside class scope (closures exempt, as Zend compiles them); duplicateimplementsentries for classes and enums.One validation helper runs from a resolveTypeDecl override, so parameters, returns, properties, constants, and closures share the same pass without double-firing.
use T, Tis deliberately still accepted — Zend silently dedupes trait use (probed).Part of the split of #39.