-
-
Notifications
You must be signed in to change notification settings - Fork 701
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
Throw error when instanceof is passed something that is not a function as constructor #899
Throw error when instanceof is passed something that is not a function as constructor #899
Conversation
Despite this throwing an error, I assume it's not a breaking change? Just makes the error message a user would receive more descriptive? |
@keithamus It was already throwing an error before, which was:
I think it's important to tell the user which type was passed in order for him to be able to fix what went wrong. Also, whenever passing |
lib/chai/core/assertions.js
Outdated
@@ -988,6 +988,13 @@ module.exports = function (chai, _) { | |||
|
|||
function assertInstanceOf (constructor, msg) { | |||
if (msg) flag(this, 'message', msg); | |||
var isFunction = constructor instanceof Function; | |||
|
|||
if (!isFunction) { |
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 think we can do
var type = typeof constructor;
if (typeof !== 'function') {
throw new Error ...
}
Maybe instead of var type = constructor === null ? 'null' : typeof constructor;
we can use type-detect
here?
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 think that it would be an overkill. In this case we just need to know whether something is a function or not in order to see if it is a constructor, there's no need for anything more complex than that.
Please let me know if I forgot any edge case, but I think we should go native if we can.
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.
Oops, speaking of edge cases, typeof
is not sufficient here.
instanceof
works not only with functions, but with any objects that have @@hasInstance
.
77ae937
to
01b1485
Compare
lib/chai/core/assertions.js
Outdated
@@ -988,6 +988,12 @@ module.exports = function (chai, _) { | |||
|
|||
function assertInstanceOf (constructor, msg) { | |||
if (msg) flag(this, 'message', msg); | |||
var constructorType = constructor === null ? 'null' : typeof constructor; |
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.
The most hardcore option is
function assertInstanceOf (constructor, msg) {
if (msg) flag(this, 'message', msg);
if (constructor !== Object(constructor)) throw new TypeError();
var object = flag(this, 'object');
var hasInstance = constructor[Symbol.hasInstance];
var isInstance;
if (hasInstance == null) {
if (typeof constructor != 'function') throw new TypeError();
isInstance = Function[Symbol.hasInstance].call(constructor, object);
} else if (typeof hasInstance == 'function') {
isInstance = hasInstance.call(constructor, object);
} else {
throw new TypeError();
}
var name = _.getName(constructor);
this.assert(isInstance
, 'expected #{this} to be an instance of ' + name
, 'expected #{this} to not be an instance of ' + name
);
};
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've reimplemented part of instanceof
algo to avoid getting @@hasInstance
twice.
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.
More concise option
function assertInstanceOf (constructor, msg) {
if (msg) flag(this, 'message', msg);
var validInstanceOfTarget = constructor === Object(constructor) && (
typeof constructor == 'function' ||
typeof constructor[Symbol.hasInstance] == 'function'
);
if (!validInstanceOfTarget) throw new TypeError();
var name = _.getName(constructor);
this.assert(
flag(this, 'object') instanceof constructor
, 'expected #{this} to be an instance of ' + name
, 'expected #{this} to not be an instance of ' + name
);
};
But this one calls constructor.[[Get]](@@hasInstance)
twice. [[Get]]
may be observable.
Agreed with @shvaikalesh. Whenever Example: var FakeConstructor = {}; // Not a function, but it has `Symbol.hasInstance` defined:
FakeConstructor[Symbol.hasInstance] = function (val) {
return val === 3;
};
var fakeInstanceA = 3;
// Should pass even though FakeConstructor isn't a function
expect(fakeInstanceA).to.be.an.instanceof(FakeConstructor);
var fakeInstanceB = 4;
// Should fail because 4 !== 3, but what does failed assertion error message use as "name"?
expect(fakeInstanceB).to.be.an.instanceof(FakeConstructor); |
Yet another thing to consider: if |
Good points from @shvaikalesh - we should handle all these cases. I suppose we could just catch any |
@shvaikalesh I'm not sure I get it. Can't we just use Can you please describe with more detail? Also, thanks everyone for your feedback! |
@lucasfcosta We can check for With all this, I agree with @keithamus that the most simple, reliable and future-compatible way is to wrap |
Thanks for the detailed explanation! Any suggestions on what the error message should be? I was reading the specs and according to the algorithm I can only think of:
But I think that's not explicit enough and I'm not sure it is a good idea to talk about internal implementation details on error messages. Please let me know if you can think of anything better. Also, if anyone wants to read about how |
instance instanceof constructor may fail, if
Many reasons here; we would have to reimplement ${instance} instanceof ${constructor} failed: ${error.message} ${error.stack} is the way to go. |
Well, we already cover some of these cases such in our However, your suggestions look awesome. Thanks! |
01b1485
to
2c4fc89
Compare
Hey friends, here it is. I also added a test to check if the error messages are being passed to our own error when Let me know if you have any further suggestions. |
lib/chai/core/assertions.js
Outdated
} catch (err) { | ||
throw new Error(target + ' instanceof ' + constructor + ' failed: ' + err.message + '.') | ||
} | ||
|
||
var name = _.getName(constructor); |
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.
Slight issue with error message wording in rare circumstances.
Example:
var FakeConstructor = {};
FakeConstructor[Symbol.hasInstance] = function (val) {
return val === 3;
};
var fakeInstanceB = 4;
expect(fakeInstanceB).to.be.an.instanceof(FakeConstructor);
Current output:
AssertionError: expected 4 to be an instance of undefined
Output after chaijs/get-func-name#20 is merged:
AssertionError: expected 4 to be an instance of null
I'm thinking that if the result of getName
is null
, then the best thing we can do is just print out the constructor
object; null
will be meaningless/misleading to the end user. In this case, that'd be:
AssertionError: expected 4 to be an instance of [object Object]
Not particularly helpful, but at least it's not misleading. An alternative would be to say "of an unnamed constructor".
AssertionError: expected 4 to be an instance of an unnamed constructor
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.
Well noticed!
This change is ready. I'll push it to this branch as soon as we release get-func-name
with that new change so we can guarantee our travis builds will succeed.
2c4fc89
to
e8d25c2
Compare
This has just been updated to include the changes you requested. Now everything should be working as expected and this also includes the new version of get-func-name, which has just been released. |
lib/chai/core/assertions.js
Outdated
var target = flag(this, 'object') | ||
var validInstanceOfTarget = constructor === Object(constructor) && ( | ||
typeof constructor === 'function' || | ||
(typeof Symbol !== 'undefined' && typeof constructor[Symbol.hasInstance] === 'function') |
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.
Are there any edge cases (environments or polyfills) in which Symbol
is defined but Symbol.hasInstance
isn't? Safe thing to do here is check that it isn't undefined either.
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.
Good catch!
e8d25c2
to
fb2f28d
Compare
Looks like Node v4 has failing tests. |
fb2f28d
to
1841618
Compare
@meeber done! |
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.
LGTM
lib/chai/core/assertions.js
Outdated
try { | ||
isInstanceOf = target instanceof constructor | ||
} catch (err) { | ||
throw new Error(target + ' instanceof ' + constructor + ' failed: ' + err.message + '.') |
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.
Stack trace of original error message is lost here.
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 realize we're going a bit full-circle here, but after further consideration I think that when the user's code itself is potentially responsible for the error being thrown, we should just pass along that error (original error object, stack trace, and message) instead of attempting to make it friendlier.
Conversely, when it's Chai that is purposefully throwing the error, then it needs to be an AssertionError
that properly passes along the ssfi
flag as well as respecting the user's custom failed assertion message. #922 handles the ssfi
part, and a small follow-up PR will handle the custom message part.
In this particular case, the user could potentially have some custom crap defined via @@hasInstance
which is responsible for an error being thrown. Therefore, I think we should pass along that error object as-is, which will be done automatically if we just remove the whole try
/catch
.
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.
As @meeber noticed, I also think we're going a bit full-circle here. The whole purpose of this PR was to give our users more friendly error messages, but given what was said I'm not sure this is the best choice anymore.
So, what do you think? Should we cancel the Error
related change and keep the other improvements or should we keep it?
I'd like to hear more opinions about that.
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.
To be clear, I like all the changes except the part where we catch an error that could possibly have been thrown by the user's code (if they had a custom @@hasInstance
that was responsible for the error). If we remove that change but keep the rest, I think we still come out ahead, and the changes we keep still satisfy the original problem from #893.
|
||
if (!validInstanceOfTarget) { | ||
var constructorType = constructor === null ? 'null' : typeof constructor; | ||
throw new Error('The instanceof assertion needs a constructor but ' + constructorType + ' was given.'); |
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.
TypeError
fits better
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.
After this was merged, I was gonna rebase #922 and turn this error into an AssertionError
that properly retains ssfi
(and then a follow up PR will properly retain the user's custom message).
lib/chai/core/assertions.js
Outdated
var validInstanceOfTarget = constructor === Object(constructor) && ( | ||
typeof constructor === 'function' || | ||
(typeof Symbol !== 'undefined' && typeof Symbol.hasInstance !== 'undefined' && | ||
typeof constructor[Symbol.hasInstance] === 'function') |
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.
[[Get]]
is called on @@hasInstance
twice: here and in instanceof
. What if it is observable?
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 guess the question is: Is there an implicit understanding that Chai, in the process of performing an instanceof
assertion, will only trigger @@hasInstance
once? Or is it allowed to trigger it as much as it wants?
The benefit of not worrying about calling it multiple times is that the code is less complex. The cost is that to test an object with an observable @@hasInstance
, you'll have to manually use JavaScript's instanceof
and then assert on the result of that, rather than using Chai's builtin instanceof
assertion.
My gut feeling is that having an observable @@hasInstance
is such an extreme edge case that it's not worth Chai jumping through any hoops to accommodate, especially given the easy workaround of invoking instanceof
manually and asserting on the result. But it doesn't add too much complexity to accommodate it either, so I won't protest if we want to do it.
Anyone have strong feelings one way or another?
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 think that if it's easier to accomodate that we really should do it.
It's better to perform this work once and in our side than passing the burden of having to work around this to our users every time they need it.
I'll make sure to update this PR as soon as I get more input on the other comment I've just made.
Also, sorry for the delay on this PR, I've been very busy because recently I've been working some extra hours because I'm going to travel at the end of this month.
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.
Sounds good to me. See #899 (comment) for ideas on only triggering @@hasInstance
once.
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.
@meeber I can trigger the getter for @@hasInstance zero times if I use the in
operator.
I also removed the try/catch
statements on this commit.
What do you think about this?
Is there anything I should improve?
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.
Using the in
operator seems like a good strategy; that way, @@hasInstance
is only triggered once when the actual instanceof
call is made.
1841618
to
0118153
Compare
throw new Error('The instanceof assertion needs a constructor but ' + constructorType + ' was given.'); | ||
} | ||
|
||
var isInstanceOf = target instanceof constructor |
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.
Semi-colon :D
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.
Thanks!
I miss linting tools 😅
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.
One missing semi-colon isn't enough to hold this up for a month, so if you can fix it @lucasfcosta, great, but if you're too busy, no problem and LGTM.
Agreed, we need to get some of these outstanding PRs pushed in before 4.0 final, otherwise they'll only be held up for many more months before 5.0 |
This aims to solve #893 and chaijs/get-func-name#20 is related to this issue.
Whenever passed something that is not an instance of a function, the
instanceof
assertion will now throw an error informing the user that he should pass a constructor to this assertion, but instead he has passed<type>
I also added tests for this in all three interfaces.
As I've said on the other PR an
instanceof
check is enough to determine whether or not something is a function, even though the@@hasInstance
symbol exists.Please let me know if I forgot anything.
EDIT: I have also started using the
typeof
check due to this explanation by @shvaikalesh.