Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update: pass rule meta to formatters RFC 10 #11551

Merged
merged 6 commits into from Mar 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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) {
EasyRhinoMSFT marked this conversation as resolved.
Show resolved Hide resolved
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