-
-
Notifications
You must be signed in to change notification settings - Fork 771
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix 2226: restore props defined on prototype chain by deleting #2237
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A very thorough investigation and a very easy to follow pull request (considering the complexity) 🥇 💯
Would you mind folding the first commit into the one that moves the tests around, so we don't have any stray ES6 code in a commit?
test/issues/issue-2226.test.js
Outdated
console.log('obj', ownPropertyDescriptor2); | ||
|
||
referee.assert.isUndefined(ownPropertyDescriptor0) | ||
referee.assert.isUndefined(ownPropertyDescriptor2) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
referee.assert.isUndefined(ownPropertyDescriptor2) | |
assert.isUndefined(ownPropertyDescriptor2) |
@@ -3,9 +3,15 @@ | |||
module.exports = function getPropertyDescriptor(object, property) { | |||
var proto = object; | |||
var descriptor; | |||
var isOwn = object && Object.getOwnPropertyDescriptor(object, property); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this needs a check for object
. Without object
, the function won't work anyway.
If anything, the function should throw a TypeError
when object
is falsy.
Considering the name isOwn
, I would expect this to be a boolean value. In its current form, it will be an object (a property descriptor).
var isOwn = object && Object.getOwnPropertyDescriptor(object, property); | |
var isOwn = Boolean(Object.getOwnPropertyDescriptor(object, property)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need object. Without it all tests crashed. The loop below takes it into consideration, but I have to do it too.
The other I can fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I seem to have been momentarily high. What you say make sense. I just remember having it fail. I will inspect again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looked again, after seeing (again) it failing when removing object
: it is indeed needed. The loop never starts when it is undefined, protected by proto
being undefined and returns undefined
in that case.
Makes it possible to see if the descriptor originates from the object or its prototype chain
Very nice indeed! Thank you for putting so much thought and time into this 🙏 |
|
Purpose (TL;DR) - mandatory
Issue #2226 shows that we somehow handle props defined on the prototype chain in the wrong manner. This has mostly gone unnoticed, until someone tried stubbing a prop defined on a prototype that had
configurable: false
. Doing arestore()
will fail in this case.Background (Problem in detail) - optional
Up until now, we have simply defined a new property on the object, regardless of where the prop actually "lives". This is fine and does not cause an issue in itself. The problem is that when we restore, we try to restore/recreate the property descriptor onto the object, regardless of that object is the original source of that descriptor.
Truth be told, it is not that the "source" does not match: the basic problem here is that we try to redefine a property that we have said is non-configurable, but that is just a symptom of the underlying problem, which is that we do not take the source into consideration at creation/restore phases.
The fix here is quite simple, as can be seen:
configurable:true
to be able to redefine and/or delete this prop from the object later onDetails that need to be fixed for clarity (now moved into #2238)
I will try to create a different issue for this, but there are several things I found out while researching this that needs fixing.
Why do we not always set
configurable: true
?Looking at the git log I found #1456 and #1462 which basically changed
true
to usefalse
if that attribute was already set tofalse
. Why does that make a difference? Well, as long as the prop is writableObject.defineProperty
will only throw ifconfigurable
isfalse
AND any of the other fields change their value. So we can doObject.defineProperty(obj, prop, {value:42})
on a non-configurable prop, as the other fields will be left alone.Why do we tread
enumerable
differently thanconfigurable
?Given the above, reasoning, why do we always set
enumerable
to true? Should it now be subjected to the same handling asconfigurable
? Yes it should have been treated in the same manner and in fact, trying to dostub.value(42)
on a prop that is neither configurable nor enumerable will throw in Sinon today. As should be apparent from the above section, this does not need to happen! We only need to keep properties the same, if they exist, otherwise, use sensible (permissive) default values.Given that, I think we need to create a separate issue to cover that. A basic fix is to make our own
defineProps
that does this:The reason we need to merge in defaultProps is that all of the listed ones are
false
by default when usingObject.defineProperty
while we usually want them to betrue
(like they are in normal assignment operations).How to verify - mandatory
The original issue had a small code listing that would blow up when run. It works now:
Checklist for author
npm run lint
passes