Perfection Kills

by kangax

Exploring Javascript by example

← back 1343 words

JSCritic

Choosing a good piece of Javascript is hard.

Every time I come across a newly-released, shiny plugin or library, I wonder what's going on underneath. Yes, it looks pretty and convenient but what does underlying code look like? Does it browser sniff or extend the DOM? Does it pollute global scope? What about compatibility with older browsers; could it be that it utilizes, say, ES5 getters/setters making it unusable in IE<9?

I always wished there was a way to quickly check how well a certain script behaves. Not like we did back in the days.

The best thing for a code quality test like this is undoubtedly through JSHint [1]. It can answer most of those questions and many more. Unfortunately, "many more" part is a bit of a problem. Plugging a script code into jshint.com usually yields tons of issues, not just with browser compatibility or global variables but also code style. These checks are a must for your own scripts, but for 3rd party code, I don't really care about missing semicolons (despite my love of them), whether constructors begin with uppercase, or if assignments happen in conditional statements. I only wish to know how well a script behaves on the outside. Now, a sloppy code style can certain be an indication of a bad quality of script overall. But more often than not it's a preference not a problem.

Few days ago, I decided to hack something together; something simple, that would allow me to quickly plug the script and see a big picture.

So I made JSCritic.

Plug in script code and it answers some of the more pressing questions.

I tried using Esprima at first, but quickly realized that most of the checks I care about are already in JSHint. So why not piggy back on that? JSCritic turned out to be a simple wrapper on top of it. I originally wrote it in Node, to be able to pass it filename and quickly see the results, then ported it to run in a browser.

You can still run it in both.

Another thing I wanted to see is minified script size. Some plugins have minified versions, some don't, some use better minifiers, some worse. I decided to minify content through UglifyJS — a de facto standard of minification at the moment — to get an objective overview of code size. Unfortunately, browser version of UglifyJS seems to be choking more often than Node one, so it might be safer to use the latter.

I have to say that JSCritic is more of a prototype at the moment. Static analysis has its limitations, as well as JSHint. I haven't had much time to polish it, but hoping to improve in the near future or with the help of ever-awesome contributors. One thing to emphasize is that for best results you should use non-minified source code (you'll see exactly why below)!

If you want to know more about tests, implementation details, and drawbacks, read on. Otherwise, hope you find it as useful as I do.

Global variables

Let's first take a look at global variables detection. Unfortunately, it seems to be very simplistic in JSHint, failing to catch cases other than plain variable/function declarations in top level code.

Here it catches foo, bar, and qux as expected, but fails with all of these:

Granted, detecting globals via static analysis is hard. A more robust solution would be to actually execute code and compare global object "signature", just like I did in detect-global bookmarklet back in 2009 (based on a script by Remy Sharp). Unfortunately, executing script is also not always easy, and global properties could be exported from various places (e.g. methods that need to be called explicitly); we have no idea which places those are.

Still, JSHint catches a good number of globals and accidental leaks like these:

It gives a decent overview, but you should still look through variables carefully as some of them might be false positives. I'm hoping this will be made more robust in the future JSHint versions (or we could try using hybrid detection method — both via static analysis and through global object signature).

Extended natives

Detecting native object extensions has few limitations as well. While it catches both Array and String in example like this:

..it fails with all of these:

As you can see, it's also simplistic and could have false negatives. There's an open JSHint issue for this.

eval & document.write

Just like with other checks, there are false positives and false negatives. Here's some of them, just to give an idea of what to expect:

and with document.write:

Browser compatibility

I included 3 checks for browser/engine compatibility — Mozilla-only extensions (let expressions, expression closures, multiple catch blocks, etc.), things IE chokes on (e.g. extra comma), and ES6 additions (array comprehensions, generators, imports, etc.). All of these things could affect cross-browser support.

Browser sniffing

To detect browser sniffing, we first check statically for occurance of navigator implied global (via JSHint), then check source for occurance of navigator.userAgent. This covers a lot of cases, but obviously won't catch any obscurities, so be careful. To make things easier, a chunk of code surrounding navigator.userAgent is pasted for expection purposes. You can quickly check what it's there for (is it for non-critical enhancement purposes or could it cause subtle bugs and/or full breakage?)

Unused variables

Finally, I included unused variables check from JSHint. While not exactly an indication of external script behavior, seeing lots of those could be an indication of sloppy (and potentially buggy) code. I put it all the way at the end, as this is the least important check.

So there it is. The set of rules can definitely be made larger (does it use ES5 features? does it use browser-sniffing-like inference? does it extend the DOM?) and more accurate in the future. For now you can use JSCritic as a quick first look under the hood.

[1] and perhaps ESLint, but I haven't had a chance to look into it.

Did you like this? Donations are welcome

comments powered by Disqus