Writing your own ESLint Plugin: Autofix code

This is the second post in "Writing your own ESLint plugin" series. Checkout Part 1


In part 1, we finished writing our very own ESLint plugin. However, manual code changes were required to fix the code that violated our custom rule. Let's tap into one of the best features of ESLint - auto fix!

Recap: Our rule aims to prevent usage of _.isNull when checking for null.

Autofixing all the things!

If you'd like ESLint to attempt to fix your code, you must do the following two steps:

1) Set the value of meta.fixable to "code" (defaults to null)

2) Pass in a fix function to context.report. The fix function receives a fixer object as it's argument.

context.report({  
    node,
    message: "Use native JavaScript to check for null",
    fix: function(fixer) {
        // autofix logic goes here
    }
});

The fixer object has a bunch of helpful methods on it that allow us to manipulate the AST. At this point, I recommend that you head over to ESLint - Applying fixes section to familiarize yourself with all the methods that are available on the fixer object.

Updating tests

Let's take a look at an example of what we're aiming for.

_.isNull(obj) // Autofix to obj === null;  

Let's add this to our test suite. Each test can take an optional output parameter which is used to determine the correctness of the auto-fix logic.

invalid: [  
    {
        code: "_.isNull(obj);",
        errors: [ errorObject ],
        output : "obj === null;",
    },
]

Run the tests after this & you should have a failing test.

Get fixing!

We have to do two things to get our example fix working.

  1. Grab the argument passed to _.isNull
  2. Replace the _.isNull call with === null

The first step is actually quite easy to do. node inside context.report refers to MemberExpression i.e _.isNull. Its parent is the actual function call that we want to the argument from.

fix: function(fixer) {  
    /**
     * node is MemberExpression (_.isNull)
     * node.parent is CallExpression (_.isNull()))
     */

    let scope = node.parent;

    // Grab the argument that was passed to the isNull function
    const arg = scope.arguments[0];
}

That's step one out of the way. Now all we have to do is append === null to it, right?

const fixedCode = `${arg} === null`;  
return fixer.replaceText(scope, fixedCode);  

The test will continue to fail after this change because arg isn't the actual argument that was passed to the isNull, it's an object representation of that.

The context object has a nifty little method on it that lets us access the source code associated with any node. Let's use that instead!

fix: function(fixer) {  
    /**
     * node is MemberExpression (_.isNull or _.isUndefined)
     * node.parent is CallExpression (_.isNull() or _.isUndefined())
     */

    let scope = node.parent;

    // Grab the argument that was passed to the isNull function
    const arg = scope.arguments[0];

    // Get an instance of `SourceCode` so we can convert the argument to source code
    // & append the fix to it
    const sourceCode = context.getSourceCode();
    let fixedCode = sourceCode.getText(arg) + ' === null';

    return fixer.replaceText(scope, fixedCode);
}

Voila! With that our tests are passing. There you have it! You now have the necessary knowledge to start implementing your own custom ESLint plugins with auto-fixing enabled!

Disclaimer: Your production level auto fix logic should deal with a lot more scenarios & edge cases. The purpose of this simple example was to explain how it all comes together
Author: Watandeep Sekhon
Software Developer