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 5 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
99 changes: 99 additions & 0 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.
EasyRhinoMSFT marked this conversation as resolved.
Show resolved Hide resolved

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 the `Rule` object.
EasyRhinoMSFT marked this conversation as resolved.
Show resolved Hide resolved

## 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 @@ -235,6 +264,76 @@ warning no-unused-vars
src/configs/clean.js:3:6
```

### Using Rule metadata

A formatter that summarizes the rules that were triggered might look like this:
EasyRhinoMSFT marked this conversation as resolved.
Show resolved Hide resolved

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

var summary = results.reduce(
function(seq, current) {
current.messages.forEach(function(msg) {
if (!seq.contains(msg.ruleId)) {
seq.push(msg.ruleId);
}
});
},
{
rules: []
}
);

if (summary.rules.length > 0) {
var lines = summary.rules.map(function (ruleId) {
var ruleMeta = rulesMeta[ruleId];
var text = ruleId;

if (ruleMeta) {
if (ruleMeta.type) {
text += " (" + ruleMeta.type + ")";
}
if (ruleMeta.docs) {
if (ruleMeta.docs.description) {
text += "\n " + ruleMeta.docs.description;
}
if (ruleMeta.docs.url) {
text += "\n " + ruleMeta.docs.url;
}
}
}

return text;
}
.join("\n\n");

return lines + "\n";
}
}
```

The output will be

```bash
space-infix-ops (layout)
require spacing around infix operators
https://eslint.org/docs/rules/space-infix-ops

semi (layout)
require or disallow semicolons instead of ASI
https://eslint.org/docs/rules/semi

no-unused-vars (problem)
disallow unused variables
https://eslint.org/docs/rules/no-unused-vars

no-shadow (suggestion)
disallow variable declarations from shadowing variables declared in the outer scope
https://eslint.org/docs/rules/no-shadow
```

## Passing Arguments to Formatters

While custom formatter do not receive arguments in addition to the results object, it is possible to pass additional data into formatters.
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>
15 changes: 13 additions & 2 deletions lib/formatters/html.js
Expand Up @@ -15,6 +15,7 @@ const path = require("path");
const pageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-page.html"), "utf-8"));
const messageTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-message.html"), "utf-8"));
const resultTemplate = lodash.template(fs.readFileSync(path.join(__dirname, "html-template-result.html"), "utf-8"));
let rulesMeta;

/**
* Given a word and a count, append an s if count is not one.
Expand Down Expand Up @@ -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,7 +90,8 @@ function renderMessages(messages, parentIndex) {
severityNumber: message.severity,
severityName: message.severity === 1 ? "Warning" : "Error",
message: message.message,
ruleId: message.ruleId
ruleId: message.ruleId,
ruleUrl: ruleUrl !== null ? ruleUrl : "https://eslint.org/docs/rules"
EasyRhinoMSFT marked this conversation as resolved.
Show resolved Hide resolved
});
}).join("\n");
}
Expand All @@ -105,13 +114,15 @@ function renderResults(results) {
// Public Interface
//------------------------------------------------------------------------------

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

totalErrors = 0;
totalWarnings = 0;

rulesMeta = data.rulesMeta;

// Iterate over results to get totals
results.forEach(result => {
totalErrors += result.errorCount;
Expand Down
16 changes: 16 additions & 0 deletions lib/formatters/json-with-metadata.js
@@ -0,0 +1,16 @@
/**
* @fileoverview JSON reporter
* @author Burak Yigit Kaya aka BYK
EasyRhinoMSFT marked this conversation as resolved.
Show resolved Hide resolved
*/
"use strict";

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

module.exports = function(results, data) {
return JSON.stringify({
results,
rulesMeta: data.rulesMeta
EasyRhinoMSFT marked this conversation as resolved.
Show resolved Hide resolved
});
};
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