diff --git a/src/AngleSharp.Js.Tests/DomTests.cs b/src/AngleSharp.Js.Tests/DomTests.cs index 5d1b975..88b41b7 100644 --- a/src/AngleSharp.Js.Tests/DomTests.cs +++ b/src/AngleSharp.Js.Tests/DomTests.cs @@ -26,5 +26,47 @@ public async Task NodeHasChildNodesWithChildren() var result = await "new DOMParser().parseFromString(`
`, 'text/html').body.firstChild.hasChildNodes()".EvalScriptAsync(); Assert.AreEqual("True", result); } + + [Test] + public async Task PrototypeChainOfElementIsBuiltCompletely() + { + var result = await "(function () { var p = Object.getPrototypeOf(document.createElement('div')), t = []; while (p) { t.push(p[Symbol.toStringTag]); p = Object.getPrototypeOf(p); } return t.join(); })()".EvalScriptAsync(); + Assert.AreEqual("HTMLDivElement,HTMLElement,Element,Node,EventTarget,", result); + } + + [Test] + public async Task ConstructorPropertyOfPrototypeRefersBackToTheConstructor() + { + var result = await "HTMLDivElement.prototype.constructor === HTMLDivElement".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task NumericIndexerOfHtmlCollectionYieldsTheElement() + { + var result = await "document.getElementsByTagName('script')[0].nodeName".EvalScriptAsync(); + Assert.AreEqual("SCRIPT", result); + } + + [Test] + public async Task NumericIndexerOfHtmlCollectionOutOfRangeIsUndefined() + { + var result = await "typeof document.getElementsByTagName('script')[5]".EvalScriptAsync(); + Assert.AreEqual("undefined", result); + } + + [Test] + public async Task NumericIndexerOfNodeListYieldsTheNode() + { + var result = await "new DOMParser().parseFromString(`
`, 'text/html').body.childNodes[0].nodeName".EvalScriptAsync(); + Assert.AreEqual("DIV", result); + } + + [Test] + public async Task NumericIndexerOfTokenListYieldsTheToken() + { + var result = await "new DOMParser().parseFromString(`
`, 'text/html').body.firstChild.classList[1]".EvalScriptAsync(); + Assert.AreEqual("b", result); + } } } diff --git a/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs b/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs index 64bd0d3..4c36f1f 100644 --- a/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs @@ -22,7 +22,19 @@ public DomConstructorInstance(EngineInstance engine, Type type) _instance = engine; FastSetProperty("toString", new PropertyDescriptor(toString, true, false, true)); SetOwnProperty("prototype", new PropertyDescriptor(_objectPrototype, false, false, false)); - _objectPrototype.FastSetProperty("constructor", new PropertyDescriptor(this, true, false, true)); + + var constructor = new PropertyDescriptor(this, true, false, true); + + // Every exposed type gets a constructor, so writing this directly would make + // each of their prototypes register its members right away. + if (_objectPrototype is DomPrototypeInstance domPrototype) + { + domPrototype.DefineDeferredProperty("constructor", constructor); + } + else + { + _objectPrototype.FastSetProperty("constructor", constructor); + } } public DomConstructorInstance(EngineInstance engine, ConstructorInfo constructor) diff --git a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs index df226b9..a3a393c 100644 --- a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs @@ -15,24 +15,75 @@ sealed class DomPrototypeInstance : ObjectInstance { private readonly String _name; private readonly EngineInstance _instance; + private readonly Type _type; + private readonly Type _baseType; - private PropertyInfo _numericIndexer; - private PropertyInfo _stringIndexer; + private List> _deferred; + private Boolean _membersSet; + private MethodInfo _numericIndexer; + private MethodInfo _stringIndexer; public DomPrototypeInstance(EngineInstance engine, Type type) : base(engine.Jint) { - var baseType = type.GetTypeInfo().BaseType ?? typeof(Object); - _name = type.GetOfficialName(baseType); + _baseType = type.GetTypeInfo().BaseType ?? typeof(Object); + _name = type.GetOfficialName(_baseType); _instance = engine; + _type = type; + } + + // A document uses a handful of the DOM types an assembly exposes, but a prototype + // is created for every one of them. Reflecting over the whole type tree is by far + // the most expensive part of that, so it waits until the prototype is looked at. + // Jint calls this before serving any property, and marks the instance initialized + // beforehand, so the registration below can use the object as usual. + protected override void Initialize() + { + _membersSet = true; Set(GlobalSymbolRegistry.ToStringTag, _name); - SetAllMembers(type); + SetAllMembers(_type); SetExtensionMembers(); // DOM objects can have properties added dynamically - Prototype = engine.GetDomPrototype(baseType); + Prototype = _instance.GetDomPrototype(_baseType); + + if (_deferred != null) + { + foreach (var property in _deferred) + { + FastSetProperty(property.Key, property.Value); + } + + _deferred = null; + } + } + + // The prototype link is only established once the members are known, so reading it + // has to initialize as well - not every reader goes through a property lookup. + protected override ObjectInstance GetPrototypeOf() + { + EnsureInitialized(); + return base.GetPrototypeOf(); + } + + /// + /// Defines a property on the prototype without forcing its members to be + /// registered. The property is applied after the members, so it keeps + /// overriding a member of the same name. + /// + public void DefineDeferredProperty(String name, PropertyDescriptor descriptor) + { + if (_membersSet) + { + FastSetProperty(name, descriptor); + } + else + { + _deferred = _deferred ?? new List>(); + _deferred.Add(new KeyValuePair(name, descriptor)); + } } public Boolean TryGetFromIndex(Object value, String index, out PropertyDescriptor result) @@ -40,12 +91,14 @@ public Boolean TryGetFromIndex(Object value, String index, out PropertyDescripto // If we have a numeric indexer and the property is numeric result = default; + EnsureInitialized(); + if (_numericIndexer != null && Int32.TryParse(index, out var numericIndex)) { try { var args = new Object[] { numericIndex }; - var orig = _numericIndexer.GetMethod.Invoke(value, args); + var orig = _numericIndexer.Invoke(value, args); result = new PropertyDescriptor(orig.ToJsValue(_instance), false, false, false); return true; } @@ -70,7 +123,7 @@ public Boolean TryGetFromIndex(Object value, String index, out PropertyDescripto if (_stringIndexer != null && !HasProperty(index)) { var args = new Object[] { index }; - var valueAtIndex = _stringIndexer.GetMethod.Invoke(value, args); + var valueAtIndex = _stringIndexer.Invoke(value, args); if (valueAtIndex == null) { @@ -229,19 +282,54 @@ private void SetProperty(String name, MethodInfo getter, MethodInfo setter, DomP private void SetIndexer(PropertyInfo property, ParameterInfo[] indexParameters) { - if (indexParameters.Length == 1) + if (indexParameters.Length != 1) { - if (indexParameters[0].ParameterType == typeof(Int32)) - { - _numericIndexer = property; - } - else if (indexParameters[0].ParameterType == typeof(String)) - { - _stringIndexer = property; - } + return; + } + + var getter = ResolveAccessor(property.GetMethod); + + if (getter == null) + { + return; + } + + if (indexParameters[0].ParameterType == typeof(Int32)) + { + _numericIndexer = getter; + } + else if (indexParameters[0].ParameterType == typeof(String)) + { + _stringIndexer = getter; } } + private MethodInfo ResolveAccessor(MethodInfo accessor) + { + // An interface may re-implement a member of one of its own base interfaces + // explicitly, e.g. "T IReadOnlyList.this[Int32 index]" declared on an + // IHtmlCollection. Such a member is private and abstract - invoking it + // reflectively throws an EntryPointNotFoundException because the actual + // implementation lives in a different slot. Resolve it against the type the + // prototype was created for, which is where the implementation can be found. + if (accessor == null || accessor.IsPublic) + { + return accessor; + } + + var name = accessor.Name; + var simpleName = name.Substring(name.LastIndexOf('.') + 1); + var parameters = accessor.GetParameters(); + var parameterTypes = new Type[parameters.Length]; + + for (var i = 0; i < parameters.Length; i++) + { + parameterTypes[i] = parameters[i].ParameterType; + } + + return _type.GetRuntimeMethod(simpleName, parameterTypes) ?? accessor; + } + private void SetMethod(String name, MethodInfo method) { //TODO Jint