Repository navigation
Conditional assertions #7
Description
So, at the moment, using babel-plugin-unassert, I can't write an assertion within a conditional:
if (options.class) assert(root instanceof options.class)
> BABEL_ENV=production ./build.sh
TypeError: ./giraphe.es6.js: Property consequent of IfStatement expected node to be of a type ["Statement"] but instead got null
at Object.validate (/Users/ec/Documents/Code/.local/giraphe/node_modules/babel-types/lib/definitions/index.js:115:13)
at Object.validate (/Users/ec/Documents/Code/.local/giraphe/node_modules/babel-types/lib/index.js:552:9)
at NodePath._replaceWith (/Users/ec/Documents/Code/.local/giraphe/node_modules/babel-traverse/lib/path/replacement.js:214:7)
at NodePath._remove (/Users/ec/Documents/Code/.local/giraphe/node_modules/babel-traverse/lib/path/removal.js:60:10)
at NodePath.remove (/Users/ec/Documents/Code/.local/giraphe/node_modules/babel-traverse/lib/path/removal.js:31:8)
at /Users/ec/Documents/Code/.local/giraphe/node_modules/babel-traverse/lib/path/lib/removal-hooks.js:40:12
at NodePath._callRemovalHooks (/Users/ec/Documents/Code/.local/giraphe/node_modules/babel-traverse/lib/path/removal.js:51:9)
at NodePath.remove (/Users/ec/Documents/Code/.local/giraphe/node_modules/babel-traverse/lib/path/removal.js:25:12)
at PluginPass.visitor.CallExpression.enter (/Users/ec/Documents/Code/.local/giraphe/node_modules/babel-plugin-unassert/index.js:57:37)
at newFn (/Users/ec/Documents/Code/.local/giraphe/node_modules/babel-traverse/lib/visitors.js:310:19)
So, I understand the dangers of messing with code around the actual call to the assert; but this particular one is really, really egregious (assertions occur alone behind a conditional all the time!)
Here's my thought for how to deal with it:
-
By default, if assertion-statement being removed is the sole child of a conditional branch, then remove that conditional branch, and insert the condition-expression as a bare statement (incase it was written to have any side-effects):
if (foo(bar)) assert(widget); # becomes foo(bar); -
And provide an option to entirely remove conditionals whose only contents are an assertion (i.e. “I am acknowledging that I know that I need to write my conditional-assertions to have no side-effects, or to include a second statement within their body if they do.”
If this is welcome, I'm happy to write a few failing test-cases and pull-request them. (=
Activity
twada commentedon May 17, 2016
@ELLIOTTCABLE Thank you for reporting and good catch!
I agree with you that some conditionals have side-effects and it's risky to remove them.
How about changing them to EmptyStatements?
becomes
or in babel
Yeah thanks! Would you send issue and PR to babel-plugin-unassert?
(Since
unassertis esprima/escodegen based and with a bit tricky implementation)falsandtru commentedon May 17, 2016
Also:
It would be better to be removed or throws semantic errors also in these cases for consistency. Otherwise, users cannot predict unassert behaviors.
twada commentedon May 17, 2016
@falsandtru as is now unassert removes assertions if and only if it is a statement (its parent is an
ExpressionStatement) since some libraries (e.g. jQuery #4) use its originalassertfunction for not standard (for the nameassertwe imagine) way.In your case,
ConditionalExpression, author would be usingassertas a original function that returns value and the code would be an assignment.unassertshouldn't remove it in such a case.The easiest way to detect which call is to remove is, the case when
assertcall is a single expression. Not in conditionals, No return values.By the way, you must be using unassert-cli and I'm glad to see it's so useful for one-liner explanation :)
ELLIOTTCABLE commentedon May 18, 2016
So, I've put … rather a lot of thought into this today; and I've come to the following conclusion:
There's no good way to do this by default.
I think, whatever you do, it's going to be dangerously surprising in a not-inconsequential number of edge-cases. In situations like this, the only good user-experience, is a combination of requiring intervention, and educating the user; so here's my current suggestion:
assert(foo)in problematic context → retain context, and insert configured replacement for the assert itself (console.log('Disabled assert:foo'), as a random, terrible example?)assert(foo)in problematic context → discard the assert, replace with EmptyStatement orvoid 0-style expression, as appropriate, but retain context (if side-effects are desired)assert(foo)in problematic context → discard the assert and parent-statement (or enclosing expression, in an expression-context)I know that sounds drastic, but I really think it's necessary for a pleasant UX:
unassertneeds to establish with the user a contract as to how it will handle asserts as early as possible. A good starting-point is mentioning some of this in the README (yes, I do think this is that important of an issue :P, it's a very common pattern.), followed by an error if they don't follow the README for their particular interface, and fail to configure one of the above behaviours.(Once the user has established which one they wish to use, then it's up to them to maintain that; and it becomes something
unassertdoesn't have to worry about anymore. i.e. if they choose ‘discard’, and then have conditionals with side-effects … well, that's on them. Y'know what I'm saying?)ELLIOTTCABLE commentedon May 18, 2016
(Clarification: The reason I think this is such a big deal, is because we're literally talking about code differences between development/testing and production. That's a pretty serious opportunity for subtle bugs, if Enormous Effort isn't expended educating the developer as exactly what will change.)
twada commentedon May 20, 2016
@ELLIOTTCABLE Thank you for clarification.
unassertrequires more documentation. Right.unassertremovesassertcalls that look like assertions in terms of Assertion (software development) - Wikipedia.In terms of ECMAScript AST, a single
CallExpressionwrapped byExpressionStatement.That is not assignment, not an argument of other calls, etc.
Your case, if statement without curly braces, is a few edge case that
CallExpressionis not wrapped byExpressionStatement.CallExpressionis a immediate child ofIfStatement.parsed AST of code above is not the same as code below
In a few cases like that, I'll go with your "Retain" strategy. It's a bugfix.
BTW, It's nice to have to fail first at compile time. So I'll consider implementing it too.
Thanks.
ELLIOTTCABLE commentedon May 20, 2016
Well, I don't see it as the same. This is pretty important, because
runtime-heavy tasks can occur in that conditional; and conditional
assertions (distinct from ‘assertions that happen to be inside a
conditional’) can be heavy like that.
Here's a real-world example of my own: I've a directed-cyclic-graph
consuming library; and when exercised by my tests, I want something like
the following:
Such methods already exist, but have a high runtime-complexity: say the
actual method under test is
O(n²)-complex; with the assertions, it'sO(2n + n²)(which is fine) … but if I'm depending on unassert toproduction-ify my code, it's Very Bad if half of that assertion gets left
in (so now my production method is
O(n + n²)).Now, some engines will optimize-away an EmptyStatement conditional that
they can statically-analyze to have no side-effects … but not all, and not
often, and almost certainly not in these problematic cases I'm mentioning
(you think Graph::isSomeComplicatedGraphState is going to be an
easily-analyzed inlineable? I doubt it :P)
This is a really common pattern in assertion-based development (I seem to
remember a really neat blog-post that broke the experience down into two
real-world 'flavours ' of assertions; the runtime-simple, and the
runtime-complex, like this one); and I don't think it's out-of-line to ask
for better support! :O
On Thu, May 19, 2016 at 8:48 PM Takuto Wada notifications@github.com
wrote:
twada commentedon May 20, 2016
IMO, conditional assertions is NOT common.
Writing assertion with conditional is error prone practice and is not effective, performant.
Both Assertion programing paradigm and Design by Contract paradigm do not encourage you to write assertions in conditionals. They (including me, ) use conditional expression inside assertions.
falsandtru commentedon May 23, 2016
I'm always using that style.
ELLIOTTCABLE commentedon May 28, 2016
The problem is, that example will fail: sure, you can short-circuit the runtime complexity of the second function, but at the cost of that assertion actually failing, when the point is that it shouldn't occur at all when node.isSomeComplicatedGraphState isn't the case:
And again, as above, I realize I can wrap
if (state) assert(other.state)into a function, and then assert that function … but that runs into one of two problems, every time:If I preform the actual
assert()within the body of the function, then we've come full-circle to the same problem of the complicated ‘should this assertion even occur’ still occurring at runtime,… but if I
assert()the function-call itself, then power-assert becomes useless, as it receives no metadata whatsoever about the actual assertion:… and in both situations, this means writing external support code for my assertions: in the worst case, each
assert()will now need a multi-line function written somewhere else, maintained somewhere else, and explained somewhere else. (In this example, it probably means exporting some new, unofficial API on thenodetype, y'know?)I suspect I'm simply not making my case well, because … it genuinely seems extremely obvious to me that this is Really Important?
Edit: To be clear, I understand the value of convention-over-configuration; and I also think the behaviour you already implemented / described is the best default behaviour. I disagree with your assertion (ha ha) that this isn't a problem; but if you understand that this is a real thing, that real people need, then we're on the same page … and I totally understand prioritizing the usual case, along with the maintenance-cost of adding configuration options to unassert, over this relatively edge-case situation.
twada commentedon May 28, 2016
@ELLIOTTCABLE Thank you for your suggestion again.
First, I apologize that I misunderstood your intention of what "conditional" means. Now I understood. Yeah it's a real problem sometimes.
Second. I apologize that my explanation is not a nice one. It's not a fair one. I'm so sorry for that.
Now I'm going to find the way to solve this problem.
First I implement your "Retain" strategy since it contains a bugfix to be released soon.
Then I'll tackle with conditional assertions in my way. So please stay tuned.