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
Expected: refuse. App\Status is an enum; App\Legacy\Status is a stranger.
Mechanism
Two independent gaps line up:
_PHP_CONFIG.class_types is frozenset({"class_declaration"}) (extract.py:737). enum_declaration and trait_declaration are absent — note the TS, Java and C# configs do include enum_declaration (extract.py:755, 796, 872).
_php_pre_scan_interfaces (engine.py:146) matches onlyinterface_declaration, so enum and trait names never reach the refusal set consumed at extract.py:3168.
So an enum name is absent from bothtype_def_nids and interface_names: it cannot resolve, and it cannot trigger a refusal. Any same-short-named class is then the single visible definition and sails through the len(type_defs) != 1 god-node guard.
Node dump (/tmp/rt6/nodes.py) shows the enum is structurally invisible — no type node, no method edge:
nodes (type | label | source_file | contained?):
None | Status.php | app/Status.php | -
None | label() | app/Status.php | CONTAINED <- file-level function, no method edge
None | Klass.php | app/Klass.php | -
None | Klass | app/Klass.php | CONTAINED <- class DOES mint a type node
None | .label() | app/Klass.php | -
method edges: [('..._klass', '..._klass_label')] <- class only
The enum's label() becomes a file-level function node rather than a method on a type.
All four receiver entry points leak
Verified in /tmp/rt6/wave3.py; every one produces the wrong edge:
Entry point
Fixture
Result
Typed property
private Status $status;
wrong edge (wave1.py::A4)
Promoted ctor param
__construct(private Status $s)
wrong edge (F1_promoted)
Typed method param
go(Status $s)
wrong edge (F1_typed_param)
FQN-typed property
private \App\Status $s;
wrong edge (F1_fqn_prop)
The FQN variant is the sharpest: the source names the enum unambiguously and the resolver still binds the stranger.
Traits: same root cause
/tmp/rt6/wave2.py::B1_trait_vs_class — trait App\Cache + unrelated class App\Legacy\Cache + private Cache $cache yields go() -> Legacy/Cache.php:.flush(). Lower realism (a trait is not a valid type, so the fixture is already broken PHP), but it is the same missing-declaration-kind hole and a fix should cover it.
Controls (guards that DO hold)
Enum alone, no colliding class → no edge (a4.py::A4_control_enum_only) — proves the enum is unresolvable, so the collision supplies the only candidate.
Two colliding classes → no edge (a4.py::A4_control_two_classes) — the god-node guard works; enums simply don't participate in it.
High. An enum mirroring a model is standard Laravel: App\Enums\Role beside an Eloquent App\Models\Role, App\Enums\Status beside App\Models\Status. PHP 8.1 enums with methods are idiomatic, and enum-typed properties are exactly how they are consumed.
What to build
Close both halves so enums and traits can neither be mis-bound nor silently ignored. Either:
(a) extend _php_pre_scan_interfaces into a general "non-class declaration" pre-scan covering enum_declaration and trait_declaration, threading the names to the resolver so they are refused alongside interfaces (minimal, matches the shipped PHP: interface-typed receivers refuse, even under short-name collision #5 pattern); or
(b) add enum_declaration to _PHP_CONFIG.class_types so enums mint real type nodes and method edges, letting the existing single-definition guard see the collision and refuse — this also fixes the recall gap below. Traits still need (a), since a trait is not a type.
(b) is the larger change but strictly better for enums; (a) alone leaves enum methods unresolvable forever.
Acceptance criteria
Enum-typed receiver with an unrelated same-short-named class yields no edge, across all four entry points (typed property, promoted param, typed param, FQN-typed property)
Trait-typed receiver with an unrelated same-short-named class yields no edge
Independent of the wrong edge above, enum methods are not resolvable as call targets at all: an enum mints no type node and no method edge, so $enumTyped->method() can never resolve even with no collision present. If the fix takes route (a), this stays true and should be named in the changelog next to the other deliberate refusals (interface-typed, union, untyped, docblock-only, chained, trait and inherited methods). If it takes route (b), enum calls become resolvable and that is a recall improvement worth naming instead.
Parent
Spec: #1 (upstream: Graphify-Labs#1682). Found by the red-team session against
2e68ec6(tickets #2-#6 + #8). Repro scripts:/tmp/rt6/(harness.py,wave1.py,a4.py,wave2.py,wave3.py,nodes.py).Problem
This is the defect verifier-2 raised for interfaces and #5 fixed — left open for the other two PHP declaration kinds that mint no definition node.
An enum-typed receiver resolves to a completely unrelated class that merely shares its short name, at
INFERRED0.8.Actual:
Expected: refuse.
App\Statusis an enum;App\Legacy\Statusis a stranger.Mechanism
Two independent gaps line up:
_PHP_CONFIG.class_typesisfrozenset({"class_declaration"})(extract.py:737).enum_declarationandtrait_declarationare absent — note the TS, Java and C# configs do includeenum_declaration(extract.py:755,796,872)._php_pre_scan_interfaces(engine.py:146) matches onlyinterface_declaration, so enum and trait names never reach the refusal set consumed atextract.py:3168.So an enum name is absent from both
type_def_nidsandinterface_names: it cannot resolve, and it cannot trigger a refusal. Any same-short-named class is then the single visible definition and sails through thelen(type_defs) != 1god-node guard.Node dump (
/tmp/rt6/nodes.py) shows the enum is structurally invisible — no type node, nomethodedge:The enum's
label()becomes a file-level function node rather than a method on a type.All four receiver entry points leak
Verified in
/tmp/rt6/wave3.py; every one produces the wrong edge:private Status $status;wave1.py::A4)__construct(private Status $s)F1_promoted)go(Status $s)F1_typed_param)private \App\Status $s;F1_fqn_prop)The FQN variant is the sharpest: the source names the enum unambiguously and the resolver still binds the stranger.
Traits: same root cause
/tmp/rt6/wave2.py::B1_trait_vs_class—trait App\Cache+ unrelatedclass App\Legacy\Cache+private Cache $cacheyieldsgo() -> Legacy/Cache.php:.flush(). Lower realism (a trait is not a valid type, so the fixture is already broken PHP), but it is the same missing-declaration-kind hole and a fix should cover it.Controls (guards that DO hold)
a4.py::A4_control_enum_only) — proves the enum is unresolvable, so the collision supplies the only candidate.a4.py::A4_control_two_classes) — the god-node guard works; enums simply don't participate in it.interface MAILER+class Mailer, case-mismatched → refused (wave2.py::B2_iface_case) — PHP: interface-typed receivers refuse, even under short-name collision #5's case-insensitive interface refusal is sound.Realism
High. An enum mirroring a model is standard Laravel:
App\Enums\Rolebeside an EloquentApp\Models\Role,App\Enums\StatusbesideApp\Models\Status. PHP 8.1 enums with methods are idiomatic, and enum-typed properties are exactly how they are consumed.What to build
Close both halves so enums and traits can neither be mis-bound nor silently ignored. Either:
_php_pre_scan_interfacesinto a general "non-class declaration" pre-scan coveringenum_declarationandtrait_declaration, threading the names to the resolver so they are refused alongside interfaces (minimal, matches the shipped PHP: interface-typed receivers refuse, even under short-name collision #5 pattern); orenum_declarationto_PHP_CONFIG.class_typesso enums mint real type nodes andmethodedges, letting the existing single-definition guard see the collision and refuse — this also fixes the recall gap below. Traits still need (a), since a trait is not a type.(b) is the larger change but strictly better for enums; (a) alone leaves enum methods unresolvable forever.
Acceptance criteria
testparity with PHP: interface-typed receivers refuse, even under short-name collision #5)Changelog note for #7 (recall gap)
Independent of the wrong edge above, enum methods are not resolvable as call targets at all: an enum mints no type node and no
methodedge, so$enumTyped->method()can never resolve even with no collision present. If the fix takes route (a), this stays true and should be named in the changelog next to the other deliberate refusals (interface-typed, union, untyped, docblock-only, chained, trait and inherited methods). If it takes route (b), enum calls become resolvable and that is a recall improvement worth naming instead.