CommaChamelon

Trailing Commas in JavaScript

by on June 10, 2013 10:00 am

The Scenario

You’re working on a JavaScript project, and your project includes the following object definition:

var deckOfCards = {
    deal : function() {
        // TODO: add dealing logic
    }
};

You go to add another method to your object.

var deckOfCards = {
    deal : function () {
        // TODO: add dealing logic
    }

    shuffle : function () {
        // TODO: add shuffling logic
    }
};

You return to your test page and get mysteriously unexpected behavior. What happened?

You’ve probably recognized the mistake in the above code, but then if you write much JavaScript, there’s a good chance you’ve made the same mistake numerous times, and you’ll probably make it again. Maybe it’s because of our experience with other languages, or maybe it’s just one of those small details, but we often forget to separate JavaScript object members with commas, especially when adding a new method to the end of a class.

So maybe you think to yourself, “You know, most browsers will tolerate a comma after the last member in the object.” You might train yourself to always put a comma after each member in an object, so that you won’t have this problem going forward. Then you would write:

var deckOfCards = {
    deal : function () {
        // TODO: add dealing logic
    },

    shuffle : function () {
        // TODO: add shuffling logic
    },
};

Then you might go on happily building your application, with its ever-growing code base, until one day your client says “Oh, by the way, it’s vitally important that this run in IE8.”

You think back on your observation that most browsers will accept a comma after the last member of a JavaScript object, only to reflect that IE8 is not in that particular majority. Now what?

Now you have a bunch of tiny, hard-to-find bugs scattered throughout a large code base, and you need to find and remove them. To make matters worse, they’re going to blend into the background. They look like valid syntax, especially if you’ve been training yourself to put them in the code on purpose. If I had a worse sense of humor, I’d call them comma chameleons. (Oh, look, I do have a worse sense of humor. Must be a byproduct of the long debugging sessions.)

Going forward, you might adopt a different convention in lieu of trailing commas. You could use the SQL convention of putting commas at the beginning of the new line, on the theory that you edit the end of a list more often than the beginning. Maybe you could always put a dummy element at the end of an object definition so that every “real” element can have a trailing comma with no issues. But right now, you just need a way to fix a mountain of existing code.

Our Solution

When our team encountered this problem, we felt that Perl might hold a solution. Indeed, a quick Google search reveals that others have fallen into the “trailing comma” trap, and some have provided RE strings that will find some of the trailing commas, some of the time. What follows is a somewhat more comprehensive script.

You can download the script here; if you’d like a step-by-step explanation of what the script will do, read on! (Note: the code as it appears below has been modified for readability; only the original version, as found via the previous link, has been tested.)

Before I get into the nuts and bolts, two caveats:

  • First, I said “somewhat more comprehensive.” What we’re looking for here is a good return on investment; it didn’t make sense to write a full-fledged JavaScript parser, which means some evil genius might find a way to hide a trailing comma that this script won’t catch. It should catch the cases that are likely to show up in real code.
  • Second, the error handling in this script is nearly nonexistent. Feel free to beef it up if you’d like.

With those caveats in mind, the script only finds the problems and tells you where they are; it does not attempt to repair the code for you.

Step 1: Find JavaScript files to process

Somehow we have to tell the script what file(s) to process. We could accept a filename on the command line and then rely on OS utilities to traverse our project tree and execute the script for each source file it locates. But directory traversal isn’t very hard in Perl anyway, so let’s allow our command line arguments to include directories to be recursively searched.

sub process
{
    my ($item) = @_;
    print “$item…\n”;

    # directory to be searched recursively?
    search($item) if –d $item;

    # …or file to be scanned?
    scan($item) if –f $item;
}

sub search
{
    my ($item) = @_;
    my $d, @f;

    # Get the list of files/directories in the directory
    opendir($d, $item);
    @f = readdir $d;
    closedir $d;

    # Process each one except the . and .. links
    foreach $f (@f) {
        next if $f eq “.” || $f eq “..”;
        proc(“$item/$f”);
    }
}

sub scan
{
    # TODO
}

# Functions declared; now process each command-line arg
foreach $nxt (@ARGV)
{
    proc($nxt);
}

Now the scan routine will be called for each file named on the command line, or found in a directory named on the command line (or a subdirectory thereof, recursively). So what should the scan function do?

Step 2: Strip Comments

One easy way to get a trailing comma is to comment out the last method in an object. We want our script to handle that case, and we suspect there might be other times when comments get in the way.

We expect to find two kinds of comment: single-line comments (//…) and block comments (/*…*/). Single-line comments alone would be pretty simple. Multi-line comments are somewhat more complex. The possible combinations of the two can get downright ugly. We’ll make some simplifying assumptions for now, so if your code is likely to say things like:

/* single line comments start with // */ ,

…then you may need to add some more robust parsing logic. At a minimum, we’ll handle cases without nested comment markers correctly.

We’ll declare $comment and set it to 0; this will be a boolean indicator of whether we’re in the middle of a multi-line comment; any time a line contains a /* with no matching */, we’ll set $comment to 1. Then we ignore subsequent lines until we find one that does contain a */.

For those lines we’re not ignoring, we just apply a little substring and/or regex logic to remove the comments that are present in the line. We don’t modify $_ itself because we want to preserve the original file contents for our output; so we use the $nocomment variable.

We start the body of the scan function like this:

my ($item) = @_;

my $comment=0;  #is this line part of a multi-line comment?
my $comma=0;  #did the last nonblank line end with a comma?
my $block;      #potential output
my $nocomment;  #input line with comments stripped out
my $f, $i;

open($f, “<$item”);
while(<$f>)
{
    $nocomment = $_;

   if ($comment) {
       #skip lines until we see an end-of-comment marker
       #then remove everything up to the marker (inclusive)
       $i = index $nocomment, '*/';
       next unless $i > -1;
       $nocomment = substr $nocomment, $i+2;
       $comment = 0;
   }

   # remove comments that are entirely on this line
   $nocomment =~ s/\/\/.*//;
   $nocomment =~ s/\/\*.*?\*\///g;

   # check if a comment spills over onto the next line
   $i = index $nocomment, '/*';
   if ($i > -1) {
       $comment = 1;
       $nocomment = substr $nocomment, 0, $i;
   }

   # TODO: detect trailing commas
}

close $f;

Note the importance of order here: We remove all instances of /*…*/ from the line, and after that is done if we still see a /* we know that a comment will carry over to subsequent lines.

Step 3: Detect Trailing Commas

We might think of a trailing comma as the string “,}” or “,]”. Of course, there could be whitespace (likely including newlines) between the comma and the closing brace. (There could be comments, too, but we’ve removed those.)

If we find a comma followed by a closing brace, we’ll output the line number and line contents. (We output the filename when we started processing the file.) If we find a comma at the end of a line, then we’ll scan subsequent lines until we know whether it is or is not a trailing comma. (The $comma variable will serve much the same purpose here as the $comment variable did for multi-line comments.)

So the last #TODO is replaced as follows:

if( $comma )
{
    # We’re resolving a , from the end of a previous line.
    # Add this line to the block of potential output.
    $block .= "  $.: $_";

    # If this isn’t a blank line, we’ll resolve the comma
    if ($nocomment !~ /^\s*$/)
    {
        $comma = 0;
    }
    # If the line starts with ] or }, it’s a trailing comma
    if ($nocomment =~ /^\s*[\]}]/)
    {
        print "$block\n";
    }
}

# look for trailing commas on this line
if( $nocomment =~ /,\s*[\]}]/ )
{
    print "  $.: $_\n" ;
}

# see if this line ends with a ,
if( $nocomment =~ /,\s*$/ )
{
    $comma = 1;
    $block = "  $.: $_";
}

And that’s should do it. Again, there are many ways to improve this script – add error handling, parse the JavaScript more thoroughly, etc. But this is a good “bang for your buck” implementation that shouldn’t produce false positives and will likely catch trailing commas unless someone’s intentionally trying to hide them.

In addition to after-the-fact bug hunting, a tool like this can be used proactively. For example, our team is considering putting a pre-commit hook in place so that code with trailing commas won’t make it into source control in the first place.

Happy Hunting!

– Mark Adelsberger, asktheteam@keyholesoftware.com

  • Share:

Leave a Reply

Things Twitter is Talking About
  • Thank your #Sysadmin - today is System Administrator Appreciation Day. http://t.co/LcvDNa9kPg
    July 25, 2014 at 8:05 AM
  • @rickincanada Thx for your tweet! Shoot us an email at asktheteam@keyholesoftware.com so we can set up a time to talk. Have a good day.
    July 24, 2014 at 3:33 PM
  • Never used JAXB? Check out a simple usage pattern that pairs #JAXB’s data binding capabilities with JPA - http://t.co/Ki9G04HV5e
    July 24, 2014 at 9:53 AM
  • Guess what today is? Tell An Old Joke Day - http://t.co/835ORWMX6N! So, why do programmers always confuse Halloween & Xmas? 31 Oct = 25 Dec
    July 24, 2014 at 8:45 AM
  • MT @midwestio: Posted another #midwestio talk recording to our YouTube channel: @MinaMarkham on modular CSS. Watch: http://t.co/aU3LpfUoi4
    July 24, 2014 at 8:25 AM
  • We just posted pictures from our National Hot Dog Day Lunch Cookout. Check them out - http://t.co/To06plaw1C
    July 23, 2014 at 4:14 PM
  • Good free cheat sheet - #Java Performance Optimization Refcard from @DZone: http://t.co/7vBgsmqy08
    July 23, 2014 at 10:48 AM
  • Did you know today is a holiday? It's National Hot Dog Day! We're gearing up for our team lunch hot dog cookout & can't wait to celebrate.
    July 23, 2014 at 9:43 AM
  • Check out our newest blog: #JAXB – A Newcomer’s Perspective, Part 1 http://t.co/Ki9G04HV5e
    July 22, 2014 at 1:22 PM
  • New post on the Keyhole blog by Mark Adelsberger: #JAXB – A Newcomer’s Perspective, Part 1 http://t.co/Ki9G04HV5e
    July 21, 2014 at 2:27 PM
  • If you're a Java dev, you're likely familiar with Annotations. But have you created your own #Java Annotations? Ex - http://t.co/BgCsYjxZKF
    July 18, 2014 at 12:10 PM
  • RT @gamasutra: Don't Miss: Unconventional Tips for Improving your Programming Skills http://t.co/6TFox7CKHU
    July 16, 2014 at 3:20 PM
  • We're about to send out our free monthly tech newsletter. Dev tips/articles via email. Not on the list yet? Sign up - http://t.co/F8h0NSiicZ
    July 15, 2014 at 11:57 AM
  • Have you ever tried creating your own #Java annotations? See a situation where it was beneficial - http://t.co/BgCsYjxZKF
    July 15, 2014 at 8:36 AM
  • There's a new post on the Keyhole blog by @jhackett01: Creating Your Own #Java Annotations - http://t.co/BgCsYjxZKF
    July 14, 2014 at 1:43 PM
  • We love development! Have you seen our weekly team blog? We show how to be successful with the tech we use. See it - http://t.co/nlRtb1XNQH
    July 12, 2014 at 2:35 PM
  • Rapid appdev has a bad rep, but there are ways to bring development time down the right way. Don't Fear the Rapid - http://t.co/aTPcAKOj0r
    July 11, 2014 at 3:10 PM
  • Automated Testing is great for dev, but does bring a set of challenges (especially for #agile teams). Success tips: http://t.co/1acl1ngO7i
    July 11, 2014 at 12:16 PM
  • This is fantastic - One small step for Google, one giant leap for empowering girls to code: http://t.co/R90V5DBkv1
    July 10, 2014 at 2:52 PM
  • #RabbitMQ: messaging software built on AMQP protocol. Learn relevant concepts & how to avoid common "gotchas" here: http://t.co/ZwMXlhKyX8
    July 10, 2014 at 9:31 AM
Keyhole Software
8900 State Line Road, Suite 455
Leawood, KS 66206
ph: 877-521-7769
© 2014 Keyhole Software, LLC. All rights reserved.