question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

RFC: proposed linting rules for arrow functions

See original GitHub issue

Proposal:

We’re currently running ESLint against metaphysics on every commit, where commits will fail if the lint finds errors it’s unable to resolve.

We have the option to specify whether or not we want our linter to have opinions about how arrow functions should be written. There are two independently-tracked variables to consider:

  1. arrow-parens allows us to specify how we feel about the optional parens around an arrow function’s arguments when there is exactly one argument. The supported values are "always" and "as-needed" and their usage is documented in the link.
  2. arrow-body-style allows us to specify whether we want to wrap the logic of arrow functions in braces or not. Supported values are "always", "as-needed" and "never".

Currently, as of this PR which updates ESLint and the AirBNB rules to their latest versions and adds linting around promises, we inherit the AirBNB rules for arrow functions for arrow-parens.

But arrow-body-style is more complicated, and is the subject of this RFC. Before my PR, we were using a version of AirBNB rules that had no opinion about arrow functions. I had to upgrade ESLint to support a promise plugin, which required me to upgrade AirBNB while I was at it. When I did that, we got all of AirBNB’s updated rules, which included as-needed for arrow-body-style.

Unfortunately, @damassi pointed out that this turned some previously braced functions with explicit- or no-return into braceless functions with implicit returns. So I overrode arrow-body-style to use always, instead, but as @damassi points out to me this isn’t the same thing as going back to how things were.

My proposal is that we fall back to AirBNB’s default around arrow-body-style, which is to enforce it “as-needed” - this means that any arrow function whose body contains only a single expression will lose the curly braces and the explicit return.

This is a change from how things were, where sometimes arrow functions used an implicit return and sometimes they didn’t, and that decision was up to the individual programmer rather than an algorithm. I feel - and I argue below - that using the default AirBNB rule makes for more readable and more easily maintainable code.

Reasoning

Arrow functions with no braces and an implicit return value provide immediate visual feedback that the function in question is ‘merely’ a map from some input to some output. The absence of braces suggests an absence of internal functional state - we have a single expression and it gets returned, and we know at a glance that there’s nothing else happening in this function.

This is incredibly powerful for apprehending a large codebase at a glance, but it also makes normal code a lot more readable. Consider the difference between:

const current = [1, 2, 3].map(item => { return item * 2 })
const proposed = [1, 2, 3].map(item => item *2)

The minute your eyes see those braces on current they have to enter the scope that the braces define. Sure, it’s just a return, and quickly looking at it makes it obvious that there isn’t anything else in the braces that you have to be aware of.

Contrast that to proposed where the string item => item*2 is itself a pure functional relation with zero moving parts. You don’t have to look ‘into’ the block because there is no block.

This kind of at-a-glance scanning works when you have large blocks of code as well - just consider this example from our source code:

const User = {
  type: UserType,
  name: "User",
  args: {
    email: {
      type: GraphQLString,
      description: "Email to search for user by",
    },
  },
  resolve: (root, option, request, { rootValue: { userLoader } }) => {
    return userLoader(option)
      .then(result => {
        return result
      })
      .catch(err => {
        if (err.statusCode === 404) {
          return false
        }
      })
  },
}

Look at that resolve function on the User schema - it’s returning a single expression, but that’s not immediately obvious. We’re looking at a wall of text and a bunch of parens and brackets, and if we don’t notice that return keyword in all of that noise we may think that this function body is doing something more complex than merely returning a single expression.

I’d argue that reframing it like this reduces cognitive overhead and makes the code easier to read:

const User = {
  type: UserType,
  name: "User",
  args: {
    email: {
      type: GraphQLString,
      description: "Email to search for user by",
    },
  },
  resolve: (root, option, request, { rootValue: { userLoader } }) => userLoader(option)
      .then(result => result)
      .catch(err => {
        if (err.statusCode === 404) {
          return false
        }
      })
}

The signal the eye receives when presented with => { is “this function has a body”. The signal it receives when presented with => anythingElse is “this function returns a pure expression”. This distinction is meaningful and helpful.

Exceptions:

Not all functions whose body consists of a single expression actually care about returning that value. When a function’s body is represented by a single expression valuable for its side-effects the fact that it becomes an implicit return is semantically tricky. In those cases we don’t care about the return value, and seeing => something tricks us into thinking that we care about something rather than the side effects of something’s invocation.

There are two places where we might find this:

  1. In tests we often have a signature that looks something like this:
it ('asserts something', () => {
  assert(foo)
});

If we apply my proposed rule changes to our tests then we get this, which while technically valid and isomorphic to the above still feels really wrong:

it('asserts something', () => assert(foo))

My solution here is simple: we don’t apply our lint rules to our tests.

  1. The other place, though, is in our code. Sometimes we write functions that we just invoke for their side effects, and sometimes those functions only have a single expression inside. I don’t have as-good an answer here, except to say that this is a choice. I’d rather implicitly return a call to console.log than need explicit braces and return statements in my map functions, right?

Additional Context:

Ultimately I don’t think there’s a ‘right’ answer here. Both sides have merit and it’s a matter of personal preference. I suspect this will boil down to which personal preference is better represented here at Artsy, and that’s fine! 😃

Issue Analytics

  • State:closed
  • Created 5 years ago
  • Reactions:1
  • Comments:14 (14 by maintainers)

github_iconTop GitHub Comments

1reaction
mbilokonskycommented, Jun 4, 2018

There was a misunderstanding in the framing of this issue. When I wrote this I was under the impression that our choice was between a draconian enforcement of brackets or a draconian enforcement of their absence, and I wanted to advocate for their absence.

I see now though that I had misunderstood the initial state of things. Not having a rule makes the most sense, so I’m going to retract this issue. If @ashfurrow or @anandaroop, who voted for it, think it’s worth bringing back up then let’s do a separate RFC that frames it more accurately.

Thanks all!

1reaction
ashfurrowcommented, May 30, 2018

I’m 👍 for it, but I think we should apply it to our tests, too. it('asserts something', () => assert(foo)) is a little weird, but not weird enough to give up linting our tests imo.

Read more comments on GitHub >

github_iconTop Results From Across the Web

PHP RFC: Arrow Functions 2.0
This proposal makes the choice that we consider “least bad”. Short closures are critically overdue, and at some point we'll have to make...
Read more >
RFC: Making it easier to use (and extend) base eslint rules ...
Releasing a new rule extension would be transparent to the end user - they are already using, say @typescript-eslint/new-cap , so if we ......
Read more >
arrow-body-style - ESLint - Pluggable JavaScript Linter
A pluggable and configurable linter tool for identifying and reporting on patterns in JavaScript. Maintain your code quality with ease.
Read more >
[RFC] Arrow Functions - externals.io
The RFC1 is slightly different from what I proposed in the ... Now the possible confussion with the different scoping rules would be...
Read more >
Hot to force JS code to have annotations for functions, classes ...
There is no such thing as "arrow methods". Are you talking of arrow functions being used as property values with the class fields...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found