-
-
Notifications
You must be signed in to change notification settings - Fork 698
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
Remove proxy frames from stack traces and improve docs/tests #884
Conversation
@@ -58,7 +58,7 @@ var call = Function.prototype.call, | |||
* @api public | |||
*/ | |||
|
|||
module.exports = function (ctx, name, method, chainingBehavior) { | |||
module.exports = function addChainableMethod(ctx, name, method, chainingBehavior) { |
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'm a big fan of naming every function even if they're being assigned to a variable.
Bonus style points for this excellent practice.
// this assertion has been overwritten since overwriting a chainable | ||
// method merely replaces the saved methods in `ctx.__methods` instead | ||
// of completely replacing the overwritten assertion. | ||
flag(this, 'ssfi', chainableMethodWrapper); |
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.
Just out of curiosity, I've always wondered what does ssfi
means, I'm not sure this is a good name since it's kind of "cryptic" (or maybe it just feels like it because I don't know what it means).
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.
According to this comment, it's "start stack function indicator". Also mentioned on http://chaijs.com/guide/plugins/. I don't feel strongly one way or another about renaming it.
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.
Hmm, since it's pretty specific I think there's nothing we can do about it.
Let's leave it that way.
Maybe a comment indicating what it means somewhere would also be nice, especially if we added a link to that comment, which was really useful for me when understanding how the whole stack trace manipulation works.
@@ -64,7 +64,7 @@ module.exports = function proxify (obj, nonChainableMethodName) { | |||
} | |||
} | |||
|
|||
return target[property]; | |||
return Reflect.get(target, 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.
FYI, Reflect.get
is not different from regular computed access (unless you are using receiver
parameter).
It also calls [[Get]]
and ToPropertyKey
. The only thing they are different is that Reflect
methods throw on non-object target
(however target
here will always be an object). Same goes for Reflect.has
above: in
does the same thing. We can simplify checks and code by removing Reflect
usages.
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 by using Reflect
we get more semantic code, for me it just feels like a good practice and a more functional way to invoke the language's internal operations.
However, since it won't have any impact and it's just a matter of personal preference I'd be happy with either options.
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 have much of a preference either way. Anyone have a strong preference 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 have an idea on how to improve proxies performance by much, utilizing third parameter to Reflect.get
, so I think we should leave it like this.
PS: much pleasure to review such careful commits 👍
// The `keep_ssfi` flag is set so that if this assertion ends up calling | ||
// the overwritten assertion, then the overwritten assertion doesn't attempt | ||
// to use itself as the starting point for removing implementation frames | ||
// from the stack trace of a failed assertion. | ||
flag(this, 'keep_ssfi', true); |
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.
Let me see if I've got this right, I'm not sure if I fully understand what this does.
So, we use the SSFI flag to store the function which should indicate where the real stack trace will start in order to remove internal implementation details, right?
Whenever an assertion gets overwritten we must turn the keep_ssfi
flag to true before calling the old assertion (_super
) in order to avoid that assertion being used as the start of the stack trace. Right?
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.
Right, so the function stored in the ssfi
flag serves as the second parameter for Error.captureStackTrace
, meaning that the function itself, and all functions called after it (which are just the internals of Chai and plugins), will be removed from the stack trace if an error is thrown. In the case of property assertions, this function is actually the proxy getter, which is why this PR was needed. But in the case of method assertions, the proxy getter function completely returns before the method assertion's function is invoked, so the method assertion's function needs to be stored in the ssfi
flag instead of the proxy getter.
But what about all functions that came before the function stored in the ssfi
flag? Usually, the first function that comes before it is the function that the user passed as the second argument to a Mocha it
invocation. This function contains the user's assertion that failed, so it's good that it's included in the stack trace. But there's a bunch of other functions still in the stack before this one: Mocha's internals. It turns out that these don't appear in the stack traces either because Mocha does its own manual filtering of the stack trace to get rid of their internals. It's important to remember this when troubleshooting an issue with Mocha.
(Note that because the assert
interface acts as a wrapper around Chai assertions, there's currently an extra function call between the user's assertion and the one stored in the ssfi
flag, so it shows in the stack trace. That's what #878 is about. It's fixable but will take some work on the assert
interface.)
As for your final question, yes, the keep_ssfi
flag is needed so that the overwriting function remains stored in the ssfi
flag even after it proceeds to call the original function that it overwrote. The first function after the user's assertion is the one that needs to be in the ssfi
flag in order for this to all work correctly.
This LGTM! I wasn't really aware of how this whole Stack Trace manipulation worked, so I did a bit of research and I found it to be very interesting. Just in case anyone wants to read more about it, take a look at the I made a few comments on the source just to make sure I got it right and added a little consideration to improve the readability of the code regarding the Awesome job @meeber! 😄 |
Pushed another commit:
|
@meeber awesome job! Chai's code is becoming even more easy and pleasurable to read than it was before with all these useful and well written comments. I'm in love with this codebase ❤️ |
Hi friends, sorry for pinging everyone right after holidays, but I've seen we have many open Pull Requests so I thought it would be a good idea for us to start reviewing and approving in order to avoid accumulating too much work. Also, can anyone make sure LGTM is working? I remember talking about abandoning it in favor of github's review system. What do you think? |
Awesome changes. Makes code so much clearer. LGTM. |
@meeber think you could resolve the conflicts for this, so we can merge. All LGTM 😄 |
Currently, only one module needs to detect if Chai's proxy protection is enabled. However, upcoming changes will involve performing this detection in other modules as well. This commit moves the detection logic to its own utility module for easy reuse.
The proper way to perform an operation's original behavior from within a proxy trap is by using `Reflect`.
Many of the utility functions had slightly misleading names or no names at all. This commit renames the functions with misleading names and adds names to functions that were missing one.
Only a couple of types of assertions were being tested for correct stack traces. This commit cleans up the existing tests and adds tests for the missing assertion types.
Proxy-related implementation frames were showing up in the stack traces for failed property assertions. This commit removes them by setting the proxy getter (instead of the property getter) as the starting point to remove all implementation frames.
Only the `expect` interface was being tested for correct stack traces. This commit adds identical tests for the `should` interface.
There was some dead code leftover from before `includeStack` was made into a config value (as opposed to existing as a property on the Assertion object). This commit removes that dead code, and adds inline documentation for the remaining stack-related code.
- Rename third parameter of Assertion constructor from `stack` to `ssfi` for consistency's sake. - Add documentation to Assertion constructor explaining what the `object`, `message`, and `ssfi` flags are for.
@keithamus Rebased and resolved conflicts! |
This PR is intentionally split into a number of commits, each one of which has a description within the commit message.
Highlights of this PR:
Note that this doesn't address #878 regarding extra frames in stack traces when using the
assert
interface.