fix(preprocessor): enforce Zend enum declaration rules - #61
fix(preprocessor): enforce Zend enum declaration rules#61AlessioGiacobbe wants to merge 4 commits into
Conversation
matyhtf
left a comment
There was a problem hiding this comment.
Two enum paths are still outside these checks.
- Forbidden magic methods introduced by a trait are accepted because the new check only runs in
prepareClassMethod(). Composed and aliased trait methods are installed throughinstallComposedTraitMethod():
trait T { public function __construct() {} }
enum Suit { use T; case Hearts; }Zend reports Enum Suit cannot include magic method __construct; this PR dry-compiles it. Centralize the enum magic-method check and call it for composed/aliased methods as well. Add at least a trait-injected __construct test (an alias renamed to a forbidden magic name should follow the same path).
- Backed enum case values are not limited to scalar literal nodes.
case Two = 1 + 1and constant references are valid PHP, but this code still reads$v->expr?->valueduring prepare:
enum Number: int { case Two = 1 + 1; }
function main(): void { var_dump(Number::Two->value); }TypePHP emits Undefined property: BinaryOp\Plus::$value, records null, and later resolves the backed case as the string case name instead of integer 2. Preserve the expression AST in prepare and evaluate it in the convert phase using the existing constant-expression machinery; do not assume a scalar node or evaluate implementation expressions during prepare. Add runtime coverage for an arithmetic expression and a constant reference.
c4a161a to
36fd7bb
Compare
|
Both paths closed, rebased on current master:
The rebase also surfaced a pleasant interaction: upstream's abstract-method checks now fire before the enum complaint for |
c4576f3 to
bef4331
Compare
matyhtf
left a comment
There was a problem hiding this comment.
Please rebase this branch onto the latest master; #45, #52, #59, #63, and #66 have now been merged.
Two constant-expression paths still need correction.
enumCaseExprsis unset only after evaluation succeeds, but there is no in-progress/visiting guard. Accessing a self-referencing case such asenum E: int { case A = E::A; }recursively entersgetClassConstValue()until the compiler exhausts the stack/memory. Zend reportsCannot declare self-referencing constant E::A. Mutual cycles need the same protection.- Lazy evaluation can run while the translator is converting a different namespace.
getClassConstValue()currently prefers$this->namespace, and the class-constant callback reducesName\FullyQualifiedto a string, so an enum expression declared in namespaceAmay be resolved relative to namespaceBwhen first referenced there.
Please add an evaluation stack keyed by the declaring enum/case, preserve the AST until successful completion, and resolve names using the expression declaration context/resolved AST name rather than the current conversion namespace. Add self-cycle, mutual-cycle, and cross-file/cross-namespace tests.
Zend rejects at compile time, and TypePHP previously accepted silently:
- properties in enums (instance, static, hooked): enum class entries
have no property table ("Enum E cannot include properties")
- magic methods other than __call/__callStatic/__invoke ("Enum E
cannot include magic method __x"); the banned set was probed one by
one against Zend 8.4.13
- a case value on a non-backed enum and a missing value on a backed
enum ("Case A of ... enum E must (not) have a value")
- duplicate case names and case/const name collisions: enum cases are
class constants ("Cannot redefine class constant E::A")
- a backing type other than int|string
- explicitly implementing UnitEnum/BackedEnum, which Zend adds itself
("cannot implement previously implemented interface"), including the
non-backed-enum BackedEnum variant
- abstract methods in enum bodies: an enum can never be abstract
Enum ClassDef flags now carry Modifiers::FINAL, mirroring ZEND_ACC_FINAL
on enum class entries, so `class B extends E` is rejected by the
existing final-class inheritance check without touching the Translator.
…sions
Two gaps against Zend 8.4 in the enum declaration rules:
- The forbidden-magic-method check only ran in prepareClassMethod(), so a
magic method arriving through a trait — either declared there or created
by an alias adaptation that renames an ordinary method to a magic name —
was silently accepted. Zend applies the ban to every method installed in
the enum ("Enum Suit cannot include magic method __construct", probed on
8.4.13 for both the composed and the aliased form). The check is now
centralized in assertEnumMayIncludeMethod() and also invoked from
installComposedTraitMethod(), which sees post-adaptation names.
- A backed case value beyond a scalar literal (case Two = 1 + 1, or a
constant reference) read the nonexistent ->value off the expression node,
warning "Undefined property" and recording null, which later made the
constant folder resolve Number::Two to the case-name string. Zend accepts
any constant expression here. The preprocessor now keeps the expression
AST (the symbol environment is incomplete during prepare) in a ClassDef
case-name => Expr map, and the convert phase evaluates it lazily with the
existing constant-expression machinery in ClassConstantValueTrait,
memoizing the result on first access. Plain constant fetches now also
resolve program constants recorded by parseConstDef(), so
`const TWO = 2; ... case Two = TWO;` folds to 2.
…oreign contexts
Two constant-expression paths in the lazy backed-case evaluation were
still incorrect:
- No in-progress guard: a self-referencing case such as
`enum E: int { case A = E::A; }` re-entered getClassConstValue()
through the stored expression AST until the stack was exhausted, and
mutual cycles (`case A = E::B; case B = E::A;`) did the same. The
evaluation now marks the case in progress for its duration
(mirroring CONST_RECURSIVE on a Zend class-constant fetch, with
gen_stub's case-table evaluation playing Zend's unmarked outer
access) and fails when a marked case is fetched again, reporting the
same constant Zend names on 8.4.13: `E::A` for the self-cycle and
`E::B` for the mutual cycle. The AST entry now survives until
evaluation succeeds, so an aborted evaluation cannot leave a
half-initialized null behind in enumCases.
- Wrong resolution context: the first fetch of a case may happen while
the translator is converting a different file (a constant initializer
in namespace B referencing A\E::X), and names inside the stored
expression were resolved against that context: the ClassConstFetch
callback reduced the name node to a bare string, which
getClassConstValue() then prefixed with the active namespace. The
callback now prefers the NameResolver's resolvedName attribute (or
the node's own fully qualified form), and the evaluation runs inside
the enum's declaration context — its namespace plus the declaring
file's use tables, captured at prepare like the trait ones — through
withDeclarationNameContext(), factored out of withTraitNameContext()
which needed the identical swap.
gen_stub's processStubFile() wrapped every exception into a bare
RuntimeException; TestError now passes through so compile diagnostics
raised during stub generation keep their type for the test harness.
Tests cover the self-cycle and mutual-cycle fatals (messages probed on
Zend 8.4.13), a cross-namespace program with a decoy B\Helper constant,
and a cross-file pair converted referencing-file-first, asserting the
zvals emitted into the stub registration (values verified against Zend
8.4.13).
bef4331 to
f679874
Compare
|
Rebased onto current master. Both paths fixed:
Tests: self-cycle and mutual-cycle fatals with the exact messages, plus cross-namespace (with a decoy same-name symbol in the referencing namespace) and cross-file positives asserting the generated case/constant values, with the referencing file converted first. |
Enum declaration rules were entirely unenforced — all of these compiled (each a Zend compile fatal, probed for the exact rule and message): properties in enums; the forbidden magic-method set (__construct, __destruct, __clone, __get/__set/__unset/__isset, __sleep/__wakeup, __set_state, __serialize/__unserialize, __toString, __debugInfo — the full set probed one by one; __call/__callStatic/__invoke stay legal); a case with a value in a non-backed enum and a case without one in a backed enum; duplicate case names and case/const clashes; backing types other than int|string; explicit
implements UnitEnum/BackedEnum; abstract methods in enum bodies.Enum ClassDef flags now also carry FINAL (mirroring ZEND_ACC_FINAL on enum class entries), so
class B extends SomeEnumis rejected by the existing final-class check with Zend's own wording, and final-class devirtualization legitimately applies to enums.Deliberately not added, because Zend evaluates them lazily at runtime: case-value/backing-type mismatches and duplicate case values.
Part of the split of #39.