diff --git a/README.md b/README.md index 60c18c732..70def16b9 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ - [`--details`](#--details) - [`--format`](#--format) - [`--fix`](#--fix) + - [`--quiet`](#--quiet) - [`--ignore-pattern`](#--ignore-pattern) - [`--config`](#--config) - [`--ui5-config`](#--ui5-config) @@ -194,6 +195,15 @@ UI5LINT_FIX_DRY_RUN=true ui5lint --fix In this mode, the linter will show the messages after the fixes would have been applied but will not actually change the files. +#### `--quiet` + +Report errors only, hiding warnings. Similar to ESLint's `--quiet` option. + +**Example:** +```sh +ui5lint --quiet +``` + #### `--ignore-pattern` Pattern/files that will be ignored during linting. Can also be defined in `ui5lint.config.js`. diff --git a/src/cli/base.ts b/src/cli/base.ts index 05d2e8c86..934b3e083 100644 --- a/src/cli/base.ts +++ b/src/cli/base.ts @@ -10,6 +10,7 @@ import {isLogLevelEnabled} from "@ui5/logger"; import ConsoleWriter from "@ui5/logger/writers/Console"; import {getVersion} from "./version.js"; import {ui5lint} from "../index.js"; +import {LintMessageSeverity} from "../linter/messages.js"; export interface LinterArg { coverage: boolean; @@ -21,6 +22,7 @@ export interface LinterArg { format: string; config?: string; ui5Config?: string; + quiet: boolean; } // yargs type definition is missing the "middlewares" property for the CommandModule type @@ -106,6 +108,12 @@ const lintCommand: FixedCommandModule = { type: "string", choices: ["stylish", "json", "markdown"], }) + .option("quiet", { + describe: "Report errors only", + type: "boolean", + default: false, + alias: "q", + }) .option("ui5-config", { describe: "Set a custom path for the UI5 Config (default: './ui5.yaml' if that file exists)", type: "string", @@ -147,6 +155,7 @@ async function handleLint(argv: ArgumentsCamelCase) { format, config, ui5Config, + quiet, } = argv; let profile; @@ -170,6 +179,21 @@ async function handleLint(argv: ArgumentsCamelCase) { ui5Config, }); + // Apply quiet mode filtering directly to the results if needed + if (quiet) { + // Filter out warnings from all result objects + for (const result of res) { + // Keep only error messages (severity === 2) + result.messages = result.messages.filter((msg) => msg.severity === LintMessageSeverity.Error); + // Reset warning counts + result.warningCount = 0; + // Reset fixableWarningCount if it exists + if ("fixableWarningCount" in result) { + result.fixableWarningCount = 0; + } + } + } + if (coverage) { const coverageFormatter = new Coverage(); await writeFile("ui5lint-report.html", await coverageFormatter.format(res, new Date())); @@ -177,15 +201,15 @@ async function handleLint(argv: ArgumentsCamelCase) { if (format === "json") { const jsonFormatter = new Json(); - process.stdout.write(jsonFormatter.format(res, details)); + process.stdout.write(jsonFormatter.format(res, details, quiet)); process.stdout.write("\n"); } else if (format === "markdown") { const markdownFormatter = new Markdown(); - process.stdout.write(markdownFormatter.format(res, details, getVersion(), fix)); + process.stdout.write(markdownFormatter.format(res, details, getVersion(), fix, quiet)); process.stdout.write("\n"); } else if (format === "" || format === "stylish") { const textFormatter = new Text(rootDir); - process.stderr.write(textFormatter.format(res, details, fix)); + process.stderr.write(textFormatter.format(res, details, fix, quiet)); } // Stop profiling after CLI finished execution if (profile) { diff --git a/src/formatter/json.ts b/src/formatter/json.ts index b4b5a9f07..f6077d172 100644 --- a/src/formatter/json.ts +++ b/src/formatter/json.ts @@ -1,7 +1,7 @@ import {LintMessage, LintResult} from "../linter/LinterContext.js"; export class Json { - format(lintResults: LintResult[], showDetails: boolean) { + format(lintResults: LintResult[], showDetails: boolean, _quiet = false) { const jsonFormattedResults: Pick< LintResult, "filePath" diff --git a/src/formatter/markdown.ts b/src/formatter/markdown.ts index 712178c44..8bba0173c 100644 --- a/src/formatter/markdown.ts +++ b/src/formatter/markdown.ts @@ -2,7 +2,7 @@ import {LintResult, LintMessage} from "../linter/LinterContext.js"; import {LintMessageSeverity} from "../linter/messages.js"; export class Markdown { - format(lintResults: LintResult[], showDetails: boolean, version: string, autofix: boolean): string { + format(lintResults: LintResult[], showDetails: boolean, version: string, autofix: boolean, quiet = false): string { let totalErrorCount = 0; let totalWarningCount = 0; let totalFatalErrorCount = 0; @@ -65,9 +65,17 @@ export class Markdown { }); let summary = "## Summary\n\n"; - summary += - `> ${totalErrorCount + totalWarningCount} problems ` + - `(${totalErrorCount} errors, ${totalWarningCount} warnings) \n`; + const errorsText = `${totalErrorCount} ${totalErrorCount === 1 ? "error" : "errors"}`; + let warningsText = ""; + if (!quiet) { + warningsText = `, ${totalWarningCount} ${totalWarningCount === 1 ? "warning" : "warnings"}`; + } + + const totalCount = quiet ? totalErrorCount : totalErrorCount + totalWarningCount; + const problemsText = `${totalCount} ${totalCount === 1 ? "problem" : "problems"}`; + + summary += `> ${problemsText} (${errorsText}${warningsText}) \n`; + if (totalFatalErrorCount) { summary += `> **${totalFatalErrorCount} fatal errors**\n`; } diff --git a/src/formatter/text.ts b/src/formatter/text.ts index 2ccd424ad..753dfbf74 100644 --- a/src/formatter/text.ts +++ b/src/formatter/text.ts @@ -44,7 +44,7 @@ export class Text { constructor(private readonly cwd: string) { } - format(lintResults: LintResult[], showDetails: boolean, autofix: boolean) { + format(lintResults: LintResult[], showDetails: boolean, autofix: boolean, quiet = false) { this.#writeln(`UI5 linter report:`); this.#writeln(""); let totalErrorCount = 0; @@ -101,12 +101,19 @@ export class Text { summaryColor = chalk.yellow; } + const errorsText = `${totalErrorCount} ${totalErrorCount === 1 ? "error" : "errors"}`; + let warningsText = ""; + if (!quiet) { + warningsText = `, ${totalWarningCount} ${totalWarningCount === 1 ? "warning" : "warnings"}`; + } + + const totalCount = quiet ? totalErrorCount : totalErrorCount + totalWarningCount; + const problemsText = `${totalCount} ${totalCount === 1 ? "problem" : "problems"}`; + this.#writeln( - summaryColor( - `${totalErrorCount + totalWarningCount} problems ` + - `(${totalErrorCount} errors, ${totalWarningCount} warnings)` - ) + summaryColor(`${problemsText} (${errorsText}${warningsText})`) ); + if (!autofix && (totalErrorCount + totalWarningCount > 0)) { this.#writeln(" Run \"ui5lint --fix\" to resolve all auto-fixable problems\n"); } diff --git a/test/lib/cli/base.integration.ts b/test/lib/cli/base.integration.ts index 69defc509..effd5f2f2 100644 --- a/test/lib/cli/base.integration.ts +++ b/test/lib/cli/base.integration.ts @@ -77,3 +77,116 @@ test.serial("ui5lint --format markdown", async (t) => { const resultProcessStdoutNL = processStdoutWriteStub.secondCall.firstArg; t.is(resultProcessStdoutNL, "\n", "second write only adds a single newline"); }); + +// Test for --quiet option with default formatter +test.serial("ui5lint --quiet", async (t) => { + const {cli, consoleLogStub, processCwdStub, processExitStub} = t.context; + + // We need to manually create a stderr stub since it's not in the context + const stderrWriteStub = sinon.stub(process.stderr, "write").returns(true); + + try { + // First run without quiet + await cli.parseAsync([]); + const normalOutput = stderrWriteStub.firstCall.firstArg; + t.true(normalOutput.length > 0, "Normal output is not empty"); + + // Reset the stub's history before the second run + stderrWriteStub.resetHistory(); + + // Then run with quiet + await cli.parseAsync(["--quiet"]); + const quietOutput = stderrWriteStub.firstCall.firstArg; + t.true(quietOutput.length > 0, "Quiet output is not empty"); + + t.is(consoleLogStub.callCount, 0, "console.log should not be used"); + t.true(processCwdStub.callCount > 0, "process.cwd was called"); + t.is(processExitStub.callCount, 0, "process.exit got never called"); + + // Reset immediately + process.exitCode = 0; + + // Check that quiet output is different from normal output + t.notDeepEqual(quietOutput, normalOutput, "Quiet output differs from normal output"); + + // Quiet output should not contain the word "warnings" in the summary + t.false(quietOutput.includes(" warnings)"), "Quiet output should not mention warnings count"); + } finally { + // Always restore the stub + stderrWriteStub.restore(); + // Ensure process.exitCode is reset + process.exitCode = 0; + } +}); + +// Test for --quiet option with JSON format +test.serial("ui5lint --quiet --format json", async (t) => { + const {cli, processExitStub, processStdoutWriteStub} = t.context; + + // Reset the stub's history + processStdoutWriteStub.resetHistory(); + + // First run without quiet + await cli.parseAsync(["--format", "json"]); + const normalJsonOutput = processStdoutWriteStub.firstCall.firstArg; + t.true(normalJsonOutput.length > 0, "Normal JSON output is not empty"); + + // Reset history for second run + processStdoutWriteStub.resetHistory(); + + // Run with quiet + await cli.parseAsync(["--quiet", "--format", "json"]); + const quietJsonOutput = processStdoutWriteStub.firstCall.firstArg; + t.true(quietJsonOutput.length > 0, "Quiet JSON output is not empty"); + + t.is(processExitStub.callCount, 0, "process.exit got never called"); + process.exitCode = 0; // Reset immediately + + // Parse and compare results + const normalJson = JSON.parse(normalJsonOutput) as LintResult[]; + const quietJson = JSON.parse(quietJsonOutput) as LintResult[]; + + // Verify quiet output has warningCount set to 0 + t.true(quietJson.some((file) => file.warningCount === 0), + "Quiet JSON output has warningCount set to 0"); + + // Compare with normalJson if it has any warnings + if (normalJson.some((file) => file.warningCount > 0)) { + t.notDeepEqual(normalJson, quietJson, "Quiet JSON output differs from normal JSON output"); + } +}); + +// Test for --quiet option with Markdown format +test.serial("ui5lint --quiet --format markdown", async (t) => { + const {cli, processExitStub, processStdoutWriteStub} = t.context; + + // Reset the stub's history + processStdoutWriteStub.resetHistory(); + + // First run without quiet + await cli.parseAsync(["--format", "markdown"]); + const normalMarkdownOutput = processStdoutWriteStub.firstCall.firstArg; + t.true(normalMarkdownOutput.length > 0, "Normal Markdown output is not empty"); + + // Reset history for second run + processStdoutWriteStub.resetHistory(); + + // Run with quiet + await cli.parseAsync(["--quiet", "--format", "markdown"]); + const quietMarkdownOutput = processStdoutWriteStub.firstCall.firstArg; + t.true(quietMarkdownOutput.length > 0, "Quiet Markdown output is not empty"); + + t.is(processExitStub.callCount, 0, "process.exit got never called"); + process.exitCode = 0; // Reset immediately + + // Check outputs + const errorMsg = "Quiet Markdown output differs from normal output"; + t.notDeepEqual(quietMarkdownOutput, normalMarkdownOutput, errorMsg); + + // Quiet output should not contain the word "warnings" in the summary + const warnMsg = "Quiet Markdown output should not mention warnings"; + t.false(quietMarkdownOutput.includes(" warnings"), warnMsg); +}); + +// Always reset exit code at the end +process.exitCode = 0; diff --git a/test/lib/cli/base.ts b/test/lib/cli/base.ts index dcefc8f7d..4cfc676b1 100644 --- a/test/lib/cli/base.ts +++ b/test/lib/cli/base.ts @@ -189,6 +189,41 @@ test.serial("ui5lint --ui5-config", async (t) => { }); }); +test.serial("ui5lint --quiet", async (t) => { + const {cli, ui5lint, formatText} = t.context; + + // Create a mock result with both errors and warnings + const lintResultWithErrorsAndWarnings: LintResult = { + filePath: "test.js", + messages: [ + {ruleId: "rule1", severity: 1, message: "Warning message"}, // Warning + {ruleId: "rule2", severity: 2, message: "Error message"}, // Error + ], + coverageInfo: [], + errorCount: 1, + fatalErrorCount: 0, + warningCount: 1, + }; + + // Override the default result with our custom one + ui5lint.resolves([lintResultWithErrorsAndWarnings]); + + await cli.parseAsync(["--quiet"]); + + t.true(ui5lint.calledOnce, "Linter is called"); + + // Verify that formatText is called with filtered results containing only errors + t.true(formatText.calledOnce, "Text formatter has been called"); + + const formatterResults = formatText.getCall(0).args[0]; + t.is(formatterResults[0].messages.length, 1, "Only error messages are included"); + t.is(formatterResults[0].messages[0].severity, 2, "Only messages with severity 2 (error) are kept"); + t.is(formatterResults[0].warningCount, 0, "Warning count is reset to 0"); + t.is(process.exitCode, 1, "Exit code is reset to 1"); + // reset process.exitCode + process.exitCode = 0; +}); + test.serial("ui5lint path/to/file.js glob/**/*", async (t) => { const {cli, ui5lint} = t.context; diff --git a/test/lib/formatter/snapshots/text.ts.md b/test/lib/formatter/snapshots/text.ts.md index 1341ea963..1866c2721 100644 --- a/test/lib/formatter/snapshots/text.ts.md +++ b/test/lib/formatter/snapshots/text.ts.md @@ -13,7 +13,7 @@ Generated by [AVA](https://avajs.dev). /Test.js␊ 5:1 error Call to deprecated function 'attachInit' of class 'Core'. Details: (since 1.118) - Please use {@link sap.ui.core.Core.ready Core.ready} instead. no-deprecated-api␊ ␊ - 1 problems (1 errors, 0 warnings)␊ + 1 problem (1 error, 0 warnings)␊ Run "ui5lint --fix" to resolve all auto-fixable problems␊ ␊ ` @@ -27,7 +27,7 @@ Generated by [AVA](https://avajs.dev). /Test.js␊ 5:1 error Call to deprecated function 'attachInit' of class 'Core' no-deprecated-api␊ ␊ - 1 problems (1 errors, 0 warnings)␊ + 1 problem (1 error, 0 warnings)␊ Run "ui5lint --fix" to resolve all auto-fixable problems␊ ␊ ␊ diff --git a/test/lib/formatter/snapshots/text.ts.snap b/test/lib/formatter/snapshots/text.ts.snap index 0f94af4d3..3a70431a6 100644 Binary files a/test/lib/formatter/snapshots/text.ts.snap and b/test/lib/formatter/snapshots/text.ts.snap differ