-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
♻️ Fixit: standalone assert helpers w/ format messages #32788
Conversation
src/pure-assert.js
Outdated
return shouldBeTruthy; | ||
|
||
// Substitute provided values into format string in message | ||
const message = Array.prototype.slice |
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.
There is one thing this does not preserve: logic which detects if an element is passed and transforms the resulting error message to contain extra element info, adds associatedElement
property, adds fromAssert
property, etc. This felt AMP-centric and not necessarily needed for a standalone helper. Can add back in now or down the line if deemed necessary
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.
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.
That might actually have significant value for useAssert
. But definitely has zero value for devAssert
. Another reason why they are not all that similar.
src/pure-assert.js
Outdated
* @param {*=} opt_6 Optional argument | ||
* @param {*=} opt_7 Optional argument | ||
* @param {*=} opt_8 Optional argument | ||
* @param {*=} opt_9 Optional argument |
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 there a reason there are exactly 9
optional arguments? Could this be a {...*}
like for pureAssertion
above?
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.
Per JSDoc: var args as individual params for better inlining at compile-time
@@ -57,9 +60,9 @@ export function getDate(value) { | |||
/** Map from attribute names to their parsers. */ | |||
const dateAttrParsers = { | |||
'datetime': (datetime) => | |||
pureUserAssert(parseDate(datetime), `Invalid date: ${datetime}`), | |||
userAssert(parseDate(datetime), 'Invalid date: %s', datetime), |
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.
FMI: Why is this format preferred to template literals?
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.
There is partial work (according to @erwinmombay it's mostly complete) around having Babel remove all assertion strings, and this would end up generating a URL to an error page, something like https://cdn.ampproject.org/error/1234?v1=${datetime}
. Error strings themselves get compiled out and replaced with error codes, saving some bytes along the way. I don't know where the I2I or docs around this are at the moment.
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.
That sort of makes sense. But another reason why this is useful: we avoid constructing the error string if userAssert
does not throw an error. And we expect that throwing an error here would be an exception (pun semi-intended).
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 believe we supported both forms, you just couldn't mix %s
inside a template literal. @alanorozco
test/unit/test-pure-assert.js
Outdated
}); | ||
|
||
it('should fail for falsey values dev', () => { | ||
expect(() => userAssert(false, 'xyz')).to.throw( |
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.
Do these not need to be wrapped with allowConsoleError
?
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 written, the assert helpers don't console log, they just throw the error. It wasn't clear to me that it was logged before, but inspecting further I see it is. console.assert()
could easily be used here, but do we think this is still necessary if the assertion throws an errer?
Since this already throws an error, that error is either a) unhandled, and reaches console anyway or b) caught and handled, in which case it would make sense for the catching code to handle/optionally log or swallow the error. Thoughts?
src/pure-assert.js
Outdated
@@ -55,40 +55,118 @@ export class UserError extends Error { | |||
* Throws a provided error if the second argument isn't trueish. | |||
* @param {Object} errorCls | |||
* @param {T} shouldBeTruthy | |||
* @param {string} message | |||
* @param {string} opt_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.
I'd just name the file src/assert.js
. Another consideration here: I'd also consider the following separation that might work better for us:
dev.js
- all dev asserts and other utils. This whole module is completely stripped from the PROD - nothing remains at all. Easy to check that this module is not part of any compilation.user.js
- all user-related utils to assert/log.
I guess what I'm really wondering here: why try to keep devAssert
and userAssert
together. Besides the fact that they both throw exceptions, is there anything else that they share? Another benefit: if we focus on devAssert
we might be able to get through all of them quicker then userAssert
and co.
/cc @jridgewell
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.
re: renaming, I was planning to do that in a follow-up, but it probably makes sense to do sooner rather than later. Will add 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'd just name the file
src/assert.js
Done
re: alternate split, it's not clear to me how much dev.js/user.js would own. I'd almost rather see something like a closure compiler comment/directive like @devOnly
that lets helpers live in the appropriate module with related logic. Curious to hear Justin's thoughts
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 critical part is devAssert
is basically covering for cases that compiler is not sufficient. When compiler has done its job, the devAsserts
should disappear as they do now. I think this is a lot better than @devOnly
since it's still a real code, just not something we want in the prod binary.
src/pure-assert.js
Outdated
opt_message || 'Assertion failed' | ||
); | ||
|
||
throw new errorCls(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.
Just to clarify: having a special UserError
class means relatively little because the errors are caught and rethrown all the time, sometimes by the code that we do not control, so the message testing is the only fully reliable way that we've found.
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.
Ack; expectation here was Bento code may throw UserError
directly if we do not re-create user.error()
structure. Will keep an eye on how this is used as error handling comes into 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.
This is absolutely ok to throw a UserError
by our code. Just making sure we're not overly reliant on it downstream. E.g. for a reporter !(error instanceof UserError)
might still be a user error because someone has rethrown it weirdly.
src/pure-assert.js
Outdated
* @closurePrimitive {asserts.truthy} | ||
*/ | ||
export function pureDevAssert(shouldBeTruthy, message) { | ||
return pureAssertion(Error, shouldBeTruthy, message); | ||
export function pureDevAssert( |
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.
IMHO we should just export devAssert
name. Also, let's try to replace all instances of devAssert
with this one ASAP. Doesn't have to be this PR, but ideally this week we could switch completely to this version.
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.
Plan (largely implemented in a handful of local branches already):
- make signatures match versions from
log.js
- update code to
import {pureDevAssert as devAssert}
etc in a handful of PRs - remove versions from
log.js
, rename removing "pure" part, find-replace all references
With the goal of making the latter two steps less likely to complicate merges during Fixit
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.
Any number of steps is ok. As long as we have an overall plan.
src/pure-assert.js
Outdated
} | ||
|
||
/** | ||
* Throws an error if the first argument isn't trueish. Mirrors devAssert in | ||
* src/log.js. | ||
* @param {T} shouldBeTruthy | ||
* @param {string} message | ||
* @param {string} opt_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.
We should also move devAssertElement
here - it's super common and useful. And current dev().assertElement
really bugs me.
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 agree; would like to do this down the line if that's alright, after the two main dev/userAssert helpers are done
src/pure-assert.js
Outdated
return shouldBeTruthy; | ||
|
||
// Substitute provided values into format string in message | ||
const message = Array.prototype.slice |
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.
That might actually have significant value for useAssert
. But definitely has zero value for devAssert
. Another reason why they are not all that similar.
@@ -57,9 +60,9 @@ export function getDate(value) { | |||
/** Map from attribute names to their parsers. */ | |||
const dateAttrParsers = { | |||
'datetime': (datetime) => | |||
pureUserAssert(parseDate(datetime), `Invalid date: ${datetime}`), | |||
userAssert(parseDate(datetime), 'Invalid date: %s', datetime), |
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.
That sort of makes sense. But another reason why this is useful: we avoid constructing the error string if userAssert
does not throw an error. And we expect that throwing an error here would be an exception (pun semi-intended).
} | ||
|
||
// Substitute provided values into format string in message | ||
const message = Array.prototype.slice |
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: Is this needed if they just did a template literal in as the 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.
See #32788 (comment)
5681b99
to
d05b755
Compare
@@ -96,7 +96,7 @@ function getOptions(element, mu) { | |||
.filter( | |||
(el) => | |||
!closestAncestorElementBySelector( | |||
pureDevAssert( | |||
devAssert( | |||
el.parentElement?.nodeType == 1 && el.parentElement, |
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.
el.parentElement?.nodeType == 1 && el.parentElement, | |
el.parentElement && el.parentElement.nodeType == 1, |
Not crucial for this PR: Is it me or is it kind of funny this is in this order?
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.
Agreed, very weird! This was introduced by yours truly in #32474 (see original assertion) when rewriting an assertElement
. Nice catch! dropped the el.parentElement
altogether since el.parentElement?.nodeType == 1
captures it all.
Co-authored-by: Caroline Liu <[email protected]>
This PR:
devAssert('Failed to %s', foobar)
)pure-assert.js
pureDevAssert
/pureUserAssert
toimport as
to make it easy to remove "pure" from name once split is complete