Skip to content

Writing a custom eslint formatter

Roy Riojas edited this page Jul 14, 2015 · 3 revisions

overview

Writing an eslint reporter/formatter is simple. Just provide a module that exports a function that will receive the results from the execution of eslint.

so the simplest formatter will be something like:

//my-awesome-formatter.js
module.exports = function (results) {
  console.log(JSON.stringify(results, null, 2));
}

So running eslint with this custom formatter:

eslint -f './my-awesome-formatter' grunt-deps/

The output will be

[
  {
    "filePath": "path/to/file.js",
    "messages": [
      {
        "ruleId": "curly",
        "severity": 2,
        "message": "Expected { after 'if' condition.",
        "line": 41,
        "column": 2,
        "nodeType": "IfStatement",
        "source": "  if ( err ) console.log( 'failed tests: ' + err );"
      },
      {
        "ruleId": "no-process-exit",
        "severity": 2,
        "message": "Don't use process.exit(); throw an error instead.",
        "line": 42,
        "column": 2,
        "nodeType": "CallExpression",
        "source": "  process.exit( exitCode );"
      }
    ],
    "errorCount": 2,
    "warningCount": 0
  },
  {
    "filePath": "Gruntfile.js",
    "messages": [],
    "errorCount": 0,
    "warningCount": 0
  }
]

As you can see the passed argument is just a list of results.

the result object

You will receive an object result from each file eslint validates

  • filePath: The path to the file (relative to the cwd)
  • messages: An array of message objects. See below for more info about messages
  • errorCount: The number of errors for the given file
  • warningCount: the number of warnings for the give file

So if you're writing a formatter that only cares about errors and warnings count you can do something like this:

module.exports = function (results) {
  var results = results || [];
  var summary = results.reduce(function (seq, current) {
    seq.errors+= current.errorCount;
    seq.warnings+= current.warningCount;
    return seq;
  }, { errors: 0, warnings: 0});
  if (summary.errors > 0 || summary.warnings > 0) {
    console.log('Errors: '+ summary.errors + ', Warnings: ' + summary.warnings + '\n');
  }
};

So running eslint with this custom formatter:

eslint -f './my-awesome-formatter' grunt-deps/

The output will be

Errors: 2, Warnings: 4

the message object

  • ruleId: the id of the rule being violated
  • severity: the severity of the failure, 1 for warnings and 2 for errors
  • message: the human readable description of the error
  • line: the line where where the issue is located
  • column: the colum where the issue is located
  • nodeType: the type of the node in the AST
  • source: a extract of the code the line where the failure happened. Note to self: I wish we have at least a couple more of lines here both before and after... that will help to provide a bit more of context. But so far it is good enough.

So if you want to also show a more detailed report you can do something like this:

module.exports = function (results) {
  var results = results || [];
  var summary = results.reduce(function (seq, current) {
    current.messages.forEach(function (msg) {
      var logMessage = {
        filePath: current.filePath,
        ruleId: msg.ruleId,
        message: msg.message,
        line: msg.line,
        column: msg.column,
        source: msg.source
      };

      if (msg.severity === 1) {
        logMessage.type = 'warning';
        seq.warnings.push(logMessage);
      }
      if (msg.severity === 2) {
        logMessage.type = 'error';
        seq.errors.push(logMessage);
      }
    });
    return seq;
  }, { errors: [], warnings: []});

  if (summary.errors.length > 0 || summary.warnings.length > 0) {
    var lines = summary.errors.concat(summary.warnings).map(function (msg) {
      return '\n' + msg.type + ' ' + msg.ruleId + '\n  ' + msg.filePath + ':' + msg.line + ':' + msg.column;
    } ).join('\n');

    return lines + '\n';
  }
};

So running eslint with this custom formatter:

eslint -f './my-awesome-formatter' grunt-deps/

The output will be

error space-infix-ops
  grunt-deps/configs/bundler.js:6:8
error semi
  grunt-deps/configs/bundler.js:6:10
warning no-unused-vars
  grunt-deps/configs/bundler.js:5:6
warning no-unused-vars
  grunt-deps/configs/bundler.js:6:6
warning no-shadow
  grunt-deps/configs/bundler.js:65:32
warning no-unused-vars
  grunt-deps/configs/clean.js:3:6

Final words

More complex reporters could written by grouping differently the errors and warnings and/or grouping the ruleIds.

I find that a reporter is easier to use if:

  • the errors are reported at the end

  • the files are printed using the following format

    file:line:colum

    Since that allows modern fancy terminals to make them links to files that open in your favorite editor. Mine's
    favorite is Sublime Text 3 but this should work on other editors and IDEs.

Other persons prefer to concentrate in a given error first, so you can group all the errors and warnings by the ruleId of the the rule they are violating. In the end it is a matter of preferences.

Bonus:

Passing arguments to your reporter. So far there is no way to pass arguments to the reporter itself when executed from the command line, but you can pass them directly to the reporter reading either from the environment variables or from the command line arguments Sadly command line arguments are discarded by the cli parser. Bummer :(

For example:

Let's say that you want to show only the messages that are errors (and filter those noise warnings)

module.exports = function (results) {
  var skipWarnings = process.env.AF_SKIP_WARNINGS === 'true'; //af stands for awesome-formatter

  var results = results || [];
  var summary = results.reduce(function (seq, current) {
    current.messages.forEach(function (msg) {
      var logMessage = {
        filePath: current.filePath,
        ruleId: msg.ruleId,
        message: msg.message,
        line: msg.line,
        column: msg.column,
        source: msg.source
      };

      if (msg.severity === 1) {
        logMessage.type = 'warning';
        seq.warnings.push(logMessage);
      }
      if (msg.severity === 2) {
        logMessage.type = 'error';
        seq.errors.push(logMessage);
      }
    });
    return seq;
  }, { errors: [], warnings: []});

  if (summary.errors.length > 0 || summary.warnings.length > 0) {
    var warnings = !skipWarnings ? summary.warnings : []; // skip the warnings in that case

    var lines = summary.errors.concat(warnings).map(function (msg) {
      return '\n' + msg.type + ' ' + msg.ruleId + '\n  ' + msg.filePath + ':' + msg.line + ':' + msg.column;
    } ).join('\n');

    return lines + '\n';
  }
};

So running eslint with this custom formatter:

AF_SKIP_WARNINGS=true eslint -f './my-awesome-formatter' grunt-deps/

The output will be

error space-infix-ops
  grunt-deps/configs/bundler.js:6:8

error semi
  grunt-deps/configs/bundler.js:6:10

I'm not a huge fan of environment variables, but until eslint allow passing parameters to formatters then this is the only solution so far.

Another bonus

Or you can simply use any of the nice formatters written by the community. Might I suggest eslint-friendly-formatter #shamelessSelfPromotion?