Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Jint.Tests/Runtime/ForInEnumerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
27 changes: 27 additions & 0 deletions Jint/Native/Array/ArrayInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,33 @@ internal override OwnPropertyProbe ProbeOwnProperty(JsValue property)
return base.ProbeOwnProperty(property);
}

/// <summary>
/// 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.
/// </summary>
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))
Expand Down
8 changes: 5 additions & 3 deletions Jint/Native/Object/ObjectInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -455,11 +455,13 @@ internal IReadOnlyList<JsValue> 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<JsValue>()))
{
// 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<IBuiltinShaped>(this).BuiltinShape.NamesAsJsStrings;
}

Expand Down
Loading