Skip to content

Commit 173e820

Browse files
authored
feat: Pass --max-warnings value to formatters (#16348)
* feat: Pass --max-warnings value to formatters Fixes #14881 * Make formatter docs wording consistent Originally suggested by @fasttime in #16348 (comment). * Update LoadedFormatter and FormatterFunction typedefs Originally suggested by @fasttime in #16348 (comment). Internally, I define two types, `MaxWarningsExceeded` and `ResultsMeta`, that the updated `LoadedFormatter` and `FormatterFunction` types can use. I'm then able to reuse the `ResultsMeta` type instead of the generic `Object` type in `ESLint` and `FlatESLint`'s `format` wrapper functions. Externally in the Node.js API docs, I describe the new second argument to `LoadedFormatter`'s `format` method inline rather than separately documenting the new types. While working on this, I noticed that `FlatESLint` is using the pre-PR #15727 `Formatter` type rather than `LoadedFormatter` and `FormatterFunction`. I'll fix that in a follow-up PR to keep this PR scoped to passing `maxWarningsExceeded` info.
1 parent 8476a9b commit 173e820

7 files changed

Lines changed: 91 additions & 19 deletions

File tree

docs/src/developer-guide/nodejs-api.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -410,8 +410,8 @@ This edit information means replacing the range of the `range` property by the `
410410

411411
The `LoadedFormatter` value is the object to convert the [LintResult] objects to text. The [eslint.loadFormatter()][eslint-loadformatter] method returns it. It has the following method:
412412

413-
* `format` (`(results: LintResult[]) => string | Promise<string>`)<br>
414-
The method to convert the [LintResult] objects to text.
413+
* `format` (`(results: LintResult[], resultsMeta: ResultsMeta) => string | Promise<string>`)<br>
414+
The method to convert the [LintResult] objects to text. `resultsMeta` is an object that will contain a `maxWarningsExceeded` object if `--max-warnings` was set and the number of warnings exceeded the limit. The `maxWarningsExceeded` object will contain two properties: `maxWarnings`, the value of the `--max-warnings` option, and `foundWarnings`, the number of lint warnings.
415415

416416
---
417417

docs/src/developer-guide/working-with-custom-formatters.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,16 +129,21 @@ Each `message` object contains information about the ESLint rule that was trigge
129129

130130
## The `context` Argument
131131

132-
The formatter function receives an object as the second argument. The object has two properties:
132+
The formatter function receives an object as the second argument. The object has the following properties:
133133

134134
* `cwd` ... The current working directory. This value comes from the `cwd` constructor option of the [ESLint](nodejs-api#-new-eslintoptions) class.
135+
* `maxWarningsExceeded` (optional): If `--max-warnings` was set and the number of warnings exceeded the limit, this property's value will be an object containing two properties: `maxWarnings`, the value of the `--max-warnings` option, and `foundWarnings`, the number of lint warnings.
135136
* `rulesMeta` ... The `meta` property values of rules. See the [Working with Rules](working-with-rules) page for more information about rules.
136137

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

139140
```js
140141
{
141142
cwd: "/path/to/cwd",
143+
maxWarningsExceeded: {
144+
maxWarnings: 5,
145+
foundWarnings: 6
146+
},
142147
rulesMeta: {
143148
"no-extra-semi": {
144149
type: "suggestion",
@@ -153,7 +158,7 @@ For example, here's what the object would look like if one rule, `no-extra-semi`
153158
unexpected: "Unnecessary semicolon."
154159
}
155160
}
156-
}
161+
},
157162
}
158163
```
159164

lib/cli.js

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const debug = require("debug")("eslint:cli");
3737
/** @typedef {import("./eslint/eslint").LintMessage} LintMessage */
3838
/** @typedef {import("./eslint/eslint").LintResult} LintResult */
3939
/** @typedef {import("./options").ParsedCLIOptions} ParsedCLIOptions */
40+
/** @typedef {import("./shared/types").ResultsMeta} ResultsMeta */
4041

4142
//------------------------------------------------------------------------------
4243
// Helpers
@@ -200,7 +201,7 @@ async function translateOptions({
200201
/**
201202
* Count error messages.
202203
* @param {LintResult[]} results The lint results.
203-
* @returns {{errorCount:number;warningCount:number}} The number of error messages.
204+
* @returns {{errorCount:number;fatalErrorCount:number,warningCount:number}} The number of error messages.
204205
*/
205206
function countErrors(results) {
206207
let errorCount = 0;
@@ -238,10 +239,11 @@ async function isDirectory(filePath) {
238239
* @param {LintResult[]} results The results to print.
239240
* @param {string} format The name of the formatter to use or the path to the formatter.
240241
* @param {string} outputFile The path for the output file.
242+
* @param {ResultsMeta} resultsMeta Warning count and max threshold.
241243
* @returns {Promise<boolean>} True if the printing succeeds, false if not.
242244
* @private
243245
*/
244-
async function printResults(engine, results, format, outputFile) {
246+
async function printResults(engine, results, format, outputFile, resultsMeta) {
245247
let formatter;
246248

247249
try {
@@ -251,7 +253,7 @@ async function printResults(engine, results, format, outputFile) {
251253
return false;
252254
}
253255

254-
const output = await formatter.format(results);
256+
const output = await formatter.format(results, resultsMeta);
255257

256258
if (output) {
257259
if (outputFile) {
@@ -406,17 +408,24 @@ const cli = {
406408
resultsToPrint = ActiveESLint.getErrorResults(resultsToPrint);
407409
}
408410

409-
if (await printResults(engine, resultsToPrint, options.format, options.outputFile)) {
411+
const resultCounts = countErrors(results);
412+
const tooManyWarnings = options.maxWarnings >= 0 && resultCounts.warningCount > options.maxWarnings;
413+
const resultsMeta = tooManyWarnings
414+
? {
415+
maxWarningsExceeded: {
416+
maxWarnings: options.maxWarnings,
417+
foundWarnings: resultCounts.warningCount
418+
}
419+
}
420+
: {};
410421

411-
// Errors and warnings from the original unfiltered results should determine the exit code
412-
const { errorCount, fatalErrorCount, warningCount } = countErrors(results);
422+
if (await printResults(engine, resultsToPrint, options.format, options.outputFile, resultsMeta)) {
413423

414-
const tooManyWarnings =
415-
options.maxWarnings >= 0 && warningCount > options.maxWarnings;
424+
// Errors and warnings from the original unfiltered results should determine the exit code
416425
const shouldExitForFatalErrors =
417-
options.exitOnFatalError && fatalErrorCount > 0;
426+
options.exitOnFatalError && resultCounts.fatalErrorCount > 0;
418427

419-
if (!errorCount && tooManyWarnings) {
428+
if (!resultCounts.errorCount && tooManyWarnings) {
420429
log.error(
421430
"ESLint found too many warnings (maximum: %s).",
422431
options.maxWarnings
@@ -427,7 +436,7 @@ const cli = {
427436
return 2;
428437
}
429438

430-
return (errorCount || tooManyWarnings) ? 1 : 0;
439+
return (resultCounts.errorCount || tooManyWarnings) ? 1 : 0;
431440
}
432441

433442
return 2;

lib/eslint/eslint.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,12 @@ const { version } = require("../../package.json");
3636
/** @typedef {import("../shared/types").Plugin} Plugin */
3737
/** @typedef {import("../shared/types").Rule} Rule */
3838
/** @typedef {import("../shared/types").LintResult} LintResult */
39+
/** @typedef {import("../shared/types").ResultsMeta} ResultsMeta */
3940

4041
/**
4142
* The main formatter object.
4243
* @typedef LoadedFormatter
43-
* @property {function(LintResult[]): string | Promise<string>} format format function.
44+
* @property {(results: LintResult[], resultsMeta: ResultsMeta) => string | Promise<string>} format format function.
4445
*/
4546

4647
/**
@@ -625,14 +626,16 @@ class ESLint {
625626
/**
626627
* The main formatter method.
627628
* @param {LintResult[]} results The lint results to format.
629+
* @param {ResultsMeta} resultsMeta Warning count and max threshold.
628630
* @returns {string | Promise<string>} The formatted lint results.
629631
*/
630-
format(results) {
632+
format(results, resultsMeta) {
631633
let rulesMeta = null;
632634

633635
results.sort(compareResultsByFilePath);
634636

635637
return formatter(results, {
638+
...resultsMeta,
636639
get cwd() {
637640
return options.cwd;
638641
},

lib/eslint/flat-eslint.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const LintResultCache = require("../cli-engine/lint-result-cache");
5757
/** @typedef {import("../shared/types").LintMessage} LintMessage */
5858
/** @typedef {import("../shared/types").ParserOptions} ParserOptions */
5959
/** @typedef {import("../shared/types").Plugin} Plugin */
60+
/** @typedef {import("../shared/types").ResultsMeta} ResultsMeta */
6061
/** @typedef {import("../shared/types").RuleConf} RuleConf */
6162
/** @typedef {import("../shared/types").Rule} Rule */
6263
/** @typedef {ReturnType<ConfigArray.extractConfig>} ExtractedConfig */
@@ -1070,14 +1071,16 @@ class FlatESLint {
10701071
/**
10711072
* The main formatter method.
10721073
* @param {LintResults[]} results The lint results to format.
1074+
* @param {ResultsMeta} resultsMeta Warning count and max threshold.
10731075
* @returns {string} The formatted lint results.
10741076
*/
1075-
format(results) {
1077+
format(results, resultsMeta) {
10761078
let rulesMeta = null;
10771079

10781080
results.sort(compareResultsByFilePath);
10791081

10801082
return formatter(results, {
1083+
...resultsMeta,
10811084
cwd,
10821085
get rulesMeta() {
10831086
if (!rulesMeta) {

lib/shared/types.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,23 @@ module.exports = {};
190190
* @property {DeprecatedRuleInfo[]} usedDeprecatedRules The list of used deprecated rules.
191191
*/
192192

193+
/**
194+
* Information provided when the maximum warning threshold is exceeded.
195+
* @typedef {Object} MaxWarningsExceeded
196+
* @property {number} maxWarnings Number of warnings to trigger nonzero exit code.
197+
* @property {number} foundWarnings Number of warnings found while linting.
198+
*/
199+
200+
/**
201+
* Metadata about results for formatters.
202+
* @typedef {Object} ResultsMeta
203+
* @property {MaxWarningsExceeded} [maxWarningsExceeded] Present if the maxWarnings threshold was exceeded.
204+
*/
205+
193206
/**
194207
* A formatter function.
195208
* @callback FormatterFunction
196209
* @param {LintResult[]} results The list of linting results.
197-
* @param {{cwd: string, rulesMeta: Record<string, RuleMeta>}} [context] A context object.
210+
* @param {{cwd: string, maxWarningsExceeded?: MaxWarningsExceeded, rulesMeta: Record<string, RuleMeta>}} [context] A context object.
198211
* @returns {string | Promise<string>} Formatted text.
199212
*/

tests/lib/cli.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,45 @@ describe("cli", () => {
244244
});
245245
});
246246

247+
describe("when the --max-warnings option is passed", () => {
248+
const flag = useFlatConfig ? "--no-config-lookup" : "--no-eslintrc";
249+
250+
describe("and there are too many warnings", () => {
251+
it(`should provide \`maxWarningsExceeded\` metadata to the formatter with configType:${configType}`, async () => {
252+
const exit = await cli.execute(
253+
`--no-ignore -f json-with-metadata --max-warnings 1 --rule 'quotes: warn' ${flag}`,
254+
"'hello' + 'world';",
255+
useFlatConfig
256+
);
257+
258+
assert.strictEqual(exit, 1);
259+
260+
const { metadata } = JSON.parse(log.info.args[0][0]);
261+
262+
assert.deepStrictEqual(
263+
metadata.maxWarningsExceeded,
264+
{ maxWarnings: 1, foundWarnings: 2 }
265+
);
266+
});
267+
});
268+
269+
describe("and warnings do not exceed the limit", () => {
270+
it(`should omit \`maxWarningsExceeded\` metadata from the formatter with configType:${configType}`, async () => {
271+
const exit = await cli.execute(
272+
`--no-ignore -f json-with-metadata --max-warnings 1 --rule 'quotes: warn' ${flag}`,
273+
"'hello world';",
274+
useFlatConfig
275+
);
276+
277+
assert.strictEqual(exit, 0);
278+
279+
const { metadata } = JSON.parse(log.info.args[0][0]);
280+
281+
assert.notProperty(metadata, "maxWarningsExceeded");
282+
});
283+
});
284+
});
285+
247286
describe("when given an invalid built-in formatter name", () => {
248287
it(`should execute with error: with configType:${configType}`, async () => {
249288
const filePath = getFixturePath("passing.js");

0 commit comments

Comments
 (0)