Skip to content

Commit 680f8d4

Browse files
committed
Add --max-warnings and --quiet command line arguments #116 #114
1 parent 488d230 commit 680f8d4

File tree

7 files changed

+168
-18
lines changed

7 files changed

+168
-18
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,14 @@ If you omit `zlinter-enable`, all lines until EOF will be disabled.
287287
### Command-Line Arguments
288288

289289
```shell
290-
zig build lint -- [--include <path> ...] [--exclude <path> ...] [--filter <path> ...] [--rule <name> ...] [--fix]
290+
zig build lint -- [--include <path> ...] [--exclude <path> ...] [--filter <path> ...] [--rule <name> ...] [--fix] [--quiet] [--max-warnings <u32>]
291291
```
292292

293293
- `--include` run the linter on these path ignoring the includes and excludes defined in the `build.zig` forcing these paths to be resolved and linted (if they exist).
294294
- `--exclude` exclude these paths from linting. This argument will be used in conjunction with the excludes defined in the `build.zig` unless used with `--include`.
295295
- `--filter` used to filter the run to a specific set of already resolved paths. Unlike `--include` this leaves the includes and excludes defined in the `build.zig` as is.
296+
- `--quiet` only report errors (not warnings).
297+
- `--max-warnings` fail if there are more than this number of warnings.
296298
- `--fix` used to automatically fix some issues (e.g., removal of unused container declarations) - **Only use this feature if you use source control as it can result loss of code!**
297299

298300
For example

src/exe/run_linter.zig

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,8 @@ fn run(
202202
switch (args.format) {
203203
.default => &default_formatter.formatter,
204204
},
205+
args.quiet,
206+
args.max_warnings,
205207
);
206208
}
207209

@@ -402,32 +404,49 @@ fn runFormatter(
402404
output_writer: *std.Io.Writer,
403405
output_tty: zlinter.ansi.Tty,
404406
formatter: *const zlinter.formatters.Formatter,
407+
quiet: bool,
408+
max_warnings: ?u32,
405409
) !RunResult {
406410
var arena = std.heap.ArenaAllocator.init(gpa);
407411
defer arena.deinit();
408412
const arena_allocator = arena.allocator();
409413

410-
var flattened = shims.ArrayList(zlinter.results.LintResult).empty;
414+
var run_result: RunResult = .success;
415+
var warning_count: usize = 0;
416+
var results_count: usize = 0;
411417
for (file_lint_problems.values()) |results| {
412-
try flattened.appendSlice(arena_allocator, results);
413-
}
414-
415-
const run_result: RunResult = run_result: {
416-
for (flattened.items) |result| {
418+
results_count += results.len;
419+
for (results) |result| {
417420
for (result.problems) |problem| {
418-
if (problem.severity == .@"error" and !problem.disabled_by_comment) {
419-
break :run_result .lint_error;
421+
if (problem.disabled_by_comment) continue;
422+
switch (problem.severity) {
423+
.@"error" => run_result = .lint_error,
424+
.warning => warning_count += 1,
425+
.off => {},
420426
}
421427
}
422428
}
423-
break :run_result .success;
424-
};
429+
}
430+
if (max_warnings) |max| {
431+
if (warning_count > max) {
432+
run_result = .lint_error;
433+
}
434+
}
435+
436+
var flattened = try shims.ArrayList(zlinter.results.LintResult).initCapacity(
437+
arena_allocator,
438+
results_count,
439+
);
440+
for (file_lint_problems.values()) |results| {
441+
flattened.appendSliceAssumeCapacity(results);
442+
}
425443

426444
try formatter.format(.{
427445
.results = try flattened.toOwnedSlice(arena_allocator),
428446
.dir = dir,
429447
.arena = arena_allocator,
430448
.tty = output_tty,
449+
.min_severity = if (quiet) .@"error" else .warning,
431450
}, output_writer);
432451

433452
return run_result;

src/lib/Args.zig

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ zig_lib_directory: ?[]const u8 = null,
1717
/// fix any discovered issues instead of reporting them.
1818
fix: bool = false,
1919

20+
/// If set to true only errors will be reported. Warnings are silently ignored.
21+
///
22+
/// By default, zlinter reports both warnings and errors. In some workflows,
23+
/// you may only want to report errors and ignore warnings — for example, in
24+
/// continuous Integration (CI) pipelines where only correctness issues.
25+
quiet: bool = false,
26+
27+
/// If set, zlinter will fail (non-zero exit code) if more than the given number
28+
/// of warnings are reported.
29+
max_warnings: ?u32 = null,
30+
2031
/// Only lint or fix (if using the fix argument) the given files. These
2132
/// are owned by the struct and should be freed by calling deinit. This will
2233
/// replace any file resolution provided by the build file.
@@ -135,6 +146,7 @@ pub fn allocParse(
135146
const State = enum {
136147
parsing,
137148
fix_arg,
149+
quiet_arg,
138150
verbose_arg,
139151
help_arg,
140152
zig_exe_arg,
@@ -149,11 +161,13 @@ pub fn allocParse(
149161
rule_config_arg,
150162
stdin_arg,
151163
fix_passes_arg,
164+
max_warnings_arg,
152165
};
153166

154167
const flags: std.StaticStringMap(State) = .initComptime(.{
155168
.{ "", .parsing },
156169
.{ "--fix", .fix_arg },
170+
.{ "--quiet", .quiet_arg },
157171
.{ "--verbose", .verbose_arg },
158172
.{ "--rule", .rule_arg },
159173
.{ "--include", .include_path_arg },
@@ -168,6 +182,7 @@ pub fn allocParse(
168182
.{ "--help", .help_arg },
169183
.{ "-h", .help_arg },
170184
.{ "--fix-passes", .fix_passes_arg },
185+
.{ "--max-warnings", .max_warnings_arg },
171186
});
172187

173188
state: switch (State.parsing) {
@@ -272,6 +287,22 @@ pub fn allocParse(
272287
}});
273288
return error.InvalidArgs;
274289
},
290+
.max_warnings_arg => {
291+
index += 1;
292+
293+
if (index == args.len) {
294+
rendering.process_printer.println(.err, "--max-warnings missing value", .{});
295+
return error.InvalidArgs;
296+
}
297+
298+
const error_message = "--max-warnings expects a u32";
299+
lint_args.max_warnings = std.fmt.parseInt(u32, args[index], 10) catch {
300+
rendering.process_printer.println(.err, error_message, .{});
301+
return error.InvalidArgs;
302+
};
303+
304+
continue :state State.parsing;
305+
},
275306
.fix_passes_arg => {
276307
index += 1;
277308
if (index == args.len) {
@@ -295,6 +326,10 @@ pub fn allocParse(
295326
lint_args.fix = true;
296327
continue :state State.parsing;
297328
},
329+
.quiet_arg => {
330+
lint_args.quiet = true;
331+
continue :state State.parsing;
332+
},
298333
.verbose_arg => {
299334
lint_args.verbose = true;
300335
continue :state State.parsing;
@@ -388,6 +423,8 @@ pub fn printHelp(printer: *rendering.Printer) void {
388423
.{ "--include", "Only lint these paths, ignoring build.zig includes/excludes" },
389424
.{ "--exclude", "Skip linting for these paths" },
390425
.{ "--filter", "Limit linting to the specified resolved paths" },
426+
.{ "--quiet", "Only report errors (not warnings)" },
427+
.{ "--max-warnings", "Fail if there are more than this number of warnings" },
391428
.{ "--fix", "Automatically fix some issues (only use with source control)" },
392429
.{ "--fix-passes", std.fmt.comptimePrint("Repeat fix this many times or until no more fixes are applied (Default {d})", .{default_fix_passes}) },
393430
};
@@ -396,7 +433,7 @@ pub fn printHelp(printer: *rendering.Printer) void {
396433
inline for (0..flags.len) |i| width = @max(flags[i][0].len, width);
397434

398435
printer.print(.out, "{s}Usage:{s} ", .{ printer.tty.ansiOrEmpty(&.{ .underline, .bold }), printer.tty.ansiOrEmpty(&.{.reset}) });
399-
printer.print(.out, "zig build <lint step> -- [--include <path>...] [--exclude <path>...] [--filter <path>...] [--rule <name>...] [--fix]\n\n", .{});
436+
printer.print(.out, "zig build <lint step> -- [--include <path>...] [--exclude <path>...] [--filter <path>...] [--rule <name>...] [--fix] [--quiet] [--max-warnings <u32>]\n\n", .{});
400437
printer.print(.out, "{s}Options:{s}\n", .{ printer.tty.ansiOrEmpty(&.{ .underline, .bold }), printer.tty.ansiOrEmpty(&.{.reset}) });
401438
for (flags) |tuple| {
402439
printer.print(
@@ -410,6 +447,7 @@ pub fn printHelp(printer: *rendering.Printer) void {
410447
},
411448
);
412449
}
450+
printer.flush() catch @panic("Failed to flush help docs");
413451
}
414452

415453
fn notArgKey(arg: []const u8) bool {
@@ -449,6 +487,22 @@ test "allocParse with fix arg" {
449487
}, args);
450488
}
451489

490+
test "allocParse with quiet arg" {
491+
var stdin_fbs = std.Io.Reader.fixed("");
492+
493+
const args = try allocParse(
494+
testing.cliArgs(&.{"--quiet"}),
495+
&.{},
496+
std.testing.allocator,
497+
&stdin_fbs,
498+
);
499+
defer args.deinit(std.testing.allocator);
500+
501+
try std.testing.expectEqualDeep(Args{
502+
.quiet = true,
503+
}, args);
504+
}
505+
452506
test "allocParse with verbose arg" {
453507
var stdin_fbs = std.Io.Reader.fixed("");
454508

@@ -838,7 +892,7 @@ test "allocParse with fix passes missing arg" {
838892
}
839893

840894
test "allocParse with invalid fix passes arg" {
841-
inline for (&.{ "-1", "256", "a" }) |arg| {
895+
inline for (&.{ "-1", "0", "256", "a" }) |arg| {
842896
var stdin_fbs = std.Io.Reader.fixed("");
843897

844898
var stderr_sink: std.Io.Writer.Allocating = .init(std.testing.allocator);
@@ -1027,6 +1081,74 @@ test "allocParse with with missing rule config rule config path" {
10271081
try std.testing.expectEqualStrings("--rule-config arg missing zon file path\n", stderr_sink.written());
10281082
}
10291083

1084+
test "allocParse with min --max-warnings arg" {
1085+
var stdin_fbs = std.Io.Reader.fixed("");
1086+
1087+
const args = try allocParse(
1088+
testing.cliArgs(&.{ "--max-warnings", "0" }),
1089+
&.{},
1090+
std.testing.allocator,
1091+
&stdin_fbs,
1092+
);
1093+
defer args.deinit(std.testing.allocator);
1094+
1095+
try std.testing.expectEqualDeep(Args{
1096+
.max_warnings = 0,
1097+
}, args);
1098+
}
1099+
1100+
test "allocParse with max --max-warnings arg" {
1101+
var stdin_fbs = std.Io.Reader.fixed("");
1102+
1103+
const args = try allocParse(
1104+
testing.cliArgs(&.{ "--max-warnings", "4294967295" }),
1105+
&.{},
1106+
std.testing.allocator,
1107+
&stdin_fbs,
1108+
);
1109+
defer args.deinit(std.testing.allocator);
1110+
1111+
try std.testing.expectEqualDeep(Args{
1112+
.max_warnings = 4294967295,
1113+
}, args);
1114+
}
1115+
1116+
test "allocParse with fix --max-warnings arg" {
1117+
var stdin_fbs = std.Io.Reader.fixed("");
1118+
1119+
var stderr_sink: std.Io.Writer.Allocating = .init(std.testing.allocator);
1120+
defer stderr_sink.deinit();
1121+
rendering.process_printer.stderr = &stderr_sink.writer;
1122+
1123+
try std.testing.expectError(error.InvalidArgs, allocParse(
1124+
testing.cliArgs(&.{"--max-warnings"}),
1125+
&.{},
1126+
std.testing.allocator,
1127+
&stdin_fbs,
1128+
));
1129+
1130+
try std.testing.expectEqualStrings("--max-warnings missing value\n", stderr_sink.written());
1131+
}
1132+
1133+
test "allocParse with invalid --max-warnings arg" {
1134+
inline for (&.{ "-1", "4294967296", "a" }) |arg| {
1135+
var stdin_fbs = std.Io.Reader.fixed("");
1136+
1137+
var stderr_sink: std.Io.Writer.Allocating = .init(std.testing.allocator);
1138+
defer stderr_sink.deinit();
1139+
rendering.process_printer.stderr = &stderr_sink.writer;
1140+
1141+
try std.testing.expectError(error.InvalidArgs, allocParse(
1142+
testing.cliArgs(&.{ "--max-warnings", arg }),
1143+
&.{},
1144+
std.testing.allocator,
1145+
&stdin_fbs,
1146+
));
1147+
1148+
try std.testing.expectEqualStrings("--max-warnings expects a u32\n", stderr_sink.written());
1149+
}
1150+
}
1151+
10301152
const testing = struct {
10311153
inline fn cliArgs(args: []const [:0]const u8) [][:0]u8 {
10321154
assertTestOnly();

src/lib/explorer.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ fn errorsToJson(tree: Ast, arena: std.mem.Allocator) !std.json.Array {
135135
try json_error.put("message", .{ .string = try render_backing.toOwnedSlice(arena) });
136136
},
137137
.@"0.15" => {
138-
var aw = std.io.Writer.Allocating.init(arena);
138+
var aw = std.Io.Writer.Allocating.init(arena);
139139
try tree.renderError(e, &aw.writer);
140140
try json_error.put("message", .{ .string = try aw.toOwnedSlice() });
141141
},

src/lib/formatters/DefaultFormatter.zig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ fn format(formatter: *const Formatter, input: Formatter.FormatInput, writer: *st
3131
) catch |e| return logAndReturnWriteFailure("Render", e);
3232

3333
for (file_result.problems) |problem| {
34+
if (@intFromEnum(problem.severity) < @intFromEnum(input.min_severity)) {
35+
continue;
36+
}
37+
3438
if (problem.disabled_by_comment) {
3539
total_disabled_by_comment += 1;
3640
continue;

src/lib/formatters/Formatter.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ pub const FormatInput = struct {
88
arena: std.mem.Allocator,
99

1010
tty: zlinter.ansi.Tty = .no_color,
11+
12+
/// Only print this severity and above. e.g., set to error to only format errors
13+
min_severity: zlinter.rules.LintProblemSeverity = .warning,
1114
};
1215

1316
pub const Error = error{

src/lib/rules.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,13 @@ pub const LenAndSeverity = struct {
188188
};
189189
};
190190

191-
pub const LintProblemSeverity = enum {
191+
pub const LintProblemSeverity = enum(u8) {
192192
/// Exit zero
193-
off,
193+
off = 0,
194194
/// Exit zero with warning
195-
warning,
195+
warning = 1,
196196
/// Exit non-zero
197-
@"error",
197+
@"error" = 2,
198198

199199
pub inline fn name(
200200
self: LintProblemSeverity,

0 commit comments

Comments
 (0)