Skip to content

Commit 6c94b01

Browse files
feat(cli): Add 'quiet' option to report errors only (#629)
UI5 linter reports findings declared as warnings and errors. There are scenarios where only findings declared as an error should be reported. ESLint offers also an CLI option "quiet" to achieve this. JIRA: CPOUI5FOUNDATION-837 --------- Co-authored-by: Merlin Beutlberger <m.beutlberger@sap.com>
1 parent 746f848 commit 6c94b01

9 files changed

Lines changed: 212 additions & 15 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- [`--details`](#--details)
2222
- [`--format`](#--format)
2323
- [`--fix`](#--fix)
24+
- [`--quiet`](#--quiet)
2425
- [`--ignore-pattern`](#--ignore-pattern)
2526
- [`--config`](#--config)
2627
- [`--ui5-config`](#--ui5-config)
@@ -194,6 +195,15 @@ UI5LINT_FIX_DRY_RUN=true ui5lint --fix
194195

195196
In this mode, the linter will show the messages after the fixes would have been applied but will not actually change the files.
196197

198+
#### `--quiet`
199+
200+
Report errors only, hiding warnings. Similar to ESLint's `--quiet` option.
201+
202+
**Example:**
203+
```sh
204+
ui5lint --quiet
205+
```
206+
197207
#### `--ignore-pattern`
198208

199209
Pattern/files that will be ignored during linting. Can also be defined in `ui5lint.config.js`.

src/cli/base.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {isLogLevelEnabled} from "@ui5/logger";
1010
import ConsoleWriter from "@ui5/logger/writers/Console";
1111
import {getVersion} from "./version.js";
1212
import {ui5lint} from "../index.js";
13+
import {LintMessageSeverity} from "../linter/messages.js";
1314

1415
export interface LinterArg {
1516
coverage: boolean;
@@ -21,6 +22,7 @@ export interface LinterArg {
2122
format: string;
2223
config?: string;
2324
ui5Config?: string;
25+
quiet: boolean;
2426
}
2527

2628
// yargs type definition is missing the "middlewares" property for the CommandModule type
@@ -106,6 +108,12 @@ const lintCommand: FixedCommandModule<object, LinterArg> = {
106108
type: "string",
107109
choices: ["stylish", "json", "markdown"],
108110
})
111+
.option("quiet", {
112+
describe: "Report errors only",
113+
type: "boolean",
114+
default: false,
115+
alias: "q",
116+
})
109117
.option("ui5-config", {
110118
describe: "Set a custom path for the UI5 Config (default: './ui5.yaml' if that file exists)",
111119
type: "string",
@@ -147,6 +155,7 @@ async function handleLint(argv: ArgumentsCamelCase<LinterArg>) {
147155
format,
148156
config,
149157
ui5Config,
158+
quiet,
150159
} = argv;
151160

152161
let profile;
@@ -170,22 +179,37 @@ async function handleLint(argv: ArgumentsCamelCase<LinterArg>) {
170179
ui5Config,
171180
});
172181

182+
// Apply quiet mode filtering directly to the results if needed
183+
if (quiet) {
184+
// Filter out warnings from all result objects
185+
for (const result of res) {
186+
// Keep only error messages (severity === 2)
187+
result.messages = result.messages.filter((msg) => msg.severity === LintMessageSeverity.Error);
188+
// Reset warning counts
189+
result.warningCount = 0;
190+
// Reset fixableWarningCount if it exists
191+
if ("fixableWarningCount" in result) {
192+
result.fixableWarningCount = 0;
193+
}
194+
}
195+
}
196+
173197
if (coverage) {
174198
const coverageFormatter = new Coverage();
175199
await writeFile("ui5lint-report.html", await coverageFormatter.format(res, new Date()));
176200
}
177201

178202
if (format === "json") {
179203
const jsonFormatter = new Json();
180-
process.stdout.write(jsonFormatter.format(res, details));
204+
process.stdout.write(jsonFormatter.format(res, details, quiet));
181205
process.stdout.write("\n");
182206
} else if (format === "markdown") {
183207
const markdownFormatter = new Markdown();
184-
process.stdout.write(markdownFormatter.format(res, details, getVersion(), fix));
208+
process.stdout.write(markdownFormatter.format(res, details, getVersion(), fix, quiet));
185209
process.stdout.write("\n");
186210
} else if (format === "" || format === "stylish") {
187211
const textFormatter = new Text(rootDir);
188-
process.stderr.write(textFormatter.format(res, details, fix));
212+
process.stderr.write(textFormatter.format(res, details, fix, quiet));
189213
}
190214
// Stop profiling after CLI finished execution
191215
if (profile) {

src/formatter/json.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {LintMessage, LintResult} from "../linter/LinterContext.js";
22

33
export class Json {
4-
format(lintResults: LintResult[], showDetails: boolean) {
4+
format(lintResults: LintResult[], showDetails: boolean, _quiet = false) {
55
const jsonFormattedResults: Pick<
66
LintResult,
77
"filePath"

src/formatter/markdown.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {LintResult, LintMessage} from "../linter/LinterContext.js";
22
import {LintMessageSeverity} from "../linter/messages.js";
33

44
export class Markdown {
5-
format(lintResults: LintResult[], showDetails: boolean, version: string, autofix: boolean): string {
5+
format(lintResults: LintResult[], showDetails: boolean, version: string, autofix: boolean, quiet = false): string {
66
let totalErrorCount = 0;
77
let totalWarningCount = 0;
88
let totalFatalErrorCount = 0;
@@ -65,9 +65,17 @@ export class Markdown {
6565
});
6666

6767
let summary = "## Summary\n\n";
68-
summary +=
69-
`> ${totalErrorCount + totalWarningCount} problems ` +
70-
`(${totalErrorCount} errors, ${totalWarningCount} warnings) \n`;
68+
const errorsText = `${totalErrorCount} ${totalErrorCount === 1 ? "error" : "errors"}`;
69+
let warningsText = "";
70+
if (!quiet) {
71+
warningsText = `, ${totalWarningCount} ${totalWarningCount === 1 ? "warning" : "warnings"}`;
72+
}
73+
74+
const totalCount = quiet ? totalErrorCount : totalErrorCount + totalWarningCount;
75+
const problemsText = `${totalCount} ${totalCount === 1 ? "problem" : "problems"}`;
76+
77+
summary += `> ${problemsText} (${errorsText}${warningsText}) \n`;
78+
7179
if (totalFatalErrorCount) {
7280
summary += `> **${totalFatalErrorCount} fatal errors**\n`;
7381
}

src/formatter/text.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class Text {
4444
constructor(private readonly cwd: string) {
4545
}
4646

47-
format(lintResults: LintResult[], showDetails: boolean, autofix: boolean) {
47+
format(lintResults: LintResult[], showDetails: boolean, autofix: boolean, quiet = false) {
4848
this.#writeln(`UI5 linter report:`);
4949
this.#writeln("");
5050
let totalErrorCount = 0;
@@ -101,12 +101,19 @@ export class Text {
101101
summaryColor = chalk.yellow;
102102
}
103103

104+
const errorsText = `${totalErrorCount} ${totalErrorCount === 1 ? "error" : "errors"}`;
105+
let warningsText = "";
106+
if (!quiet) {
107+
warningsText = `, ${totalWarningCount} ${totalWarningCount === 1 ? "warning" : "warnings"}`;
108+
}
109+
110+
const totalCount = quiet ? totalErrorCount : totalErrorCount + totalWarningCount;
111+
const problemsText = `${totalCount} ${totalCount === 1 ? "problem" : "problems"}`;
112+
104113
this.#writeln(
105-
summaryColor(
106-
`${totalErrorCount + totalWarningCount} problems ` +
107-
`(${totalErrorCount} errors, ${totalWarningCount} warnings)`
108-
)
114+
summaryColor(`${problemsText} (${errorsText}${warningsText})`)
109115
);
116+
110117
if (!autofix && (totalErrorCount + totalWarningCount > 0)) {
111118
this.#writeln(" Run \"ui5lint --fix\" to resolve all auto-fixable problems\n");
112119
}

test/lib/cli/base.integration.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,116 @@ test.serial("ui5lint --format markdown", async (t) => {
7777
const resultProcessStdoutNL = processStdoutWriteStub.secondCall.firstArg;
7878
t.is(resultProcessStdoutNL, "\n", "second write only adds a single newline");
7979
});
80+
81+
// Test for --quiet option with default formatter
82+
test.serial("ui5lint --quiet", async (t) => {
83+
const {cli, consoleLogStub, processCwdStub, processExitStub} = t.context;
84+
85+
// We need to manually create a stderr stub since it's not in the context
86+
const stderrWriteStub = sinon.stub(process.stderr, "write").returns(true);
87+
88+
try {
89+
// First run without quiet
90+
await cli.parseAsync([]);
91+
const normalOutput = stderrWriteStub.firstCall.firstArg;
92+
t.true(normalOutput.length > 0, "Normal output is not empty");
93+
94+
// Reset the stub's history before the second run
95+
stderrWriteStub.resetHistory();
96+
97+
// Then run with quiet
98+
await cli.parseAsync(["--quiet"]);
99+
const quietOutput = stderrWriteStub.firstCall.firstArg;
100+
t.true(quietOutput.length > 0, "Quiet output is not empty");
101+
102+
t.is(consoleLogStub.callCount, 0, "console.log should not be used");
103+
t.true(processCwdStub.callCount > 0, "process.cwd was called");
104+
t.is(processExitStub.callCount, 0, "process.exit got never called");
105+
106+
// Reset immediately
107+
process.exitCode = 0;
108+
109+
// Check that quiet output is different from normal output
110+
t.notDeepEqual(quietOutput, normalOutput, "Quiet output differs from normal output");
111+
112+
// Quiet output should not contain the word "warnings" in the summary
113+
t.false(quietOutput.includes(" warnings)"), "Quiet output should not mention warnings count");
114+
} finally {
115+
// Always restore the stub
116+
stderrWriteStub.restore();
117+
// Ensure process.exitCode is reset
118+
process.exitCode = 0;
119+
}
120+
});
121+
122+
// Test for --quiet option with JSON format
123+
test.serial("ui5lint --quiet --format json", async (t) => {
124+
const {cli, processExitStub, processStdoutWriteStub} = t.context;
125+
126+
// Reset the stub's history
127+
processStdoutWriteStub.resetHistory();
128+
129+
// First run without quiet
130+
await cli.parseAsync(["--format", "json"]);
131+
const normalJsonOutput = processStdoutWriteStub.firstCall.firstArg;
132+
t.true(normalJsonOutput.length > 0, "Normal JSON output is not empty");
133+
134+
// Reset history for second run
135+
processStdoutWriteStub.resetHistory();
136+
137+
// Run with quiet
138+
await cli.parseAsync(["--quiet", "--format", "json"]);
139+
const quietJsonOutput = processStdoutWriteStub.firstCall.firstArg;
140+
t.true(quietJsonOutput.length > 0, "Quiet JSON output is not empty");
141+
142+
t.is(processExitStub.callCount, 0, "process.exit got never called");
143+
process.exitCode = 0; // Reset immediately
144+
145+
// Parse and compare results
146+
const normalJson = JSON.parse(normalJsonOutput) as LintResult[];
147+
const quietJson = JSON.parse(quietJsonOutput) as LintResult[];
148+
149+
// Verify quiet output has warningCount set to 0
150+
t.true(quietJson.some((file) => file.warningCount === 0),
151+
"Quiet JSON output has warningCount set to 0");
152+
153+
// Compare with normalJson if it has any warnings
154+
if (normalJson.some((file) => file.warningCount > 0)) {
155+
t.notDeepEqual(normalJson, quietJson, "Quiet JSON output differs from normal JSON output");
156+
}
157+
});
158+
159+
// Test for --quiet option with Markdown format
160+
test.serial("ui5lint --quiet --format markdown", async (t) => {
161+
const {cli, processExitStub, processStdoutWriteStub} = t.context;
162+
163+
// Reset the stub's history
164+
processStdoutWriteStub.resetHistory();
165+
166+
// First run without quiet
167+
await cli.parseAsync(["--format", "markdown"]);
168+
const normalMarkdownOutput = processStdoutWriteStub.firstCall.firstArg;
169+
t.true(normalMarkdownOutput.length > 0, "Normal Markdown output is not empty");
170+
171+
// Reset history for second run
172+
processStdoutWriteStub.resetHistory();
173+
174+
// Run with quiet
175+
await cli.parseAsync(["--quiet", "--format", "markdown"]);
176+
const quietMarkdownOutput = processStdoutWriteStub.firstCall.firstArg;
177+
t.true(quietMarkdownOutput.length > 0, "Quiet Markdown output is not empty");
178+
179+
t.is(processExitStub.callCount, 0, "process.exit got never called");
180+
process.exitCode = 0; // Reset immediately
181+
182+
// Check outputs
183+
const errorMsg = "Quiet Markdown output differs from normal output";
184+
t.notDeepEqual(quietMarkdownOutput, normalMarkdownOutput, errorMsg);
185+
186+
// Quiet output should not contain the word "warnings" in the summary
187+
const warnMsg = "Quiet Markdown output should not mention warnings";
188+
t.false(quietMarkdownOutput.includes(" warnings"), warnMsg);
189+
});
190+
191+
// Always reset exit code at the end
192+
process.exitCode = 0;

test/lib/cli/base.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,41 @@ test.serial("ui5lint --ui5-config", async (t) => {
189189
});
190190
});
191191

192+
test.serial("ui5lint --quiet", async (t) => {
193+
const {cli, ui5lint, formatText} = t.context;
194+
195+
// Create a mock result with both errors and warnings
196+
const lintResultWithErrorsAndWarnings: LintResult = {
197+
filePath: "test.js",
198+
messages: [
199+
{ruleId: "rule1", severity: 1, message: "Warning message"}, // Warning
200+
{ruleId: "rule2", severity: 2, message: "Error message"}, // Error
201+
],
202+
coverageInfo: [],
203+
errorCount: 1,
204+
fatalErrorCount: 0,
205+
warningCount: 1,
206+
};
207+
208+
// Override the default result with our custom one
209+
ui5lint.resolves([lintResultWithErrorsAndWarnings]);
210+
211+
await cli.parseAsync(["--quiet"]);
212+
213+
t.true(ui5lint.calledOnce, "Linter is called");
214+
215+
// Verify that formatText is called with filtered results containing only errors
216+
t.true(formatText.calledOnce, "Text formatter has been called");
217+
218+
const formatterResults = formatText.getCall(0).args[0];
219+
t.is(formatterResults[0].messages.length, 1, "Only error messages are included");
220+
t.is(formatterResults[0].messages[0].severity, 2, "Only messages with severity 2 (error) are kept");
221+
t.is(formatterResults[0].warningCount, 0, "Warning count is reset to 0");
222+
t.is(process.exitCode, 1, "Exit code is reset to 1");
223+
// reset process.exitCode
224+
process.exitCode = 0;
225+
});
226+
192227
test.serial("ui5lint path/to/file.js glob/**/*", async (t) => {
193228
const {cli, ui5lint} = t.context;
194229

test/lib/formatter/snapshots/text.ts.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Generated by [AVA](https://avajs.dev).
1313
<base path>/Test.js␊
1414
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␊
1515
16-
1 problems (1 errors, 0 warnings)␊
16+
1 problem (1 error, 0 warnings)␊
1717
Run "ui5lint --fix" to resolve all auto-fixable problems␊
1818
1919
`
@@ -27,7 +27,7 @@ Generated by [AVA](https://avajs.dev).
2727
<base path>/Test.js␊
2828
5:1 error Call to deprecated function 'attachInit' of class 'Core' no-deprecated-api␊
2929
30-
1 problems (1 errors, 0 warnings)␊
30+
1 problem (1 error, 0 warnings)␊
3131
Run "ui5lint --fix" to resolve all auto-fixable problems␊
3232
3333
-1 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)