diff --git a/src/AngleSharp.Js.Tests/DomTests.cs b/src/AngleSharp.Js.Tests/DomTests.cs index dcd220a..c4baa29 100644 --- a/src/AngleSharp.Js.Tests/DomTests.cs +++ b/src/AngleSharp.Js.Tests/DomTests.cs @@ -232,6 +232,27 @@ public async Task NumericIndexerOfNamedNodeMapStillReadsBothWays() Assert.AreEqual("1,title,t", result); } + // An element whose id happens to be numeric must not surface as an index of the + // collection. The array-like projection owns every array-index key - an index past the + // end is authoritatively absent - so the named entry answers only non-index names, + // which is also how WebIDL resolves the collision. Every answer has to say the same + // thing, the descriptor included. + [Test] + public async Task NamedEntryWithAnIndexShapedNameIsNotAnIndex() + { + var result = await "(function () { var f = document.createElement('form'); f.id = '5'; document.documentElement.appendChild(f); var c = document.forms; return (Object.getOwnPropertyDescriptor(c, '5') === undefined) + ',' + ('5' in c) + ',' + (c['5'] === undefined) + ',' + c.hasOwnProperty('5') + ',' + (c[0] === f); })()".EvalScriptAsync(); + Assert.AreEqual("true,false,true,false,true", result); + } + + // The guard above must not overreach: a named entry whose name is not an array index + // keeps resolving, descriptor and all. + [Test] + public async Task NamedEntryWithAnOrdinaryNameStillResolves() + { + var result = await "(function () { var f = document.createElement('form'); f.id = 'login'; document.documentElement.appendChild(f); var c = document.forms; return (c.login === f) + ',' + (Object.getOwnPropertyDescriptor(c, 'login') !== undefined); })()".EvalScriptAsync(); + Assert.AreEqual("true,true", result); + } + // A symbol cannot be an index, and it must not be turned into one either - the // well-known symbols are probed on every kind of object by library code. [Test] diff --git a/src/AngleSharp.Js.nuspec b/src/AngleSharp.Js.nuspec index 0284aed..31706a6 100644 --- a/src/AngleSharp.Js.nuspec +++ b/src/AngleSharp.Js.nuspec @@ -18,27 +18,27 @@ - + - + - + - + - + - + diff --git a/src/AngleSharp.Js/Proxies/DomCollectionInstance.cs b/src/AngleSharp.Js/Proxies/DomCollectionInstance.cs index 8f75431..2bfe5d2 100644 --- a/src/AngleSharp.Js/Proxies/DomCollectionInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomCollectionInstance.cs @@ -1,6 +1,7 @@ namespace AngleSharp.Js { using AngleSharp.Js.Cache; + using Jint; using Jint.Native; using Jint.Native.Object; using Jint.Native.Symbol; @@ -109,6 +110,16 @@ public override PropertyDescriptor GetOwnProperty(JsValue property) return descriptor; } + // The base has answered every array-index key authoritatively - an index past the + // end is a miss its value and existence hooks already committed to - so the string + // indexer must not be asked about one. WebIDL says the same: an object supporting + // indexed properties never serves an array-index name from its named getter. An + // element whose id is "5" is findable through a non-index name, never through 5. + if (IsArrayIndexName(property)) + { + return PropertyDescriptor.Undefined; + } + if (Prototype is DomPrototypeInstance prototype && prototype.TryGetFromNamedIndex(_value, property, out _)) { @@ -118,6 +129,33 @@ public override PropertyDescriptor GetOwnProperty(JsValue property) return PropertyDescriptor.Undefined; } + // The test the engine's array-like model applies to a key, mirrored exactly so this + // class and its base always classify a name the same way: a number holding a UInt32 + // below the maximum, or its canonical string form - no sign, no space, no leading zero. + private static Boolean IsArrayIndexName(JsValue property) + { + if (property.IsNumber()) + { + var value = property.AsNumber(); + var index = (UInt32)value; + return value == index && index != UInt32.MaxValue; + } + + if (property.IsString()) + { + var name = property.ToString(); + + if (name.Length == 0 || (name.Length > 1 && (name[0] < '1' || name[0] > '9'))) + { + return false; + } + + return UInt32.TryParse(name, out var index) && index != UInt32.MaxValue; + } + + return false; + } + protected override void SetOwnProperty(JsValue property, PropertyDescriptor desc) { if (Prototype is DomPrototypeInstance prototype &&