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
47 changes: 47 additions & 0 deletions Jint.Tests/Runtime/ArrayTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,53 @@ public void FilterSkipsHoles()
Assert.Equal("[1,3]", result);
}

[Fact]
public void HoleReadFindsInheritedIndexOnArrayPrototype()
{
// The pristine-prototypes shortcut must disengage the moment Array.prototype (or
// Object.prototype) gains an index property: hole reads and `in` then walk the chain.
var engine = new Engine();
var result = engine.Evaluate("""
var a = [1, , 3];
var before = [a[1], 1 in a, a[10], 10 in a];
Array.prototype[1] = 'ap';
Object.prototype[10] = 'op';
var after = [a[1], 1 in a, a[10], 10 in a];
JSON.stringify([before, after]);
""").AsString();

Assert.Equal("[[null,false,null,false],[\"ap\",true,\"op\",true]]", result);
}

[Fact]
public void HoleReadHonorsIndexGetterOnArrayItself()
{
// an exotic own descriptor clears the fast-access invariant on the instance
var engine = new Engine();
var result = engine.Evaluate("""
var a = [1, 2, 3];
Object.defineProperty(a, '5', { get: function () { return 'got'; } });
[a[5], 5 in a, a[7] === undefined].join(',');
""").AsString();

Assert.Equal("got,true,true", result);
}

[Fact]
public void HoleReadWalksCustomPrototypeChain()
{
var engine = new Engine();
var result = engine.Evaluate("""
var proto = { 1: 'inherited' };
var a = [0];
a.length = 3;
Object.setPrototypeOf(a, proto);
[a[1], 1 in a, a[2] === undefined, 2 in a].join(',');
""").AsString();

Assert.Equal("inherited,true,true,false", result);
}

[Fact]
public void FilterSubclassUsesSpecies()
{
Expand Down
36 changes: 32 additions & 4 deletions Jint/Native/Array/ArrayInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,15 @@ internal JsValue Get(uint index)
{
if (!TryGetValue(index, out var value))
{
// Reading a hole walks the prototype chain, but when nothing relevant has changed
// (no exotic descriptors here, prototypes pristine — the same invariant the write
// fast path trusts) the walk provably yields undefined: skip it and the per-hole
// JsString the probe would allocate.
if (CanUseFastAccess)
{
return Undefined;
}

value = Prototype?.Get(JsString.Create(index)) ?? Undefined;
}

Expand All @@ -614,9 +623,18 @@ internal JsValue Get(uint index)

public sealed override JsValue Get(JsValue property, JsValue receiver)
{
if (IsSafeSelfTarget(receiver) && IsArrayIndex(property, out var index) && TryGetValue(index, out var value))
if (IsSafeSelfTarget(receiver) && IsArrayIndex(property, out var index))
{
return value;
if (TryGetValue(index, out var value))
{
return value;
}

// hole/out-of-range read with pristine prototypes: see Get(uint)
if (CanUseFastAccess)
{
return Undefined;
}
}

if (CommonProperties.Length.Equals(property))
Expand Down Expand Up @@ -662,9 +680,19 @@ public sealed override bool Set(JsValue property, JsValue value, JsValue receive

public sealed override bool HasProperty(JsValue property)
{
if (IsArrayIndex(property, out var index) && GetValue(index, unwrapFromNonDataDescriptor: false) is not null)
if (IsArrayIndex(property, out var index))
{
return true;
if (GetValue(index, unwrapFromNonDataDescriptor: false) is not null)
{
return true;
}

// absent index with pristine prototypes: the chain walk provably finds nothing
// (same invariant as Get; exotic own descriptors clear CanUseFastAccess)
if (CanUseFastAccess)
{
return false;
}
}

return base.HasProperty(property);
Expand Down
Loading