Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Update: pass rule meta to formatters RFC 10 (#11551)
* Update: pass rule meta to formatters (RFC #10)

* Fix html formatter & unit tests

* Update cli unit tests to stub getRules

* Update working-with-custom-formatters.md

* Update working-with-custom-formatters.md

* PR feedback
  • Loading branch information
EasyRhinoMSFT authored and ilyavolodin committed Mar 30, 2019
1 parent b452f27 commit bc3e427
Show file tree
Hide file tree
Showing 8 changed files with 528 additions and 171 deletions.
46 changes: 38 additions & 8 deletions docs/developer-guide/working-with-custom-formatters.md
Expand Up @@ -19,6 +19,35 @@ eslint -f ./my-awesome-formatter.js src/

In order to use a local file as a custom formatter, you must begin the filename with a dot (such as `./my-awesome-formatter.js` or `../formatters/my-awesome-formatter.js`).

### The `data` Argument

The exported function receives an optional second argument named `data`. The `data` object provides extended information related to the analysis results. Currently, the `data` object consists of a single property named `rulesMeta`. This property is a dictionary of rule metadata, keyed with `ruleId`. The value for each entry is the `meta` property from the corresponding rule object. The dictionary contains an entry for each rule that was run during the analysis.

Here's what the `data` object would look like if one rule, `no-extra-semi`, had been run:

```js
{
rulesMeta: {
"no-extra-semi": {
type: "suggestion",
docs: {
description: "disallow unnecessary semicolons",
category: "Possible Errors",
recommended: true,
url: "https://eslint.org/docs/rules/no-extra-semi"
},
fixable: "code",
schema: [],
messages: {
unexpected: "Unnecessary semicolon."
}
}
}
}
```

The [Using Rule metadata](#using-rule-metadata) example shows how to use the `data` object in a custom formatter. See the [Working with Rules](https://eslint.org/docs/developer-guide/working-with-rules) page for more information about rules.

## Packaging the Custom Formatter

Custom formatters can also be distributed through npm packages. To do so, create an npm package with a name in the format of `eslint-formatter-*`, where `*` is the name of your formatter (such as `eslint-formatter-awesome`). Projects should then install the package and can use the custom formatter with the `-f` (or `--formatter`) flag like this:
Expand Down Expand Up @@ -157,7 +186,7 @@ Errors: 2, Warnings: 4
A more complex report will look something like this:

```javascript
module.exports = function(results) {
module.exports = function(results, data) {
var results = results || [];

var summary = results.reduce(
Expand All @@ -166,6 +195,7 @@ module.exports = function(results) {
var logMessage = {
filePath: current.filePath,
ruleId: msg.ruleId,
ruleUrl: data.rulesMeta[msg.ruleId].url,
message: msg.message,
line: msg.line,
column: msg.column
Expand Down Expand Up @@ -196,7 +226,7 @@ module.exports = function(results) {
"\n" +
msg.type +
" " +
msg.ruleId +
msg.ruleId + (msg.ruleUrl ? " (" + msg.ruleUrl + ")" : ""
"\n " +
msg.filePath +
":" +
Expand All @@ -221,17 +251,17 @@ eslint -f ./my-awesome-formatter.js src/
The output will be
```bash
error space-infix-ops
error space-infix-ops (https://eslint.org/docs/rules/space-infix-ops)
src/configs/bundler.js:6:8
error semi
error semi (https://eslint.org/docs/rules/semi)
src/configs/bundler.js:6:10
warning no-unused-vars
warning no-unused-vars (https://eslint.org/docs/rules/no-unused-vars)
src/configs/bundler.js:5:6
warning no-unused-vars
warning no-unused-vars (https://eslint.org/docs/rules/no-unused-vars)
src/configs/bundler.js:6:6
warning no-shadow
warning no-shadow (https://eslint.org/docs/rules/no-shadow)
src/configs/bundler.js:65:32
warning no-unused-vars
warning no-unused-vars (https://eslint.org/docs/rules/no-unused-vars)
src/configs/clean.js:3:6
```
Expand Down
10 changes: 9 additions & 1 deletion lib/cli.js
Expand Up @@ -81,15 +81,23 @@ function translateOptions(cliOptions) {
*/
function printResults(engine, results, format, outputFile) {
let formatter;
let rules;

try {
formatter = engine.getFormatter(format);
rules = engine.getRules();
} catch (e) {
log.error(e.message);
return false;
}

const output = formatter(results);
const rulesMeta = {};

rules.forEach((rule, ruleId) => {
rulesMeta[ruleId] = rule.meta;
});

const output = formatter(results, { rulesMeta });

if (output) {
if (outputFile) {
Expand Down
2 changes: 1 addition & 1 deletion lib/formatters/html-template-message.html
Expand Up @@ -3,6 +3,6 @@
<td class="clr-<%= severityNumber %>"><%= severityName %></td>
<td><%- message %></td>
<td>
<a href="https://eslint.org/docs/rules/<%= ruleId %>" target="_blank" rel="noopener noreferrer"><%= ruleId %></a>
<a href="<%= ruleUrl %>" target="_blank" rel="noopener noreferrer"><%= ruleId %></a>
</td>
</tr>
22 changes: 16 additions & 6 deletions lib/formatters/html.js
Expand Up @@ -62,9 +62,10 @@ function renderColor(totalErrors, totalWarnings) {
* Get HTML (table rows) describing the messages.
* @param {Array} messages Messages.
* @param {int} parentIndex Index of the parent HTML row.
* @param {Object} rulesMeta Dictionary containing metadata for each rule executed by the analysis.
* @returns {string} HTML (table rows) describing the messages.
*/
function renderMessages(messages, parentIndex) {
function renderMessages(messages, parentIndex, rulesMeta) {

/**
* Get HTML (table row) describing a message.
Expand All @@ -74,6 +75,13 @@ function renderMessages(messages, parentIndex) {
return lodash.map(messages, message => {
const lineNumber = message.line || 0;
const columnNumber = message.column || 0;
let ruleUrl;

if (rulesMeta) {
const meta = rulesMeta[message.ruleId];

ruleUrl = lodash.get(meta, "docs.url", null);
}

return messageTemplate({
parentIndex,
Expand All @@ -82,30 +90,32 @@ function renderMessages(messages, parentIndex) {
severityNumber: message.severity,
severityName: message.severity === 1 ? "Warning" : "Error",
message: message.message,
ruleId: message.ruleId
ruleId: message.ruleId,
ruleUrl
});
}).join("\n");
}

/**
* @param {Array} results Test results.
* @param {Object} rulesMeta Dictionary containing metadata for each rule executed by the analysis.
* @returns {string} HTML string describing the results.
*/
function renderResults(results) {
function renderResults(results, rulesMeta) {
return lodash.map(results, (result, index) => resultTemplate({
index,
color: renderColor(result.errorCount, result.warningCount),
filePath: result.filePath,
summary: renderSummary(result.errorCount, result.warningCount)

}) + renderMessages(result.messages, index)).join("\n");
}) + renderMessages(result.messages, index, rulesMeta)).join("\n");
}

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------

module.exports = function(results) {
module.exports = function(results, data) {
let totalErrors,
totalWarnings;

Expand All @@ -122,6 +132,6 @@ module.exports = function(results) {
date: new Date(),
reportColor: renderColor(totalErrors, totalWarnings),
reportSummary: renderSummary(totalErrors, totalWarnings),
results: renderResults(results)
results: renderResults(results, data.rulesMeta)
});
};
16 changes: 16 additions & 0 deletions lib/formatters/json-with-metadata.js
@@ -0,0 +1,16 @@
/**
* @fileoverview JSON reporter, including rules metadata
* @author Chris Meyer
*/
"use strict";

//------------------------------------------------------------------------------
// Public Interface
//------------------------------------------------------------------------------

module.exports = function(results, data) {
return JSON.stringify({
results,
metadata: data
});
};
9 changes: 9 additions & 0 deletions tests/lib/cli.js
Expand Up @@ -726,6 +726,7 @@ describe("cli", () => {
results: []
});
sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done");
sandbox.stub(fakeCLIEngine.prototype, "getRules").returns(new Map());
fakeCLIEngine.outputFixes = sandbox.stub();

localCLI = proxyquire("../../lib/cli", {
Expand Down Expand Up @@ -762,6 +763,7 @@ describe("cli", () => {
results: []
});
sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done");
sandbox.stub(fakeCLIEngine.prototype, "getRules").returns(new Map());
fakeCLIEngine.outputFixes = sandbox.mock().once();

localCLI = proxyquire("../../lib/cli", {
Expand Down Expand Up @@ -799,6 +801,7 @@ describe("cli", () => {
fakeCLIEngine.prototype = leche.fake(CLIEngine.prototype);
sandbox.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report);
sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done");
sandbox.stub(fakeCLIEngine.prototype, "getRules").returns(new Map());
fakeCLIEngine.outputFixes = sandbox.mock().withExactArgs(report);

localCLI = proxyquire("../../lib/cli", {
Expand Down Expand Up @@ -835,6 +838,7 @@ describe("cli", () => {
fakeCLIEngine.prototype = leche.fake(CLIEngine.prototype);
sandbox.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report);
sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done");
sandbox.stub(fakeCLIEngine.prototype, "getRules").returns(new Map());
fakeCLIEngine.getErrorResults = sandbox.stub().returns([]);
fakeCLIEngine.outputFixes = sandbox.mock().withExactArgs(report);

Expand Down Expand Up @@ -886,6 +890,7 @@ describe("cli", () => {
results: []
});
sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done");
sandbox.stub(fakeCLIEngine.prototype, "getRules").returns(new Map());
fakeCLIEngine.outputFixes = sandbox.mock().never();

localCLI = proxyquire("../../lib/cli", {
Expand Down Expand Up @@ -916,6 +921,7 @@ describe("cli", () => {
results: []
});
sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done");
sandbox.stub(fakeCLIEngine.prototype, "getRules").returns(new Map());
fakeCLIEngine.outputFixes = sandbox.stub();

localCLI = proxyquire("../../lib/cli", {
Expand Down Expand Up @@ -951,6 +957,7 @@ describe("cli", () => {
fakeCLIEngine.prototype = leche.fake(CLIEngine.prototype);
sandbox.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report);
sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done");
sandbox.stub(fakeCLIEngine.prototype, "getRules").returns(new Map());
fakeCLIEngine.outputFixes = sandbox.mock().never();

localCLI = proxyquire("../../lib/cli", {
Expand Down Expand Up @@ -987,6 +994,7 @@ describe("cli", () => {
fakeCLIEngine.prototype = leche.fake(CLIEngine.prototype);
sandbox.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report);
sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done");
sandbox.stub(fakeCLIEngine.prototype, "getRules").returns(new Map());
fakeCLIEngine.getErrorResults = sandbox.stub().returns([]);
fakeCLIEngine.outputFixes = sandbox.mock().never();

Expand Down Expand Up @@ -1024,6 +1032,7 @@ describe("cli", () => {
fakeCLIEngine.prototype = leche.fake(CLIEngine.prototype);
sandbox.stub(fakeCLIEngine.prototype, "executeOnText").returns(report);
sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done");
sandbox.stub(fakeCLIEngine.prototype, "getRules").returns(new Map());
fakeCLIEngine.outputFixes = sandbox.mock().never();

localCLI = proxyquire("../../lib/cli", {
Expand Down

0 comments on commit bc3e427

Please sign in to comment.