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

feat: pass cwd to formatters (refs eslint/rfcs#57) #13392

Merged
merged 8 commits into from Nov 30, 2021
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
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