diff --git a/docs/developer-guide/working-with-custom-formatters.md b/docs/developer-guide/working-with-custom-formatters.md index 96b0f8fd537..54db6632847 100644 --- a/docs/developer-guide/working-with-custom-formatters.md +++ b/docs/developer-guide/working-with-custom-formatters.md @@ -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); }; ``` @@ -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: @@ -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", @@ -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, @@ -147,6 +118,38 @@ 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 @@ -154,7 +157,7 @@ Each `message` object contains information about the ESLint rule that was trigge 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) { @@ -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( @@ -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 diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 29d7e5cbc92..6274772ad85 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -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") { @@ -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()); diff --git a/tests/fixtures/formatters/cwd.js b/tests/fixtures/formatters/cwd.js new file mode 100644 index 00000000000..b9c4966efd4 --- /dev/null +++ b/tests/fixtures/formatters/cwd.js @@ -0,0 +1,4 @@ +/*global module*/ +module.exports = function(results, context) { + return context.cwd; +}; diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index 16beb92b87b..2ba4839baf6 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -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); }); @@ -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", () => { diff --git a/tests/lib/cli.js b/tests/lib/cli.js index 143a3ac1efc..59ff0eaf7a4 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -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); }); diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index d984f5a3082..4e140f7fe0f 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -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 () => { @@ -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()", () => {