authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-06-25 13:10:59-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-06-26 00:53:28+02:00
log7db2ef6104921c42ce42e8edda848160eda8973d
treed29192cb7bc487c24475c7731c85a718ad4290d3
parent56e313b288928e998e186286a2ddc3211c20aa71

zig fmt: add simple tool for measuring complexity

can be used to share how an edit to a source file increases or reduces complexity with a heuristic that is more insightful than line count

1 files changed, 32 insertions(+), 14 deletions(-)

src/fmt.zig+32-14
......@@ -23,6 +23,7 @@ const usage_fmt =
2323 \\ --ast-check Run zig ast-check on every file
2424 \\ --exclude [file] Exclude file or directory from formatting
2525 \\ --zon Treat all input files as ZON, regardless of file extension
26 \\ --complexity Print a complexity report for each file as well as total
2627 \\
2728 \\
2829;
......@@ -39,6 +40,10 @@ const Fmt = struct {
3940 out_buffer: std.Io.Writer.Allocating,
4041 stdout_writer: *Io.File.Writer,
4142
43 complexity: bool,
44 total_tokens: u64,
45 total_nodes: u64,
46
4247 const SeenMap = std.AutoHashMap(Io.File.INode, void);
4348};
4449
......@@ -48,8 +53,11 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
4853 var check_flag = false;
4954 var check_ast_flag = false;
5055 var force_zon = false;
56 var complexity = false;
57
5158 var input_files = std.array_list.Managed([]const u8).init(gpa);
5259 defer input_files.deinit();
60
5361 var excluded_files = std.array_list.Managed([]const u8).init(gpa);
5462 defer excluded_files.deinit();
5563
......@@ -68,7 +76,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
6876 i += 1;
6977 const next_arg = args[i];
7078 color = std.meta.stringToEnum(Color, next_arg) orelse {
71 fatal("expected [auto|on|off] after --color, found '{s}'", .{next_arg});
79 fatal("expected [auto|on|off] after --color, found {q}", .{next_arg});
7280 };
7381 } else if (mem.eql(u8, arg, "--stdin")) {
7482 stdin_flag = true;
......@@ -76,6 +84,8 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
7684 check_flag = true;
7785 } else if (mem.eql(u8, arg, "--ast-check")) {
7886 check_ast_flag = true;
87 } else if (mem.eql(u8, arg, "--complexity")) {
88 complexity = true;
7989 } else if (mem.eql(u8, arg, "--exclude")) {
8090 if (i + 1 >= args.len) {
8191 fatal("expected parameter after --exclude", .{});
......@@ -86,7 +96,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
8696 } else if (mem.eql(u8, arg, "--zon")) {
8797 force_zon = true;
8898 } else {
89 fatal("unrecognized parameter: '{s}'", .{arg});
99 fatal("unrecognized parameter: {q}", .{arg});
90100 }
91101 } else {
92102 try input_files.append(arg);
......@@ -175,6 +185,9 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
175185 .color = color,
176186 .out_buffer = .init(gpa),
177187 .stdout_writer = &stdout_writer,
188 .complexity = complexity,
189 .total_tokens = 0,
190 .total_nodes = 0,
178191 };
179192 defer fmt.seen.deinit();
180193 defer fmt.out_buffer.deinit();
......@@ -198,7 +211,12 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
198211 for (input_files.items) |file_path| {
199212 try fmtPath(&fmt, file_path, check_flag, Io.Dir.cwd(), file_path);
200213 }
201 try fmt.stdout_writer.interface.flush();
214
215 if (complexity) {
216 std.log.info("total: tokens={d} nodes={d}", .{ fmt.total_tokens, fmt.total_nodes });
217 }
218
219 try fmt.stdout_writer.flush();
202220 if (fmt.any_error) {
203221 process.exit(1);
204222 }
......@@ -208,7 +226,7 @@ fn fmtPath(fmt: *Fmt, file_path: []const u8, check_mode: bool, dir: Io.Dir, sub_
208226 fmtPathFile(fmt, file_path, check_mode, dir, sub_path) catch |err| switch (err) {
209227 error.IsDir, error.AccessDenied => return fmtPathDir(fmt, file_path, check_mode, dir, sub_path),
210228 else => {
211 std.log.err("unable to format '{s}': {s}", .{ file_path, @errorName(err) });
229 std.log.err("formatting {q}: {t}", .{ file_path, err });
212230 fmt.any_error = true;
213231 return;
214232 },
......@@ -244,7 +262,7 @@ fn fmtPathDir(
244262 try fmtPathDir(fmt, full_path, check_mode, dir, entry.name);
245263 } else {
246264 fmtPathFile(fmt, full_path, check_mode, dir, entry.name) catch |err| {
247 std.log.err("unable to format '{s}': {t}", .{ full_path, err });
265 std.log.err("unable to format {q}: {t}", .{ full_path, err });
248266 fmt.any_error = true;
249267 return;
250268 };
......@@ -341,6 +359,12 @@ fn fmtPathFile(
341359 }
342360 }
343361
362 if (fmt.complexity) {
363 std.log.info("{s}: tokens={d} nodes={d}", .{ file_path, tree.tokens.len, tree.nodes.len });
364 fmt.total_tokens += tree.tokens.len;
365 fmt.total_nodes += tree.nodes.len;
366 }
367
344368 // As a heuristic, we make enough capacity for the same as the input source.
345369 fmt.out_buffer.clearRetainingCapacity();
346370 try fmt.out_buffer.ensureTotalCapacity(source_code.len);
......@@ -365,13 +389,7 @@ fn fmtPathFile(
365389}
366390
367391/// Provided for debugging/testing purposes; unused by the compiler.
368pub fn main() !void {
369 const gpa = std.heap.smp_allocator;
370 var arena_instance = std.heap.ArenaAllocator.init(gpa);
371 const arena = arena_instance.allocator();
372 const args = try process.argsAlloc(arena);
373 var threaded: std.Io.Threaded = .init(gpa, .{});
374 defer threaded.deinit();
375 const io = threaded.io();
376 return run(gpa, arena, io, args[1..]);
392pub fn main(init: process.Init) !void {
393 const args = try init.minimal.args.toSlice(init.arena.allocator());
394 return run(init.gpa, init.arena.allocator(), init.io, args[1..]);
377395}