-
Notifications
You must be signed in to change notification settings - Fork 29.6k
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
assert: enforce type check in deepStrictEqual #10282
Conversation
ecdd024
to
d065c7c
Compare
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 labelled this semver-major
since it looks like a pretty clear behavioural change to me, even if it seems to make a lot of sense from the first look at it.
Fixes #10258.
Could you add a Fixes: https://github.com/nodejs/node/issues/10258
line at the end of the commit message? :)
lib/assert.js
Outdated
return a.source === b.source && | ||
a.global === b.global && | ||
a.multiline === b.multiline && | ||
a.lastIndex === b.lastIndex && |
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.
So, this is not related to the changes you are making here, but I would have a couple of suggestions:
-
I guess you could probably make arguments both for including
lastIndex
here and for leaving it out, and I’m tempted to say “drop it”. I probably wouldn’t expect this behaviour. -
Maybe instead of the
global
,multiline
andignoreCase
checks a single check for.flags
would be a good idea? There are new flags likeu
that would currently be ignored.
(If you think these suggestions make sense, it’s probably best to keep that for a separate commit/separate PR.)
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 suggestion, I think I will just submit another commit in this PR.
lib/assert.js
Outdated
if (ArrayBuffer.isView(actual) && ArrayBuffer.isView(expected) && | ||
getTag(actual) === getTag(expected) && | ||
!(actual instanceof Float32Array || | ||
actual instanceof Float64Array)) { |
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 indentation seems off here?
Also, could you update the instanceof
checks here to use getTag
, too? (probably also best kept for a separate commit or PR)
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, why my make lint
doesn't scream at me about this :/? I will update these indentations, thanks. And the tag checks can go into another commit I think.
|
||
const date = new Date('2016'); | ||
|
||
// See #10258 |
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.
nit: Can I suggest that you update this to use the full URL here?
const fake = new FakeDate(); | ||
|
||
/* eslint-disable no-restricted-properties */ | ||
// We are testing deepEqual! |
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.
Is the eslint-disable tag to that deepEqual
can be used 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.
no-restricted-properties
is the one I get from make lint
, I can't find a specific eslint-disable tag for deepEqual
, do you know what that is?
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.
@joyeecheung Nope, and to be clear, I wasn’t requesting you to change anything here, just making sure that I understood why the line is there in the first place
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.
Oh yes, this disable tag is for all the deepEqual
s below :).
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 it's fine the way you did it, but if you want to get even more granular with the linting options, you can disable the check on just the lines that need it by either using // eslint-disable-line no-restricted-properties
or // eslint-disable-next-line no-restricted-properties
(depending on if you can fit the comment on the same line or if it needs to go on the line right before the assert.deepEqual()
).
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.
Thank you for the tips!
// For deepStrictEqual we check the runtime type, | ||
// then reveal the fakeness of the fake date | ||
assert.throws(() => assert.deepStrictEqual(date, fake), | ||
`AssertionError: ${date} deepStrictEqual Date { }`); |
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 indentation is off here, and there’s a assert.throws
pitfall: You can’t specify a string to check the error message against; only a constructor, RegExp
or function that checks the argument work here. (You can probably just wrap the expression in new RegExp
or something)
(edit: link to docs: https://nodejs.org/api/assert.html#assert_assert_throws_block_error_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.
Sorry for not reading the docs more thoroughly. I'll wrap them up in new RegExp
, thanks!
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.
Sorry for not reading the docs more thoroughly.
@joyeecheung If it helps… that mistake is something found a lot in our own tests, at least until very recently ;)
const date2 = new MyDate('2016'); | ||
|
||
// deepEqual return true as long as the time are the same, | ||
// but deepStrictEqual check properties |
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.
typos: returns
and checks
(here and below)
d065c7c
to
89cdd8f
Compare
89cdd8f
to
df77ce1
Compare
@addaleax Thanks for the review, I've updated this PR, PTAL. |
df77ce1
to
5cdfc0e
Compare
Thanks! This looks pretty good so far, I’ll have time for a full review of this later. CI: https://ci.nodejs.org/job/node-test-commit/6668/ |
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 for doing this! Mostly seems good to me. Not sure if we'd have to "unlock" the API, but (as of right now, at least) I'd be in favor of doing that. Some small comments.
lib/assert.js
Outdated
// equivalent if it is also a Date object that refers to the same time. | ||
} else if (util.isDate(actual) && util.isDate(expected)) { | ||
return actual.getTime() === expected.getTime(); | ||
// 7.2. If both values are Date objects, |
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.
These numbers (7.2
) correspond to the sections of a specification that assert
followed in its initial implementation. http://wiki.commonjs.org/wiki/Unit_Testing/1.0
If we're going to change the algorithm, I'd recommend removing the numbers, since we're not following the spec anymore.
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.
Didn't know this is a thing :O I will remove those numbers. Thanks.
test/parallel/test-assert.js
Outdated
const re1 = /a/; | ||
re1.lastIndex = 3; | ||
assert.throws(makeBlock(a.deepEqual, re1, /a/)); | ||
} |
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.
Instead of removing this test, would it make sense to change it to assert.doesNotThrow
?
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 suggestion. I will update these. Thanks.
test/parallel/test-assert.js
Outdated
{ | ||
const re1 = /a/; | ||
re1.lastIndex = 3; | ||
assert.throws(makeBlock(a.deepStrictEqual, re1, /a/)); |
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.
Same here: assert.doesNotThrow()
?
@Trott Thank you for the review, I have updated this PR, PTAL. |
@nodejs/ctc Would we need to "unlock" the assert module to land this? I think the answer is: "technically yes" but there might be some wiggle room on something like this? |
Added the |
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 if CI is ✅ and CITGM is ✨
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. Maybe read me adding the “semver-major” label more as a precaution than anything else; I am fine considering this a bugfix.
Extend the assert-throws-arguments custom ESLint rule to also check for the use of template literals as a second argument to assert.throws. PR-URL: nodejs#10301 Ref: nodejs#10282 (comment) Reviewed-By: Prince John Wesley <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Roman Reiss <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Italo A. Casas <[email protected]>
Extend the assert-throws-arguments custom ESLint rule to also check for the use of template literals as a second argument to assert.throws. PR-URL: nodejs#10301 Ref: nodejs#10282 (comment) Reviewed-By: Prince John Wesley <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Roman Reiss <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Italo A. Casas <[email protected]>
Extend the assert-throws-arguments custom ESLint rule to also check for the use of template literals as a second argument to assert.throws. PR-URL: #10301 Ref: #10282 (comment) Reviewed-By: Prince John Wesley <[email protected]> Reviewed-By: Jeremiah Senkpiel <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Roman Reiss <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Italo A. Casas <[email protected]>
lib/assert.js
Outdated
return true; | ||
} | ||
// Notes: Type tags are [[Class]] properties returned by | ||
// obj->class_name() in C++ or Object.prototype.toString.call(obj) in JS |
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.
[[Class]]
is gone in ES6, and obj->class_name()
doesn't seem to be part of the public V8 API. I don't think we really need to mention them.
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 mentioned obj->class_name()
because that looks like it's what util.isDate()
(Value::IsDate
) and alike come down to in a macro. Also it's why process
and a lot of other objects created in the C++ land has their own tags, although after a quick search looks like they actually use FunctionTemplate::SetClassName()
to set this, and never use the getter, so probably we can just mention FunctionTemplate::SetClassName()
would alter this.
[[Class]]
is described as "historical" in another PR, just as it is in the spec note.
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 left a few comments, but in general, it'd be awesome if new code in node core was robust against user modification of the language builtins.
lib/assert.js
Outdated
} | ||
|
||
function isArguments(tag) { | ||
return tag == '[object Arguments]'; |
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.
why is this ==
and a few lines above, ===
? I assume this should be ===
?
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, it's mistake, thanks for catching that.
lib/assert.js
Outdated
// See https://tc39.github.io/ecma262/#sec-object.prototype.tostring | ||
// for a list of tags pre-defined in the spec. | ||
// There are some unspecified tags in the wild too(e.g. typed array tags). | ||
const actualTag = Object.prototype.toString.call(actual); |
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.
For the sake of robustness, please cache Object.prototype.toString
at module level, and don't rely on nobody having monkeyed with Object.prototype
at runtime :-)
lib/assert.js
Outdated
// If both values are Date objects, | ||
// check if the time underneath are equal first. | ||
if (util.isDate(actual) && util.isDate(expected)) { | ||
if (actual.getTime() !== expected.getTime()) { |
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.
It's safer to cache Date.prototype.getTime
at module level, and .call
it here - rather than relying both on .getTime
not being shadowed on these two objects, and on Date.prototype
being unbroken.
lib/assert.js
Outdated
// Here compare returns true, so actual.length === expected.length | ||
// if they both only contain numeric keys, | ||
// we don't need to exam further | ||
if (Object.keys(actual).length === actual.length && |
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.
Similarly, Object.keys
should be cached at module level, not runtime-looked-up on Object
This is blocked by #11128 |
Refactors _deepEqual and fixes a few code paths that lead to behaviors contradicting what the doc says. Before this commit certain types of objects (Buffers, Dates, etc.) are not checked properly, and can get away with different prototypes AND different enumerable owned properties because _deepEqual would jump to premature conclusion for them. Since we no longer follow CommonJS unit testing spec, the checks for primitives and object prototypes are moved forward for faster failure. Improve regexp and float* array checks: * Don't compare lastIndex of regexps, because they are not enumerable, so according to the docs they should not be compared * Compare flags of regexps instead of separate properties * Use built-in tags to test for float* arrays instead of using instanceof Use full link to the archived GitHub repository. Use util.objectToString for future improvements to that function that makes sure the call won't be tampered with. Refs: nodejs#10282 (comment) Refs: nodejs#10258 (comment)
Refactors _deepEqual and fixes a few code paths that lead to behaviors contradicting what the doc says. Before this commit certain types of objects (Buffers, Dates, etc.) are not checked properly, and can get away with different prototypes AND different enumerable owned properties because _deepEqual would jump to premature conclusion for them. Since we no longer follow CommonJS unit testing spec, the checks for primitives and object prototypes are moved forward for faster failure. Improve regexp and float* array checks: * Don't compare lastIndex of regexps, because they are not enumerable, so according to the docs they should not be compared * Compare flags of regexps instead of separate properties * Use built-in tags to test for float* arrays instead of using instanceof Use full link to the archived GitHub repository. Use util.objectToString for future improvements to that function that makes sure the call won't be tampered with. PR-URL: #11128 Refs: #10282 (comment) Refs: #10258 (comment) Reviewed-By: James M Snell <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: Michaël Zasso <[email protected]>
@ChALkeR Yeah this is not blocked anymore. Thanks! Did a rebase locally, haven't decided about #10282 (comment), I'll push it later. |
2dabb67
to
3efc514
Compare
Rebased with documentation and a few more test cases (for faked |
Add checks for the built-in type tags to catch objects with faked prototypes. See https://tc39.github.io/ecma262/#sec-object.prototype.tostring for a partial list of built-in tags. Fixes: nodejs#10258
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.
Still LGTM :)
|
||
1. Primitive values are compared using the [Strict Equality Comparison][] | ||
( `===` ). | ||
2. [`[[Prototype]]`][prototype-spec] of objects are compared using | ||
the [Strict Equality Comparison][] too. | ||
3. [Type tags][Object.prototype.toString()] of objects should be the same. |
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 can’t find the definition of [Object.prototype.toString()]
… am I missing something?
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.
Uh, forgot to put the reference link below :P
doc/api/assert.md
Outdated
@@ -141,6 +142,25 @@ assert.deepEqual({a: 1}, {a: '1'}); | |||
assert.deepStrictEqual({a: 1}, {a: '1'}); | |||
// AssertionError: { a: 1 } deepStrictEqual { a: '1' } | |||
// because 1 !== '1' using strict equality | |||
|
|||
// The following objects don't have owned properties |
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.
typo: own properties
?
@addaleax Thanks for the review, fixed the typo and the missing link. |
The last CITGM run is also a bit old, so: CITGM: https://ci.nodejs.org/view/Node.js-citgm/job/citgm-smoker/641/ |
Went ahead and landed this in efec14a :) |
Add checks for the built-in type tags to catch objects with faked prototypes. See https://tc39.github.io/ecma262/#sec-object.prototype.tostring for a partial list of built-in tags. Fixes: #10258 PR-URL: #10282 Reviewed-By: Rich Trott <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
Add checks for the built-in type tags to catch objects with faked prototypes. See https://tc39.github.io/ecma262/#sec-object.prototype.tostring for a partial list of built-in tags. Fixes: nodejs#10258 PR-URL: nodejs#10282 Reviewed-By: Rich Trott <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: James M Snell <[email protected]>
For a write-up on this PR, see #10282 (comment)
The less-breaking part of this PR has been moved to #11128
Checklist
make -j4 test
(UNIX), orvcbuild test nosign
(Windows) passesAffected core subsystem(s)
assert, test
Description of change
Add checks for the built-in tags and refactor _deepEqual,
most user-land modules do this for a strict, deep comparison.
nodejs/node-v0.x-archive#7178 already added
this check for argument objects, even in non-strict tests.
Fixes #10258.
Docs updates are postponed due to #7815.
See https://tc39.github.io/ecma262/#sec-object.prototype.tostring
for a partial list of the tags.