Transitioning to strict mode
Le 18/02/2013 11:10, Claus Reinke a écrit :
I'm looking forward to any recommendation you'd have to improve this guide, specifically about the runtime errors where I said something about "100% coverage test suite" and I'm not entirely sure about that.
Talking about 100% coverage and "catching all errors" is never a good combination - even if you should have found an example of where this works, it will be an exception.
There are a couple of things I'm sure of. For instance, direct eval aside (eval needs some specific work anyway because its semantics is changed a lot), if you have 100% coverage, every instance of setting to an undeclared variable will be caught. There is no exception. But I wonder if that's the case for all runtime errors I listed. Otherwise, in general, I agree that a test suite with 100% coverage that pass doesn't mean that the program is correct. Specifically, in the "semantics changes" section, I don't talk about test suites, not even with 100% coverage.
Also, in practice, for large projects, 100% coverage is a fantasy. I know many software contracts are signed agreeing on 80% coverage, because 100% is a lot of work and not even necessary.
What I'm trying to convey in the different sections is the type and amount of work that is necessary to be sure the code works when moved to strict mode.
Then there is the issue of pragmatic concerns (can throw at runtime, can change semantics on old engines), as expressed in this post
scriptogr.am/micmath/post/should-you-use-strict-in-your-production-javascript
To push adoption of strict mode, it might need one or two refinements.
"we definitely don't want those silent bugs to throw runtime errors to
our end users on live websites."
I could not agree more. But when I read this sentence, I can't help thinking "but why would that ever happen?" Transitioning to strict mode does not mean putting "use strict"; at the top of the program and pushing to production. That's the very reason I wrote the guide actually. I'll expand the intro to talk about that. People should run their code locally, test it before pushing to production. If people don't test locally before pushing to production, transitioning to strict mode should be the least of their concerns. Also, gradually transitioning down to the function granularity means that if an error ever slips into production, it's easy to revert just the one function that is not strict-ready yet.
On older browser not running strict mode
That point is a very valid concern (and I should probably expand the guide on this point). I think this point can be summarized by 2 rules:
- Unless you're a language expert and know what you're doing (you don't need that guide anyway), just stay away from things where the semantics is different 1.1) eval 1.2) arguments (unless you're in a case where you'd use ...args in ES6) 1.3) odd cases of dynamic "this" (this in non-constructor/method, primitive values boxed in objects)
- Strict mode doesn't make your code throw (either syntactically or dynamically)
If those 2 rules are followed, the code will run the same in strict and non-strict, no need to worry about it. Developing new code in strict mode will de facto enforce the second rule (assuming people don't want their code to throw as the normal behavior). Only discipline (with the help of a static checker watching for the this/eval/arguments keywords?) will help to follow the first rule.
Does this sounds false to anyone?
Concatenation
If the rules of the previous section are followed the code has the exact same semantics in strict and non-strict. So if the code is not run in the mode it was initially intended for, it won't make any difference.
Thanks for the feedback and the link to the article :-)
On older browser not running strict mode
I was precisely going to write that it is missing an important explicit advice to produce code that runs both under strict and non-strict mode.
- stay away from things where the semantics is different 1.1) eval
Indeed, eval should only be used by experts; ironically, experts try to avoid eval. :-)
1.2) arguments (unless you're in a case where you'd use ...args in ES6)
I would say: Use the "arguments" object only for arguments that are not explicitly named. (And perhaps: use named arguments when possible, although it is more a question of good style than anything else. I guess it is what you meant by "unless you're in a case where you'd use ...args in ES6", but that phrase was a bit confusing for me.)
Talking about 100% coverage and "catching all errors" is never a good combination - even if you should have found an example of where this works, it will be an exception.
There are a couple of things I'm sure of. For instance, direct eval aside (eval needs some specific work anyway because its semantics is changed a lot), if you have 100% coverage, every instance of setting to an undeclared variable will be caught. There is no exception.
Out of curiosity, what does your favorite test coverage tool report for the source below? And what does it report when you comment out the directive?
function test(force) {
"use strict";
function isStrict() { return !this }
console.log(isStrict());
if (!force && (!isStrict() && (doocument="unndefined"))) {
console.log("we don't have lift-off");
} else {
console.log("ready to go!");
// do stuff
}
!isStrict() && console.log(doocument);
}
test(false);
test(true);
Le 18/02/2013 16:48, Claus Reinke a écrit :
Talking about 100% coverage and "catching all errors" is never a good combination - even if you should have found an example of where this works, it will be an exception. There are a couple of things I'm sure of. For instance, direct eval aside (eval needs some specific work anyway because its semantics is changed a lot), if you have 100% coverage, every instance of setting to an undeclared variable will be caught. There is no exception.
Out of curiosity, what does your favorite test coverage tool report for the source below? And what does it report when you comment out the directive? :-p Ok, there are exceptions if your code depends on semantic changes described in the third section of the article (dynamic this/eval/arguments). That's you case with how you define isStrict (dynamic this) So: if your code does not depend on semantic changes, all instances of setting to an undeclared variable will be caught.
So I guess the first thing to do when transitioning to strict mode is getting rid of all the things that result in non-direct error semantic changes (dynamic this/eval/arguments).
Thanks for the feedback,
The guide looks really good. Well done!
One thing I learned when trying to convince others to use strict mode is a tool to help catching the syntax errors. Scanning a large app code by hand is tedious and error prone. This is one of the reasons I built an online validator: esprima.org/demo/validate.html. Maybe that can be useful.
if your code does not depend on semantic changes, all instances of setting to an undeclared variable will be caught.
Just wanted to shake your faith in testing :-) The example code might look unlikely, but real code is more complex and might evolve nasty behavior without such artificial tuning.
You still need more than statement or branch coverage. Otherwise, we might get 100% "coverage" while missing edge cases
function raise() {
"use strict";
if( Math.random()>0.5 || (Math.random()<0.5) && (variable = 0))
console.log(true);
else
console.log(false);
}
raise();
raise();
raise(); // adjust probabilities and call numbers until we get
// "reliable" 100% branch coverage with no errors; then
// wait for the odd assignment to happen anyway, in
// production, not reproducably
Throwing or not throwing Reference Errors is also a semantics change, and since errors can be caught, we can react to their presence/absence, giving another avenue for accidental semantics changes.
Undeclared variables are likely to be unintended, and likely to lead to bugs, but they might also still let the code run successfully to completion where strict mode errors do or don't, depending on circumstances.
Testing increases confidence (sometimes too much so) but cannot prove correctness, only the absence of selected errors.
What I'd like to understand is why likely static scoping problems should lead to a runtime error, forcing the dependence on testing.
If they'd lead to compile time errors (for strict code), there'd be no chance of missing them on the developer engine, independent of incomplete test suite or ancient customer engines. Wouldn't that remove one of the concerns against using strict mode? What am I missing?
Le 18/02/2013 23:29, Claus Reinke a écrit :
Out of curiosity, what does your favorite test coverage tool report for the source below? And what does it report when you comment out the directive? :-p Ok, there are exceptions if your code depends on semantic changes described in the third section of the article (dynamic this/eval/arguments). That's you case with how you define isStrict (dynamic this) So: if your code does not depend on semantic changes, all instances of setting to an undeclared variable will be caught.
Just wanted to shake your faith in testing :-) The example code might look unlikely, but real code is more complex and might evolve nasty behavior without such artificial tuning.
You still need more than statement or branch coverage. Otherwise, we might get 100% "coverage" while missing edge cases
function raise() { "use strict"; if( Math.random()>0.5 || (Math.random()<0.5) && (variable = 0)) console.log(true); else console.log(false); }
raise(); raise(); raise(); // adjust probabilities and call numbers until we get // "reliable" 100% branch coverage with no errors; then // wait for the odd assignment to happen anyway, in // production, not reproducably
There is no "reliable 100% coverage" in this case. The coverage I guess is... probabilistic?
Throwing or not throwing Reference Errors is also a semantics change, and since errors can be caught, we can react to their presence/absence, giving another avenue for accidental semantics changes.
I agree it's a semantic change, but it's one that's special in the development workflow. The common practice is to fix code that throws, whatever that means. non-directly-throwing semantic changes require a different kind of attention and testing. I understand errors can be caught by a try-catch placed for other reasons, but whoever cares about transitioning to strict mode will be careful to this kind of issues.
Undeclared variables are likely to be unintended, and likely to lead to bugs, but they might also still let the code run successfully to completion where strict mode errors do or don't, depending on circumstances.
I agree. The goal when transitioning to strict mode is also to preserve the semantics of the original code. I've tried to provide examples of how to fix common errors. For the undeclared variable case, I've explained how to legitimately assign a global variable if that's what was really intended. This way, there is a quick fix that preserves the semantics. Other fixes for all the error cases are welcome as contributions.
Testing increases confidence (sometimes too much so) but cannot prove correctness, only the absence of selected errors.
I fully agree.
What I'd like to understand is why likely static scoping problems should lead to a runtime error, forcing the dependence on testing. If they'd lead to compile time errors (for strict code), there'd be no chance of missing them on the developer engine, independent of incomplete test suite or ancient customer engines. Wouldn't that remove one of the concerns against using strict mode? What am I missing?
I guess it's too late now for ES5 strict mode. What was the rationale behind making it a runtime error?
I think there were plans to make it a compile-time error... was it with the ES6 opt-in? :-s Can it be retrofit in new syntax which are their own opt-in (module, class...)?
On Thu, Feb 21, 2013 at 9:12 AM, David Bruant <bruant.d at gmail.com> wrote:
Le 18/02/2013 23:29, Claus Reinke a écrit :
What I'd like to understand is why likely static scoping problems should lead to a runtime error, forcing the dependence on testing. If they'd lead to compile time errors (for strict code), there'd be no chance of missing them on the developer engine, independent of incomplete test suite or ancient customer engines. Wouldn't that remove one of the concerns against using strict mode? What am I missing?
I guess it's too late now for ES5 strict mode. What was the rationale behind making it a runtime error?
I think there were plans to make it a compile-time error... was it with the ES6 opt-in? :-s Can it be retrofit in new syntax which are their own opt-in (module, class...)?
For the ES5 semantics of the interaction of the global scope and the global object, how could you make this a static error? What would you statically test? Would you statically reject the following program, where <someExpression> is itself just some valid expression computing a value (that might be the string "foo")? Note that "this" below is the global object, since it occurs at top level in a program.
"use strict"; this[<someExpression>] = 8;
console.log(foo);
You still need more than statement or branch coverage. Otherwise, we might get 100% "coverage" while missing edge cases
function raise() { "use strict"; if( Math.random()>0.5 || (Math.random()<0.5) && (variable = 0)) console.log(true); else console.log(false); }
raise(); raise(); raise(); // adjust probabilities and call numbers until we get // "reliable" 100% branch coverage with no errors; then // wait for the odd assignment to happen anyway, in // production, not reproducably There is no "reliable 100% coverage" in this case. The coverage I guess is... probabilistic?
Yes, and yet current test coverage tools, even if they go beyond statement coverage and test for branch coverage, will happily give you a 100% coverage report (with a little tuning, even repeatedly). That is why the page you linked to talks about levels of test coverage beyond branch coverage, and why it is important to know about such limitations.
Given the complexities of test suites and experience with irreproducible bugs, testers might even be tempted to overlook the occasional random test suite failure if it doesn't happen again on re-running the suite?
I understand errors can be caught by a try-catch placed for other reasons, but whoever cares about transitioning to strict mode will be careful to this kind of issues.
I was thinking of services that need to stay up, no matter what (restart first, check what happened later). At least, one can hope that they would notice a Reference error in their logs.
Other fixes for all the error cases are welcome as contributions.
Providing fixes is good. It would be even better if there was a tool that pointed out any and all potential problem spots related to strict mode introduction, flagging non-strict-safe functions for review (Ariya's validator does only check syntax, not scoping or this, I think, but might serve as a starting point).
But now we're into debugging strict-mode-related issues, instead of using strict mode to reduce the likelihood of issues.
I used to be firmly in the (naïve?) strict-mode-is-better camp and couldn't understand why switching everything to strict mode was considered a bad idea. This thread has documented the reasons why some coders are wary about the idea. If your page can help to steer a way around the obstacles, even better.
Claus
For the ES5 semantics of the interaction of the global scope and the global object, how could you make this a static error? What would you statically test? Would you statically reject the following program, where <someExpression> is itself just some valid expression computing a value (that might be the string "foo")? Note that "this" below is the global object, since it occurs at top level in a program.
"use strict"; this[<someExpression>] = 8; console.log(foo);
My first reaction would be to reject the 3rd line statically. We can't hope to check dynamic scoping statically, but we could enforce safety of the language parts that look like they invoke static scoping.
Either declaring 'foo' or logging 'this["foo"]' would be available as workarounds. So I don't see this example as an argument for a runtime error on the 3rd line.
Claus
I think I would have preferred that. But thinking back to the constraints we imposed on ourselves during the ES5 design process, I doubt we could have made that choice. Every additional difference between ES3 (or ES5 sloppy) vs ES5 strict had to be argued for. The counter-argument was "migration tax". Given how slow the uptake of strict mode has been, the migration tax counter-argument was even more right than I thought at the time.
Le 21/02/2013 19:16, Mark S. Miller a écrit :
On Thu, Feb 21, 2013 at 9:12 AM, David Bruant <bruant.d at gmail.com <mailto:bruant.d at gmail.com>> wrote:
Le 18/02/2013 23:29, Claus Reinke a écrit : What I'd like to understand is why likely static scoping problems should lead to a runtime error, forcing the dependence on testing. If they'd lead to compile time errors (for strict code), there'd be no chance of missing them on the developer engine, independent of incomplete test suite or ancient customer engines. Wouldn't that remove one of the concerns against using strict mode? What am I missing? I guess it's too late now for ES5 strict mode. What was the rationale behind making it a runtime error? I think there were plans to make it a compile-time error... was it with the ES6 opt-in? :-s Can it be retrofit in new syntax which are their own opt-in (module, class...)?
For the ES5 semantics of the interaction of the global scope and the global object, how could you make this a static error? "use hypothetic strict"; var a; a = 12; // a was declared, no problem b = b+1; // SyntaxError on the assignment regardless of |'b' in this|
If someone wants to assign to the global 'b', it's still possible to do: this.b = b+1; // or window.b = b+1; Or maybe they forgot to declare b and they just need to declare it somewhere to fix the SyntaxError. At least, the intent will be very explicit.
What would you statically test? "is the variable being assigned declared in the same script?" And I am specifically speaking about variables assignments, that is AssignmentExpression (ES5-11.13) where LeftHandSideExpression is an Identifier. If LeftHandSideExpression is a "MemberExpression [ Expression ]" or "MemberExpression . IdentifierName" in which "MemberExpression" resolves to the global object, I have no problem with it. At least it's very explicit that the global object is being assigned something.
Would you statically reject the following program, where <someExpression> is itself just some valid expression computing a value (that might be the string "foo")? Note that "this" below is the global object, since it occurs at top level in a program.
"use strict"; this[<someExpression>] = 8; console.log(foo);
I would not reject it as I said above. I think there are 2 different concerns:
- assigning a value to an undeclared variable
- adding a property to the global object
I think I only care about preventing the former (I'll talk about the latter below), because the intent is ambiguous and the disambiguation can be either "declare it" or "add a property to the global object like you really mean it". The rule for strict mode could have been: throw a SyntaxError when trying to assign a value to an undeclared variable. As I suggest in the guide, these errors are almost free to fix. Just add "use strict";, read your console which tells you at which line there is an error, what the syntax error is and fix it.
If people really want to add a property to the global object, they can anyway through "this[<expression>] = 8" at the top-level scope as you
suggest or by aliasing the global in non-top-level scopes as in: (function(global){ global[<expression>] = "value"; })(this) or using an existing alias like "window" or "frames" in the web browser. So preventing people from adding global properties is a lost cause. Preventing people from assigning to an undeclared variable isn't.
I'd like to share a piece of documentation I've recently written here. It's a guide to help developers understand how they can transition to strict mode and what they should be aware of while making this transition. Differences between strict and non-strict are divided into 3 categories: syntax errors, runtime errors, semantic changes. Each category requires a different amount of work and attention from developers.
I'm looking forward to any recommendation you'd have to improve this guide, specifically about the runtime errors where I said something about "100% coverage test suite" and I'm not entirely sure about that.