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
  • Pssst... Our free monthly newsletter comes out tomorrow with dev tips/articles via email. Not on the list? Sign up: http://t.co/F8h0NSzleZ
    October 22, 2014 at 1:46 PM
  • How do we harness the power of callbacks without the confusing mess of nested functions in #JavaScript? Promises - http://t.co/obK811q48q
    October 21, 2014 at 2:18 PM
  • Pssst... Our free monthly newsletter comes out tomorrow with dev tips/articles via email. Not on the list? Sign up: http://t.co/F8h0NSzleZ
    October 21, 2014 at 12:05 PM
  • Did you know today is Clean Your Virtual Desktop Day? It really is: https://t.co/TCRpWgTmxg Celebrate by organizing your desktop files.
    October 20, 2014 at 4:50 PM
  • Don't miss the newest post from @bricemciver: Make Me a Promise - http://t.co/obK811q48q #JavaScript
    October 20, 2014 at 10:43 AM
  • RT @DZone: #Docker 1.3 Releases with Security, Signed Images, and Process Injection by @bendzone #devops http://t.co/uytIwFPgO6
    October 17, 2014 at 10:04 AM
  • If you have 15+ years #Java exp, you don't expect to be puzzled debugging a null pointer exception. See an exception: http://t.co/m2iDgNEleK
    October 17, 2014 at 9:51 AM
  • Many on our team attended the #Royals victory last night & @cdesalvo even got a selfie with the Gov. Go #KansasCity! http://t.co/N1Psooe2CE
    October 16, 2014 at 3:39 PM
  • Interesting ExplainLikeI'm5 talk: Why do companies develop iOS first when Android holds 70% of the 'Smart' Market? http://t.co/fxgjIBmqBi
    October 16, 2014 at 12:26 PM
  • We're looking for a top-notch #Java developer to join our team. Learn more about our company culture & the role - http://t.co/0fKsFmN0Ql
    October 16, 2014 at 9:08 AM
  • Want to learn to create custom #Java annotations & process them using the Reflection API? @jhackett01's tutorials - http://t.co/mf1F3eIDY3
    October 15, 2014 at 11:43 AM
  • Happy Ada Lovelace Day! It's a celebration of the achievements of women in STEM - if there's a woman in tech that you admire, tell her today
    October 15, 2014 at 9:13 AM
  • .@fpmoles We absolutely agree - thanks for reading!
    October 15, 2014 at 8:13 AM
  • With 15 yrs exp, @bmongar didn't expect surprise when debugging a null pointer exception. Why it puzzled him - http://t.co/m2iDgNEleK #Java
    October 14, 2014 at 11:20 AM
  • #Royals fans with tickets to tonight's canceled game, here's what you need to know - http://t.co/EErHht3zoN
    October 13, 2014 at 4:23 PM
  • RT @UzilitySoftware: Watch as Wayne explains to the boss, Marvin, what an agile board is about. #scrumalliance #scrum http://t.co/5MzB1bNw…
    October 13, 2014 at 12:01 PM
  • Getting started with #MongoDB? (Flexible #NoSQL for Humongous Data) Here's a free cheat sheet from the folks @Dzone - http://t.co/oBMvICzfcL
    October 13, 2014 at 11:10 AM
  • Brad Mongar's newest post is live on the Keyhole blog - #Java and the Sweet Science http://t.co/m2iDgNEleK
    October 13, 2014 at 8:59 AM
  • RT @housecor: If users have share links to your web app like this: "Go to here. Then click here. Then here." You're doing it wrong. #de
    October 10, 2014 at 2:18 PM
  • CSS is 20 years old today! Happy birthday, #CSS - web design would not be the same without you. http://t.co/8tEMoUjorI
    October 10, 2014 at 9:55 AM
Keyhole Software
8900 State Line Road, Suite 455
Leawood, KS 66206
ph: 877-521-7769
© 2014 Keyhole Software, LLC. All rights reserved.