Skip to content
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

Merged
merged 10 commits into from Feb 23, 2021

Conversation

rcebulko
Copy link
Contributor

This PR:

  • adds support for format string assertion messages (devAssert('Failed to %s', foobar))
  • adds tests for pure-assert.js
  • modifies uses of pureDevAssert/pureUserAssert to import as to make it easy to remove "pure" from name once split is complete

return shouldBeTruthy;

// Substitute provided values into format string in message
const message = Array.prototype.slice
Copy link
Contributor Author

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

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 Show resolved Hide resolved
* @param {*=} opt_6 Optional argument
* @param {*=} opt_7 Optional argument
* @param {*=} opt_8 Optional argument
* @param {*=} opt_9 Optional argument
Copy link
Contributor

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?

Copy link
Contributor Author

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),
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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).

Copy link
Contributor

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

});

it('should fail for falsey values dev', () => {
expect(() => userAssert(false, 'xyz')).to.throw(
Copy link
Contributor

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?

Copy link
Contributor Author

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 Show resolved Hide resolved
@@ -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
Copy link
Contributor

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:

  1. 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.
  2. 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

Copy link
Contributor Author

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

Copy link
Contributor Author

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

Copy link
Contributor

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.

opt_message || 'Assertion failed'
);

throw new errorCls(message);
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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.

* @closurePrimitive {asserts.truthy}
*/
export function pureDevAssert(shouldBeTruthy, message) {
return pureAssertion(Error, shouldBeTruthy, message);
export function pureDevAssert(
Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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.

}

/**
* 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
Copy link
Contributor

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.

Copy link
Contributor Author

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 Show resolved Hide resolved
return shouldBeTruthy;

// Substitute provided values into format string in message
const message = Array.prototype.slice
Copy link
Contributor

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),
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -96,7 +96,7 @@ function getOptions(element, mu) {
.filter(
(el) =>
!closestAncestorElementBySelector(
pureDevAssert(
devAssert(
el.parentElement?.nodeType == 1 && el.parentElement,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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?

Copy link
Contributor Author

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.

test/unit/test-assert.js Outdated Show resolved Hide resolved
rcebulko and others added 3 commits February 23, 2021 10:50
Co-authored-by: Caroline Liu <10456171+caroqliu@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants