diff --git a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs index 630cc8c066..49065fe33c 100644 --- a/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs +++ b/crates/perry-runtime/src/object/field_get_set/get_field_by_name.rs @@ -852,6 +852,47 @@ pub extern "C" fn js_object_get_field_by_name( if let Some(v) = result { return JSValue::from_bits(v.to_bits()); } + // Static DATA fields are INHERITED by subclasses, exactly like + // static methods: `class D {}; D.kind = "x"; class G extends D {}` + // makes `G.kind === "x"` (the class-object proto chain + // `G.__proto__ === D` carries statics). The own-field read above + // only consulted `class_id`; walk the parent class_id chain here + // so an inherited static field (or runtime `Parent.x = …` + // assignment — both live in CLASS_DYNAMIC_PROPS) resolves. Static + // METHODS are handled by `lookup_static_method_in_chain` below; + // this covers the data-field case that was returning `undefined` + // (Auth.js sets `SignInError.kind = "signIn"` and reads it off a + // `CredentialsSignin` subclass to pick the sign-in vs error page). + { + let mut cid = class_id; + let mut depth = 0usize; + while depth < 32 { + match get_parent_class_id(cid) { + Some(p) if p != 0 && p != cid => { + cid = p; + depth += 1; + } + _ => break, + } + if super::super::class_registry::class_is_key_deleted(cid, name) { + // A key deleted on THIS ancestor is not provided by + // it, but a higher ancestor may still define it — + // `delete Mid.foo` must let `Sub.foo` inherit + // `Base.foo`, not resolve to undefined. Skip this + // level and keep walking up (safe: `cid`/`depth` + // advance at the top of every iteration). + continue; + } + let inherited = CLASS_DYNAMIC_PROPS.with(|m| { + m.borrow() + .get(&cid) + .and_then(|props| props.get(name).copied()) + }); + if let Some(v) = inherited { + return JSValue::from_bits(v.to_bits()); + } + } + } if super::super::class_registry::lookup_static_method_in_chain(class_id, name) .is_some() { diff --git a/test-files/test_gap_static_field_inheritance.ts b/test-files/test_gap_static_field_inheritance.ts new file mode 100644 index 0000000000..989b2b090c --- /dev/null +++ b/test-files/test_gap_static_field_inheritance.ts @@ -0,0 +1,32 @@ +// A subclass inherits its parent's static DATA properties — both in-body static +// fields and runtime `Parent.x = …` assignments — via the class-object prototype +// chain (`Sub.__proto__ === Base`). Regression: perry inherited static METHODS +// but returned `undefined` for inherited static data fields, so +// `Sub.tag` / `Sub.kind` read undefined even though `Base` defined them. (Auth.js +// sets `SignInError.kind = "signIn"` and reads it off a `CredentialsSignin` +// subclass to choose the sign-in vs error redirect page.) +class Base { + static inBody = "field"; +} +(Base as any).external = "assigned"; +class Sub extends Base {} +class Leaf extends Sub {} +console.log("Sub.inBody=" + (Sub as any).inBody); +console.log("Sub.external=" + (Sub as any).external); +console.log("Leaf.inBody=" + (Leaf as any).inBody); +console.log("Leaf.external=" + (Leaf as any).external); +// own field still wins over inherited +(Sub as any).external = "own"; +console.log("Sub.own-wins=" + (Sub as any).external); +console.log("Base.unshadowed=" + (Base as any).external); + +// A key deleted on an INTERMEDIATE ancestor must not shadow a higher one: +// `delete Mid.foo` should let `Sub.foo` inherit `Base.foo`, not resolve to +// undefined. (The chain walk must `continue` past the deleted level, not +// `break` out of the whole traversal.) +class DBase { static tag = "base"; } +class DMid extends DBase { static tag = "mid"; } +class DSub extends DMid {} +console.log("del-intermediate before:", (DSub as any).tag); +delete (DMid as any).tag; +console.log("del-intermediate after :", (DSub as any).tag);