The ESLint API

ESLint is an amazing project. At first, perhaps one would think it solves a rather gray task (rules for the structure of code). But when one thinks one step further this is one of the most important, sexy aspects of programming there is.

Well ordered code is spelled predictability. And predictability is a mean to good communication. The more specific the rules are for how code is to be written in a project, I presume, the more efficient can you write and read new code and also communicate about it.

ESLint main dependency is Esprima, a project for working with the JavaScript Abstract Syntax Tree (AST); ESLint also provides an API for making plugins and rules of your own.

In this blog post, we will, in the end, write a plugin of our own. For the sake of simplicity, let's envision a project in which prints (logs) actually are crucial. The project has it's own print-function and we want to prohibit the use of the console API's log method. Theory

Consider this JavaScript code:

function addNums(...nums) {
  return nums.reduce((a, b) => a + b);
}

console.log(`2 + 2 = ${addNums(2, 2)}`); 
// output: 2 + 2 = 4

Esprima includes both a lexical analyzer (a tokenizer) and a syntax analyzer (another word for AST, well what comes before). Somewhere in its inner workings, it starts out with by performing a lexical analysis of the code (function, addNums, (, …, nums, ) and so on).

A lexical analysis breaks down the code into atoms, the smallest possible semantic unit of the code, and creates a symbol table for all sorts of declared names (in the case above: nums, a and b).

The 'atoms' would also include 'keywords' like function, return, reduce, etc. At the end of this process, a token stream emerges, replacing the character stream. I guess you could see it as a sort of word-list of 'words', present in the program code.

The next step is called syntax analysis and creates the AST.

For purposes of understanding, I think it's valuable to contemplate the difference between something abstract and concrete. Something abstract or abstractions in general, are only possible as derivations of a concrete 'entity' of some sort; it provides a 'distance' and brings about the essentials of the concrete subject matter, while ignoring what's trivial (depending on the context).

When the code is considered in the syntax analysis, it focuses on the semantics of the programming language, ignoring, for instance, the visual elements of a code. In most programming languages (Python and Coffeescript being two exceptions) indentations (tabs) are for 'us', not the machine - it's not an essential part of the code to the machine.

The same thing is true also for the 'concrete' syntax of a language. Most programming languages use functions, but in the inner workings of an AST, how a programming language actually programmatically requires the user to spell is trivial (func or function).

Therefore what things are "called" - that is, the naming of the "abstractions" - are contingent. One would hope though, that the transpiler would make use of an explanatory naming system, something that's the case with Esprima (and Acorn, another quite famous AST-ifier) who follows the suggestion of the project ESTree,, a standardization project.

These kind of abstractions are useful to the compiler, enabling a way to translate from one language to another. Esprima, following ESTree, would articulate a AST for a function with this kind of terminology:

{
    "type": "FunctionDeclaration",
    "start": 0,
    "end": 68,
    "id": {
    "type": "Identifier",
    "start": 9,
    "end": 16,
    "name": "addNums"
    }
}

As you can see, an AST much resembles any JSON object with data. Everything has a key and value, and a value can also be another key holding other values. Practice

In this example we will make us of the following dependencies.

Chai and Mocha is for testing. eslint-plugin-local makes it possible to easy have a local setup (no need for package.json configs or pushing to NPM).

First of, let's add our rule to the .eslintrc

rules: {
    'local/replace-console-log-with-the-logger': 'error'
}

Now let's do the test so we make sure of an accurate output. We begin by importing our rule and the RuleTester, an ESLint specific test API, and initiate it.

const rule = require('../.eslintplugin/replace-console-log-with-the-logger/rule.js');
const {RuleTester} = require('eslint');
const ruleTester = new RuleTester();

Now we make us of the ruleTester. We specify the id of the rule and the rule. We give an example of valid code as well as invalid. Important is that we want to test the output of applying the fix.

ruleTester.run('replace-console-log-with-the-logger', rule, {
  valid: [`logIt('Hello console!')`],
  invalid: [
    {
      code: `console.log('Hello console!')`,
      errors: [error],
      output: `logIt('Hello console!')`
    }
  ]
});

The rule in itself is in this case straight forward. When writing rules you need access to the AST of some code snippet that focus a problem. I highly recommend The AST Explorer

const isConsoleLogUsed = node =>
  node &&
  node.callee.type === 'MemberExpression' &&
  node.callee.object.name === 'console' &&
  node.callee.property.name === 'log';

After examing a common console.log snippet we can easily identify its different parts. This function merely checks if the node at hand fulfills the terms we state.

The ESLint API for making plugins of your own is quite straightforward. If a particular node does not fulfill the terms in our isConsoleLogUsed function we return the node (we don't change it). But if the node actually makes use of console.log, we modify it. In a simple example like this one, we don't really need to modify the AST, instead, we modify the source code in its text form.

const rule = {
  create: context => ({
    CallExpression(node) {
      if (!isConsoleLogUsed(node)) {
        return;
      }
      context.report({
        node,
        message: 'Should use The Logger, not the log method of the console API',
        fix: fixer => {
          const scope = node.parent;
          const sourceCode = context.getSourceCode();
          const lengthOfConsoleLogString = 'console.log'.length;
          const removedConsoleLog = sourceCode
            .getText(node)
            .substring(lengthOfConsoleLogString);
          const withTheLogger = `logIt${removedConsoleLog}`;
          return fixer.replaceText(scope, withTheLogger);
        }
      });
    }
  })
};
module.exports = rule;

I've just started exploring the ESLint API for writing plugins. I will continue so.