diff --git a/Jint.Tests/Runtime/ForInEnumerationTests.cs b/Jint.Tests/Runtime/ForInEnumerationTests.cs
index bf99bb56a..ccc579dda 100644
--- a/Jint.Tests/Runtime/ForInEnumerationTests.cs
+++ b/Jint.Tests/Runtime/ForInEnumerationTests.cs
@@ -319,6 +319,27 @@ public void ForInOverArrayEnumeratesIndicesThenExtraProperties()
Assert.Equal("0,1,2,foo", result);
}
+ [Fact]
+ public void ForInOverArrayIncludesInheritedEnumerableArrayPrototypeIndex()
+ {
+ // Regression: Array.prototype is itself array-backed, so script-placed elements on it live in
+ // its dense store — the builtin-shape shared name list does not include them. The for-in key
+ // shortcut must not hand out that list once Array.prototype carries own index keys: the
+ // inherited enumerable '5' must be enumerated after the own indices, and the own '1' must
+ // shadow the inherited '1'.
+ var engine = new Engine();
+ var result = engine.Evaluate("""
+ Array.prototype[5] = 'inherited';
+ Array.prototype[1] = 'shadowed';
+ var arr = [10, 20];
+ var out = [];
+ for (var k in arr) { out.push(k); }
+ out.join(',');
+ """).AsString();
+
+ Assert.Equal("0,1,5", result);
+ }
+
[Fact]
public void ForInKeysAreStrings()
{
diff --git a/Jint/Native/Array/ArrayInstance.cs b/Jint/Native/Array/ArrayInstance.cs
index f95c86ac3..88760baca 100644
--- a/Jint/Native/Array/ArrayInstance.cs
+++ b/Jint/Native/Array/ArrayInstance.cs
@@ -506,6 +506,33 @@ internal override OwnPropertyProbe ProbeOwnProperty(JsValue property)
return base.ProbeOwnProperty(property);
}
+ ///
+ /// True when any own index-keyed element exists: a present dense element or a materialized
+ /// per-index descriptor. O(1) for the pristine case that matters (Array.prototype's empty
+ /// backing store) — the dense scan only runs over slots the object actually allocated.
+ ///
+ internal bool HasAnyOwnIndex()
+ {
+ if (_sparse is { Count: > 0 })
+ {
+ return true;
+ }
+
+ var dense = _dense;
+ if (dense is not null)
+ {
+ foreach (var element in dense)
+ {
+ if (element is not null)
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
internal JsValue Get(uint index)
{
if (!TryGetValue(index, out var value))
diff --git a/Jint/Native/Object/ObjectInstance.cs b/Jint/Native/Object/ObjectInstance.cs
index bb20152ee..ba27bccf4 100644
--- a/Jint/Native/Object/ObjectInstance.cs
+++ b/Jint/Native/Object/ObjectInstance.cs
@@ -455,11 +455,13 @@ internal IReadOnlyList GetForInStringKeys()
}
else if ((_type & InternalTypes.BuiltinShapeMode) != InternalTypes.Empty
&& _properties is null
+ && (this is not Array.ArrayInstance arrayInstance || !arrayInstance.HasAnyOwnIndex())
&& ReferenceEquals(GetInitialOwnStringPropertyKeys(), System.Linq.Enumerable.Empty()))
{
- // No function-derived length/name/prototype prefix and no hybrid dictionary additions, so the
- // built-in's shared, ordered name list is exactly its own string keys — e.g. Object.prototype
- // (the common deeper for-in level).
+ // No function-derived length/name/prototype prefix, no hybrid dictionary additions, and no
+ // exotic index storage (Array.prototype is array-backed: script CAN place elements on it,
+ // which the shared name list would miss), so the built-in's shared, ordered name list is
+ // exactly its own string keys — e.g. Object.prototype (the common deeper for-in level).
return Unsafe.As(this).BuiltinShape.NamesAsJsStrings;
}