Skip to content

Commit

Permalink
feat: pass cwd to formatters (refs eslint/rfcs#57) (#13392)
Browse files Browse the repository at this point in the history
* chore: Make fixture test assertions more explicit

I need to add another formatter to this directory that will shift all of
these results, so I'm making these assertions more explicit so it's
obvious why a seemingly-unrelated change will cause these to fail.

* Update: pass cwd to formatters (refs eslint/rfcs#57)

* chore: Update tests using formatter fixture dir

* update docs

* Fix link to ESLint API docs

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

* Remove unused docs.category rule meta field

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

* Fix typo in docs note

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

Co-authored-by: Brandon Mills <mills.brandont@gmail.com>
Co-authored-by: Brandon Mills <btmills@users.noreply.github.com>
Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
  • Loading branch information
4 people committed Nov 30, 2021
1 parent 4ccb633 commit 808ad35
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 54 deletions.
75 changes: 39 additions & 36 deletions docs/developer-guide/working-with-custom-formatters.md
Expand Up @@ -6,7 +6,7 @@ Each formatter is just a function that receives a `results` object and returns a

```js
//my-awesome-formatter.js
module.exports = function(results) {
module.exports = function(results, context) {
return JSON.stringify(results, null, 2);
};
```
Expand All @@ -29,35 +29,6 @@ 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 `--format`) flag like this:
Expand All @@ -78,14 +49,14 @@ Tips for `package.json`:

See all [formatters on npm](https://www.npmjs.com/search?q=eslint-formatter);

## The `results` Object
## The `results` Argument

The `results` object passed into a formatter is an array of objects containing the lint results for individual files. Here's some example output:

```js
[
{
filePath: "path/to/file.js",
filePath: "/path/to/a/file.js",
messages: [
{
ruleId: "curly",
Expand All @@ -112,7 +83,7 @@ The `results` object passed into a formatter is an array of objects containing t
"var err = doStuff();\nif (err) console.log('failed tests: ' + err);\nprocess.exit(1);\n"
},
{
filePath: "Gruntfile.js",
filePath: "/path/to/Gruntfile.js",
messages: [],
errorCount: 0,
warningCount: 0,
Expand Down Expand Up @@ -147,14 +118,46 @@ Each `message` object contains information about the ESLint rule that was trigge
* **column**: the column where the issue is located.
* **nodeType**: the type of the node in the [AST](https://github.com/estree/estree/blob/master/spec.md#node-objects)

## The `context` Argument

The formatter function receives an object as the second argument. The object has two properties:

* `cwd` ... The current working directory. This value comes from the `cwd` constructor option of the [ESLint](nodejs-api.md#-new-eslintoptions) class.
* `rulesMeta` ... The `meta` property values of rules. See the [Working with Rules](working-with-rules.md) page for more information about rules.

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

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

**Note:** if a linting is executed by deprecated `CLIEngine` class, the `context` argument may be a different value because it is up to the API users. Please check whether the `context` argument is an expected value or not if you want to support legacy environments.

## Examples

### Summary formatter

A formatter that only cares about the total count of errors and warnings will look like this:

```javascript
module.exports = function(results) {
module.exports = function(results, context) {
// accumulate the errors and warnings
var summary = results.reduce(
function(seq, current) {
Expand Down Expand Up @@ -196,7 +199,7 @@ Errors: 2, Warnings: 4
A more complex report will look something like this:

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

var summary = results.reduce(
Expand All @@ -205,7 +208,7 @@ module.exports = function(results, data) {
var logMessage = {
filePath: current.filePath,
ruleId: msg.ruleId,
ruleUrl: data.rulesMeta[msg.ruleId].docs.url,
ruleUrl: context.rulesMeta[msg.ruleId].docs.url,
message: msg.message,
line: msg.line,
column: msg.column
Expand Down
5 changes: 4 additions & 1 deletion lib/eslint/eslint.js
Expand Up @@ -622,7 +622,7 @@ class ESLint {
throw new Error("'name' must be a string");
}

const { cliEngine } = privateMembersMap.get(this);
const { cliEngine, options } = privateMembersMap.get(this);
const formatter = cliEngine.getFormatter(name);

if (typeof formatter !== "function") {
Expand All @@ -642,6 +642,9 @@ class ESLint {
results.sort(compareResultsByFilePath);

return formatter(results, {
get cwd() {
return options.cwd;
},
get rulesMeta() {
if (!rulesMeta) {
rulesMeta = createRulesMeta(cliEngine.getRules());
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/formatters/cwd.js
@@ -0,0 +1,4 @@
/*global module*/
module.exports = function(results, context) {
return context.cwd;
};
43 changes: 35 additions & 8 deletions tests/lib/cli-engine/cli-engine.js
Expand Up @@ -1158,28 +1158,44 @@ describe("CLIEngine", () => {
configFile: getFixturePath("configurations", "semi-error.json")
});

const report = engine.executeOnFiles([getFixturePath("formatters")]);
const fixturePath = getFixturePath("formatters");
const report = engine.executeOnFiles([fixturePath]);

assert.strictEqual(report.results.length, 4);
assert.strictEqual(report.errorCount, 0);
assert.strictEqual(report.warningCount, 0);
assert.strictEqual(report.fixableErrorCount, 0);
assert.strictEqual(report.fixableWarningCount, 0);
assert.strictEqual(report.results[0].messages.length, 0);
assert.strictEqual(report.results[1].messages.length, 0);
assert.strictEqual(report.results[2].messages.length, 0);
assert.strictEqual(report.results.length, 5);
assert.strictEqual(path.relative(fixturePath, report.results[0].filePath), "async.js");
assert.strictEqual(report.results[0].errorCount, 0);
assert.strictEqual(report.results[0].warningCount, 0);
assert.strictEqual(report.results[0].fixableErrorCount, 0);
assert.strictEqual(report.results[0].fixableWarningCount, 0);
assert.strictEqual(report.results[0].messages.length, 0);
assert.strictEqual(path.relative(fixturePath, report.results[1].filePath), "broken.js");
assert.strictEqual(report.results[1].errorCount, 0);
assert.strictEqual(report.results[1].warningCount, 0);
assert.strictEqual(report.results[1].fixableErrorCount, 0);
assert.strictEqual(report.results[1].fixableWarningCount, 0);
assert.strictEqual(report.results[1].messages.length, 0);
assert.strictEqual(path.relative(fixturePath, report.results[2].filePath), "cwd.js");
assert.strictEqual(report.results[2].errorCount, 0);
assert.strictEqual(report.results[2].warningCount, 0);
assert.strictEqual(report.results[2].fixableErrorCount, 0);
assert.strictEqual(report.results[2].fixableWarningCount, 0);
assert.strictEqual(report.results[2].messages.length, 0);
assert.strictEqual(path.relative(fixturePath, report.results[3].filePath), "simple.js");
assert.strictEqual(report.results[3].errorCount, 0);
assert.strictEqual(report.results[3].warningCount, 0);
assert.strictEqual(report.results[3].fixableErrorCount, 0);
assert.strictEqual(report.results[3].fixableWarningCount, 0);
assert.strictEqual(report.results[3].messages.length, 0);
assert.strictEqual(path.relative(fixturePath, report.results[4].filePath), path.join("test", "simple.js"));
assert.strictEqual(report.results[4].errorCount, 0);
assert.strictEqual(report.results[4].warningCount, 0);
assert.strictEqual(report.results[4].fixableErrorCount, 0);
assert.strictEqual(report.results[4].fixableWarningCount, 0);
assert.strictEqual(report.results[4].messages.length, 0);
});


Expand All @@ -1190,28 +1206,39 @@ describe("CLIEngine", () => {
configFile: getFixturePath("configurations", "single-quotes-error.json")
});

const report = engine.executeOnFiles([getFixturePath("formatters")]);
const fixturePath = getFixturePath("formatters");
const report = engine.executeOnFiles([fixturePath]);

assert.strictEqual(report.errorCount, 6);
assert.strictEqual(report.warningCount, 0);
assert.strictEqual(report.fixableErrorCount, 6);
assert.strictEqual(report.fixableWarningCount, 0);
assert.strictEqual(report.results.length, 5);
assert.strictEqual(path.relative(fixturePath, report.results[0].filePath), "async.js");
assert.strictEqual(report.results[0].errorCount, 0);
assert.strictEqual(report.results[0].warningCount, 0);
assert.strictEqual(report.results[0].fixableErrorCount, 0);
assert.strictEqual(report.results[0].fixableWarningCount, 0);
assert.strictEqual(path.relative(fixturePath, report.results[1].filePath), "broken.js");
assert.strictEqual(report.results[1].errorCount, 0);
assert.strictEqual(report.results[1].warningCount, 0);
assert.strictEqual(report.results[1].fixableErrorCount, 0);
assert.strictEqual(report.results[1].fixableWarningCount, 0);
assert.strictEqual(report.results[2].errorCount, 3);
assert.strictEqual(path.relative(fixturePath, report.results[2].filePath), "cwd.js");
assert.strictEqual(report.results[2].errorCount, 0);
assert.strictEqual(report.results[2].warningCount, 0);
assert.strictEqual(report.results[2].fixableErrorCount, 3);
assert.strictEqual(report.results[2].fixableErrorCount, 0);
assert.strictEqual(report.results[2].fixableWarningCount, 0);
assert.strictEqual(path.relative(fixturePath, report.results[3].filePath), "simple.js");
assert.strictEqual(report.results[3].errorCount, 3);
assert.strictEqual(report.results[3].warningCount, 0);
assert.strictEqual(report.results[3].fixableErrorCount, 3);
assert.strictEqual(report.results[3].fixableWarningCount, 0);
assert.strictEqual(path.relative(fixturePath, report.results[4].filePath), path.join("test", "simple.js"));
assert.strictEqual(report.results[4].errorCount, 3);
assert.strictEqual(report.results[4].warningCount, 0);
assert.strictEqual(report.results[4].fixableErrorCount, 3);
assert.strictEqual(report.results[4].fixableWarningCount, 0);
});

it("should process when file is given by not specifying extensions", () => {
Expand Down
11 changes: 7 additions & 4 deletions tests/lib/cli.js
Expand Up @@ -249,10 +249,13 @@ describe("cli", () => {

// Check metadata.
const { metadata } = JSON.parse(log.info.args[0][0]);
const expectedMetadata = Array.from(BuiltinRules).reduce((obj, [ruleId, rule]) => {
obj.rulesMeta[ruleId] = rule.meta;
return obj;
}, { rulesMeta: {} });
const expectedMetadata = {
cwd: process.cwd(),
rulesMeta: Array.from(BuiltinRules).reduce((obj, [ruleId, rule]) => {
obj[ruleId] = rule.meta;
return obj;
}, {})
};

assert.deepStrictEqual(metadata, expectedMetadata);
});
Expand Down
35 changes: 30 additions & 5 deletions tests/lib/eslint/eslint.js
Expand Up @@ -1210,24 +1210,40 @@ describe("ESLint", () => {
cwd: path.join(fixtureDir, ".."),
overrideConfigFile: getFixturePath("configurations", "semi-error.json")
});
const results = await eslint.lintFiles([getFixturePath("formatters")]);
const fixturePath = getFixturePath("formatters");
const results = await eslint.lintFiles([fixturePath]);

assert.strictEqual(results.length, 4);
assert.strictEqual(results[0].messages.length, 0);
assert.strictEqual(results[1].messages.length, 0);
assert.strictEqual(results[2].messages.length, 0);
assert.strictEqual(results.length, 5);
assert.strictEqual(path.relative(fixturePath, results[0].filePath), "async.js");
assert.strictEqual(results[0].errorCount, 0);
assert.strictEqual(results[0].warningCount, 0);
assert.strictEqual(results[0].fixableErrorCount, 0);
assert.strictEqual(results[0].fixableWarningCount, 0);
assert.strictEqual(results[0].messages.length, 0);
assert.strictEqual(path.relative(fixturePath, results[1].filePath), "broken.js");
assert.strictEqual(results[1].errorCount, 0);
assert.strictEqual(results[1].warningCount, 0);
assert.strictEqual(results[1].fixableErrorCount, 0);
assert.strictEqual(results[1].fixableWarningCount, 0);
assert.strictEqual(results[1].messages.length, 0);
assert.strictEqual(path.relative(fixturePath, results[2].filePath), "cwd.js");
assert.strictEqual(results[2].errorCount, 0);
assert.strictEqual(results[2].warningCount, 0);
assert.strictEqual(results[2].fixableErrorCount, 0);
assert.strictEqual(results[2].fixableWarningCount, 0);
assert.strictEqual(results[2].messages.length, 0);
assert.strictEqual(path.relative(fixturePath, results[3].filePath), "simple.js");
assert.strictEqual(results[3].errorCount, 0);
assert.strictEqual(results[3].warningCount, 0);
assert.strictEqual(results[3].fixableErrorCount, 0);
assert.strictEqual(results[3].fixableWarningCount, 0);
assert.strictEqual(results[3].messages.length, 0);
assert.strictEqual(path.relative(fixturePath, results[4].filePath), path.join("test", "simple.js"));
assert.strictEqual(results[4].errorCount, 0);
assert.strictEqual(results[4].warningCount, 0);
assert.strictEqual(results[4].fixableErrorCount, 0);
assert.strictEqual(results[4].fixableWarningCount, 0);
assert.strictEqual(results[4].messages.length, 0);
});

it("should process when file is given by not specifying extensions", async () => {
Expand Down Expand Up @@ -4731,6 +4747,15 @@ describe("ESLint", () => {
await engine.loadFormatter(5);
}, /'name' must be a string/u);
});

it("should pass cwd to the `cwd` property of the second argument.", async () => {
const cwd = getFixturePath();
const engine = new ESLint({ cwd });
const formatterPath = getFixturePath("formatters", "cwd.js");
const formatter = await engine.loadFormatter(formatterPath);

assert.strictEqual(formatter.format([]), cwd);
});
});

describe("getErrorResults()", () => {
Expand Down

0 comments on commit 808ad35

Please sign in to comment.