The Cranky Ghost in the Machine

Sometimes I swear source code can say as much about its author as any poetry. This might sound like an exaggeration (or like I don’t read too much poetry), but I’m often surprised by how style and values find their way into the seemingly-lifeless language of software.

For me, nowhere has this been more apparent than in JSHint. Although it’s been a highly successful open source project in terms of number of contributors, the hand of the original architect is still evident today. I recently came across this influence while fixing a pre-release regression, and I thought the details might help demonstrate my point.

Background

To understand the regression and the solution, you’ll need a little context on the history of the JSHint project.

Originally, a skilled developer created a tool named “JSLint” (note the “L”) for the purpose of detecting errors in JavaScript source code. Like other “linting” utilities before it, the tool operated by scanning the input code for patterns that were likely entered mistakenly. It was the most powerful tool of its kind, and many people started using it in their projects.

Over time, though, the project maintainer began implementing safety checks that were more “opinionated.” In other words, JSLint began requiring users to conform to a certain style of coding, one which wasn’t necessarily less prone to error. This style aligned with the personal preferences of the author, but many users objected to the new rules.

One such user decided to create a new version of the project where any rule could be disabled. He re-named his “fork” of the project to JSHint (note the “H”), and the new project has been developed with an emphasis on configurability ever since–almost five years.

The problem

Not many users know this, but JSHint automatically enables its newcap option when entering strict mode code:

if (state.tokens.curr.value === "use strict") {
  if (!state.option["(explicitNewcap)"]) {
    state.option.newcap = true;
  }
  state.option.undef = true;
}

This means that for any code that begins with a 'use strict'; directive prologue, JSHint will require that constructor functions begin with a capital letter.

If you find this behavior surprising, you’re not alone. None of the current maintainers of JSHint were aware of it until recently. For a project that was inspired by the desire to give control to the user, this seems kind of presumptuous. It turns out that it’s a legacy of the JSLint project, present in its very first public release. Here’s the original implementation of that feature:

function use_strict() {
    if (nexttoken.value === 'use strict') {
        advance();
        advance(';');
        strict_mode = true;
        option.newcap = true;
        option.undef = true;
        return true;
    } else {
        return false;
    }
}

So even though it is somewhat antithetical to the spirit of the project, this behavior is not itself a regression. The problem came about as we improved support for ES2015 language features.

ES2015 defines completely new ways that source code may enter into strict mode semantics. In addition to the “use strict” directive that we all love typing, ES2015 code is interpreted in strict mode when it appears within a class body and when it is loaded as a module.

In order to minimize duplication in the JSHint source code, we wanted to unify the way we detect strict mode across the codebase:

/**
 * Determine if the code currently being linted is strict mode code.
 *
 * @returns {boolean}
 */isStrict: function() {
  return this.directive["use strict"] || this.inClassBody ||
    this.option.module || this.option.strict === "implied";
},

We updated all checks for strictness accordingly, including the section above:

- if (state.tokens.curr.value === "use strict") {
+ if (state.isStrict()) {
    if (!state.option["(explicitNewcap)"]) {
      state.option.newcap = true;
    }
    state.option.undef = true;
  }

Using that helper method had the desired effect: it made JSHint’s treatment of strict mode code more consistent. Unfortunately in this case, this led to new warnings. Now that JSHint enabled newcap for class body code, it suddenly started issuing warnings for code like this:

class Parent {
  constructor(childCtors) {
    this.children = childCtors.map(child => new child());
  }
}

In most cases, we would label this new warning a bug fix rather than a regression because it makes JSHint more consistent. Here, though, the “consistent” behavior (automatic opt-in to newcap) is undocumented and unintuitive, so that argument doesn’t really hold water.

The solution

We briefly considered preserving the existing semantics by special-casing thenew-cap opt-in condition–effectively reverting the change for just that oneif statement:

  if (state.isStrict()) {
-   if (!state.option["(explicitNewcap)"]) {
+   if (state.tokens.curr.value === "use strict" &&
+     !state.option["(explicitNewcap)"]) {
      state.option.newcap = true;
    }
    state.option.undef = true;
  }

…but we didn’t like that solution for two reasons:

  1. We love consistency too much
  2. We really don’t care for newcap at all (because of the weak assumptions it has to make in order to work. That’s why it’s been deprecated for over a year.)

Instead, we decided to disable the automatic opt-in behavior altogether. This avoids the regression and gets us closer to a newcap-free world. Users who preferred this behavior may be a little put out, but because the change makes JSHint less strict, no one’s build will be breaking for it.

Conclusion

What I find most interesting about all this is the way one developer’s opinions had been preserved across many years and many changes from many contributors. It serves as a reminder of how we codify our personal values whenever we write software.

There’s nothing wrong with this when it’s deliberate–developers understand software as their mechanism for shaping the world around them. It’s the unconscious side-effects that we should worry about. Our code has a power to enable, restrict, and generally control others (to an extent that even we ourselves may find surprising). This means there’s a real risk of harm when we write thoughtlessly.

I don’t think this is completely avoidable, but that doesn’t mean we shouldn’t be vigilant. To me, this means insisting on peer review and questioning any assumptions it uncovers. You never know who is going to have to debug your code five years from now.

We just released JSHint 2.9.2; it contains this fix and a bunch more. Enjoy!

Comments

Contact Us

We'd love to hear from you. Get in touch!

Boston

201 South Street, Boston, MA 02111

New York

315 Church St, New York, NY 10013

Phone & Email

(617)379-2752 hello@bocoup.com