Skip to content

Stop DOM nodes from claiming inherited members as own properties - #112

Merged
FlorianRappl merged 1 commit into
AngleSharp:develfrom
lahma:fix/dom-node-own-properties
Jul 26, 2026
Merged

Stop DOM nodes from claiming inherited members as own properties#112
FlorianRappl merged 1 commit into
AngleSharp:develfrom
lahma:fix/dom-node-own-properties

Conversation

@lahma

@lahma lahma commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

DomNodeInstance.GetOwnProperty falls back to returning the prototype's descriptor when the node has no own property of that name. [[GetOwnProperty]] is not supposed to look past the object itself, so a node ends up disagreeing with itself:

document.body.hasOwnProperty('firstChild')                       // true
Object.getOwnPropertyDescriptor(document.body, 'firstChild')     // an accessor
Object.getOwnPropertyNames(document.body)                        // []
Object.keys(document.body)                                       // []

Library code that feature-detects with hasOwnProperty before walking the prototype chain gets the wrong answer, and anything that pairs getOwnPropertyNames with getOwnPropertyDescriptor sees an object whose keys and descriptors do not agree.

Change

Drop the fallback. An indexer is the only thing that can legitimately produce an own property here, so that probe stays; everything else is a member of the DOM interface, lives on the prototype, and is found by the engine's ordinary own -> prototype lookup.

Reads, writes, method calls and in are unaffected - only the "is this mine?" answer changes.

Effect on member lookup

This started as a correctness fix, and on the Jint version the branch was written against that is all it was. Now that #120 has moved the project to Jint 4.14.0, the change has a second, mechanical consequence worth recording. Both points below are read off the engine's code path; nothing here has been measured.

JintMemberExpression resolves node.member by first asking the receiver whether it has an own property of that name.

  • A redundant probe disappears. With the fallback in place the override runs twice per read: once for that own-property question, and again inside the slow path of ObjectInstance.Get. Each run walks to the prototype and asks it for the descriptor. Without the fallback the first question misses cheaply and the engine resolves the member on the prototype itself, so the second walk is gone.
  • A per-site prototype cache becomes reachable. Jint 4.14.0 caches the resolved descriptor per member-access site, but installs the entry only when the receiver reports no own property for the name - precisely what the fallback prevented. Worth being clear about the shape of this: the entry is keyed on the receiver instance, so it pays off on repeated reads of the same node at the same site, not on a traversal that touches a different node each iteration. It also only covers members found on the direct prototype, which is the case here because each DOM prototype registers the whole interface tree.

Neither point is the reason for the change; the own-property answer being wrong is. They are noted because the version bump quietly turned a neutral change into a mildly beneficial one.

Tests

DomTests.InheritedMemberIsNotAnOwnPropertyOfTheNode fails on devel and passes here. Three further tests pin the behaviour that must not change: the member is still visible through the prototype, an inherited accessor still reads and writes, and a property assigned from script is still reported by both hasOwnProperty and getOwnPropertyNames.

I also diffed the full own-property name set and Symbol.toStringTag of every prototype in the chain of ten different DOM object kinds, plus the global object, before and after: byte-identical. That comparison was re-run against the current devel, since #117 changed when prototype members get registered.

Rebased onto current devel. The suite is green on net8.0, net462 and net472 - 131 tests on devel, 135 here.

🤖 Generated with Claude Code

https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik

@FlorianRappl FlorianRappl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@FlorianRappl FlorianRappl added this to the v1.0 milestone Jul 26, 2026
@lahma
lahma force-pushed the fix/dom-node-own-properties branch from a661c1f to 0984060 Compare July 26, 2026 19:08
`DomNodeInstance.GetOwnProperty` falls back to returning the prototype's
descriptor when the node has no own property of that name. `[[GetOwnProperty]]`
is not supposed to look past the object itself, so a node ends up disagreeing
with itself:

    document.body.hasOwnProperty('firstChild')          // true
    Object.getOwnPropertyDescriptor(b, 'firstChild')    // an accessor
    Object.getOwnPropertyNames(document.body)           // []
    Object.keys(document.body)                          // []

Library code that feature-detects with `hasOwnProperty` before walking the
prototype chain gets the wrong answer, and anything that pairs
`getOwnPropertyNames` with `getOwnPropertyDescriptor` sees an object whose keys
and descriptors do not agree.

Drop the fallback. An indexer is the only thing that can legitimately produce an
own property here, so that probe stays; everything else is a member of the DOM
interface, lives on the prototype, and is found by the engine's ordinary
own -> prototype lookup. Reads, writes, method calls and `in` are unaffected -
only the "is this mine?" answer changes.

As a side effect the node stops answering the prototype's own-property probe on
behalf of the prototype, which is what lets an engine cache a prototype lookup:
such caches only engage once the receiver genuinely reports no own property.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik
@lahma
lahma force-pushed the fix/dom-node-own-properties branch from 0984060 to ef497c2 Compare July 26, 2026 20:33
@FlorianRappl
FlorianRappl merged commit d2c1c74 into AngleSharp:devel Jul 26, 2026
5 checks passed
@lahma
lahma deleted the fix/dom-node-own-properties branch July 26, 2026 20:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants