Olov Lassus

I made JSLint restrict mode clean. Here's what I found

21 Apr 2011

I’ve created this thing called restrict mode for JavaScript. It’s a definition of a proper subset of the language which I believe is great because I think it increases robustness and makes your program more easy to reason about. It comes with a tool that will check if your program really complies to restrict mode, when you say it does. The tool is called restricter and is a plugin for JSShaper, an extensible framework for JavaScript syntax tree shaping. JSShaper is cool for many other reasons as well but this post is about restrict mode. For me restrict mode makes JS programming more enjoyable since it helps me focus on real programming challenges instead of annoyances/defects/inconsistencies of the JS language. It eases my mind.

I want to know how well the restrict mode subset of JavaScript matches existing programs. I’ve already written about how I made v8bench restrict mode clean. I’ve also done the same to jQuery and Kraken, though I haven’t written about it yet (I’m worse at writing than coding but it’s on my todo-list). When I saw a JSLint discussion on reddit I figured that it would be a great test case, so I decided to make it restrict mode clean and write about what I found. I’m glad I did because it exposed a bug in JSShaper (now fixed). I also think that it serves as a good example of what you can expect from restrict mode.

Here are my findings and how I changed JSLint to make it restrict mode clean. In my checkout of JSLint, commit 99091cd30d354e77cf95 was the latest and greatest so the changes apply cleanly against that revision.

First of all, we need to specify which parts of the program should follow restrict mode. We want it to cover all of JSLint so that the JSShaper restricter plugin can help us catch any issues. JSLint is already enclosed in a function which starts with a "use strict" string. Let’s add "use restrict" as well:

var JSLINT = (function () {
-    "use strict";
+    "use strict"; "use restrict";

To show what I changed I’ve included the diff’s (ceremony removed). Red - lines are what I removed, green + lines are what I added. Scroll down for full patch.

With that change in place, I ran jslint.js through JSShaper (restricter): js run-restricter.js -- JSLint/jslint.js > JSLint/jslint_restrict.js. I tweaked the jslint.html file to use the restrict mode checked version and to dump the errors to console instead of throwing exceptions. To make JSLint restrict mode clean I just had to inspect the errors (when running JSLint) and fix the issues. I iterated on this, and here’s what I found and changed:

First change is to fix a coercion-happy String.prototype.isAlpha. When "hello".isAlpha() or str.isAlpha() is evaluated the expression left of the dot is first boxed into a temporary String object, effectively the same as (new String("hello")).isAlpha() or (new String(str)).isAlpha(). In the String.prototype.isAlpha function, the temporary object is bound to this. [This behavior was then changed in ES5 strict mode to further add to the confusion since it’s not backwards compatible, which is another reason for First change]. If this is used as an operand like below it will trigger coercion, sometimes with unexpected results. You can get the primitive value from a String object by applying the valueOf method or String function. Let’s do that, so that the <= and >= operators won’t need to coerce their operands any longer:

String.prototype.isAlpha = function() {
-            return (this >= 'a' && this <= 'z\uffff') ||
-                (this >= 'A' && this <= 'Z\uffff');
+            var v = this.valueOf();
+            return (v >= 'a' && v <= 'z\uffff') ||
+                (v >= 'A' && v <= 'Z\uffff');
         };

Second change is to do the same for String.prototype.isDigit:

String.prototype.isDigit = function () {
-            return (this >= '0' && this <= '9');
+            var v = this.valueOf();
+            return (v >= '0' && v <= '9');
         };

Third change is to fix String.prototype.name. It used to return this sometimes, which is not the right thing to do. I described above how primitives get boxed into temporary objects. Returning these objects is almost always bad for two reasons: It spreads the coercion disease, and it’s not what the user of the function expects (because now sometimes the function returns strings, sometimes it returns String objects). Let’s fix that by returning this.valueOf(). Let’s also use the primitive value with the + operator, and the coercion is gone. Here we go:

String.prototype.name = function () {
-            if (ix.test(this)) {
-                return this;
+            var v = this.valueOf();
+            if (ix.test(v)) {
+                return v;
             }
-            if (nx.test(this)) {
-                return '"' + this.replace(nxg, function (a) {
+            if (nx.test(v)) {
+                return '"' + v.replace(nxg, function (a) { // this.replace works too
                     if (escapes[a]) {
                         return escapes[a];
                     }
                     return '\\u' + ('0000' + a.charCodeAt().toString(16)).slice(-4);
                 }) + '"';
             }
-            return '"' + this + '"';
+            return '"' + v + '"';
         };

Fourth and final change is to fix the report function which performs a lot of string formatting relying on numbers being coerced into strings (str + nostr). It’s a fine example of code that would greatly benefit from using Fmt or something similar. There’s also the option to bail out of restrict mode for the entire report function by annotating it with /*@loose*/. For this example I fixed it in the least obtrusive manner by applying the String function to the numbers. Here we go:

@@ -6523,7 +6526,7 @@ loop:   for (;;) {
-                            warning.line + ' character ' + warning.character : '') +
+                            String(warning.line) + ' character ' + String(warning.character) : 
@@ -6536,7 +6539,7 @@ loop:   for (;;) {
-                        data.implieds[i].line + '</i>';
+                        String(data.implieds[i].line) + '</i>';
@@ -6545,7 +6548,7 @@ loop:   for (;;) {
-                        data.unused[i].line + ' </i> <small>' +
+                        String(data.unused[i].line) + ' </i> <small>' +
@@ -6583,7 +6586,7 @@ loop:   for (;;) {
-                output.push('<br><div class=function><i>' + the_function.line + '</i> ' +
+                output.push('<br><div class=function><i>' + String(the_function.line) + '</i> ' +

And that’s it. JSLint is now restrict mode clean, as far as I can tell. I didn’t find any tests but I’ve tested it manually with a few programs, including JSLint itself.

What have we learned?

For a start, it’s another step in the right direction in verifying that the restrict mode subset of JavaScript is sane and well enough matches the JS subset people already use in practice. It took little effort to get JSLint restrict mode clean, and it required no prior experience with that code base. We had to change a few lines of code as expected, and the signal vs noise was very high.

Did we find any real defects? We caught String.prototype.name sometimes returning temporary boxed objects which I classify as a defect, even though it doesn’t cause any harm at the moment. Fixing that is a keeper.

Even if we don’t buy into the idea restrict mode, is there something else that we’d like to keep? I’d suggest the fixes for coercion happiness examplified in change two and three. Especially so if you follow the recommendation of preferring === over == (I do). Think about it: It would be a bit weird to see this >= '0' && this <= '0' yield true when v is a temporary boxed String object (it will coerce), but have this === '0' yield false (it won’t coerce). Leaving it as it is will increase the risk of future bugs. If you don’t prefer === over == then you’re likely not to care about this change.

And some of you would perhaps consider keeping the changes for our fourth finding as well (str + nostr) even if you didn’t yet buy in on restrict mode, because quite frankly the string formatting code in that function is not the most beautiful or easiest code to follow. Using Fmt or something similar could greatly improve it. Many will consider the code fine as-is and won’t change it unless they go restrict.

To summarize: Of our four findings/changes, three seem noteworthy enough that I think many would consider applying the changes even if they don’t care about restrict mode. Not bad for a few hours of work of which the majority went into writing this post.

If you choose to keep all changes as well as the "use restrict" directive, then you can easily run the program through JSShaper (restricter) again in the future and perhaps find more stuff. It could help you during development, and it could help your program become more robust. You can choose to run the checked version only once in a while, every time your unit tests run or all the time. It’s totally up to you.

A program complying to the subset of JavaScript defined by restrict mode (i.e. a restrict mode clean program) is also much easier to reason about compared to a program that (may) use the entire spectra of loose JS semantics. That may help you when debugging tomorrow or when coming back to your old code one year from now. If you read your friend’s code and happen to find the "use restrict” directive in there, you may find yourself relieved knowing that not only will you faster and more accurately understand what it does and how, chances are that your friend accurately understands his/her own program too.

The JSLint restrict mode experiment exceeded my expectations. Why don’t you try applying restrict mode to one of your own programs? What’s to lose?

There’s more info available on restrictmode.org. If you have any questions about restrict mode or JSShaper then post a message to the JSShaper mailing list and I’ll help you out. You can also contact me directly.

Here’s the patch for everything described above (jslint.html and jslint.js).

Show comments (reddit)


Follow me on Twitter

« JavaScript modules in shells, browsers, everywhere!    ↑ Home     »