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 =...@@ -23,6 +23,7 @@ const usage_fmt =
23 \\ --ast-check Run zig ast-check on every file23 \\ --ast-check Run zig ast-check on every file
24 \\ --exclude [file] Exclude file or directory from formatting24 \\ --exclude [file] Exclude file or directory from formatting
25 \\ --zon Treat all input files as ZON, regardless of file extension25 \\ --zon Treat all input files as ZON, regardless of file extension
26 \\ --complexity Print a complexity report for each file as well as total
26 \\27 \\
27 \\28 \\
28;29;
...@@ -39,6 +40,10 @@ const Fmt = struct {...@@ -39,6 +40,10 @@ const Fmt = struct {
39 out_buffer: std.Io.Writer.Allocating,40 out_buffer: std.Io.Writer.Allocating,
40 stdout_writer: *Io.File.Writer,41 stdout_writer: *Io.File.Writer,
4142
43 complexity: bool,
44 total_tokens: u64,
45 total_nodes: u64,
46
42 const SeenMap = std.AutoHashMap(Io.File.INode, void);47 const SeenMap = std.AutoHashMap(Io.File.INode, void);
43};48};
4449
...@@ -48,8 +53,11 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !...@@ -48,8 +53,11 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
48 var check_flag = false;53 var check_flag = false;
49 var check_ast_flag = false;54 var check_ast_flag = false;
50 var force_zon = false;55 var force_zon = false;
56 var complexity = false;
57
51 var input_files = std.array_list.Managed([]const u8).init(gpa);58 var input_files = std.array_list.Managed([]const u8).init(gpa);
52 defer input_files.deinit();59 defer input_files.deinit();
60
53 var excluded_files = std.array_list.Managed([]const u8).init(gpa);61 var excluded_files = std.array_list.Managed([]const u8).init(gpa);
54 defer excluded_files.deinit();62 defer excluded_files.deinit();
5563
...@@ -68,7 +76,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !...@@ -68,7 +76,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
68 i += 1;76 i += 1;
69 const next_arg = args[i];77 const next_arg = args[i];
70 color = std.meta.stringToEnum(Color, next_arg) orelse {78 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});
72 };80 };
73 } else if (mem.eql(u8, arg, "--stdin")) {81 } else if (mem.eql(u8, arg, "--stdin")) {
74 stdin_flag = true;82 stdin_flag = true;
...@@ -76,6 +84,8 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !...@@ -76,6 +84,8 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
76 check_flag = true;84 check_flag = true;
77 } else if (mem.eql(u8, arg, "--ast-check")) {85 } else if (mem.eql(u8, arg, "--ast-check")) {
78 check_ast_flag = true;86 check_ast_flag = true;
87 } else if (mem.eql(u8, arg, "--complexity")) {
88 complexity = true;
79 } else if (mem.eql(u8, arg, "--exclude")) {89 } else if (mem.eql(u8, arg, "--exclude")) {
80 if (i + 1 >= args.len) {90 if (i + 1 >= args.len) {
81 fatal("expected parameter after --exclude", .{});91 fatal("expected parameter after --exclude", .{});
...@@ -86,7 +96,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !...@@ -86,7 +96,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
86 } else if (mem.eql(u8, arg, "--zon")) {96 } else if (mem.eql(u8, arg, "--zon")) {
87 force_zon = true;97 force_zon = true;
88 } else {98 } else {
89 fatal("unrecognized parameter: '{s}'", .{arg});99 fatal("unrecognized parameter: {q}", .{arg});
90 }100 }
91 } else {101 } else {
92 try input_files.append(arg);102 try input_files.append(arg);
...@@ -175,6 +185,9 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !...@@ -175,6 +185,9 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
175 .color = color,185 .color = color,
176 .out_buffer = .init(gpa),186 .out_buffer = .init(gpa),
177 .stdout_writer = &stdout_writer,187 .stdout_writer = &stdout_writer,
188 .complexity = complexity,
189 .total_tokens = 0,
190 .total_nodes = 0,
178 };191 };
179 defer fmt.seen.deinit();192 defer fmt.seen.deinit();
180 defer fmt.out_buffer.deinit();193 defer fmt.out_buffer.deinit();
...@@ -198,7 +211,12 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !...@@ -198,7 +211,12 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
198 for (input_files.items) |file_path| {211 for (input_files.items) |file_path| {
199 try fmtPath(&fmt, file_path, check_flag, Io.Dir.cwd(), file_path);212 try fmtPath(&fmt, file_path, check_flag, Io.Dir.cwd(), file_path);
200 }213 }
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();
202 if (fmt.any_error) {220 if (fmt.any_error) {
203 process.exit(1);221 process.exit(1);
204 }222 }
...@@ -208,7 +226,7 @@ fn fmtPath(fmt: *Fmt, file_path: []const u8, check_mode: bool, dir: Io.Dir, sub_...@@ -208,7 +226,7 @@ fn fmtPath(fmt: *Fmt, file_path: []const u8, check_mode: bool, dir: Io.Dir, sub_
208 fmtPathFile(fmt, file_path, check_mode, dir, sub_path) catch |err| switch (err) {226 fmtPathFile(fmt, file_path, check_mode, dir, sub_path) catch |err| switch (err) {
209 error.IsDir, error.AccessDenied => return fmtPathDir(fmt, file_path, check_mode, dir, sub_path),227 error.IsDir, error.AccessDenied => return fmtPathDir(fmt, file_path, check_mode, dir, sub_path),
210 else => {228 else => {
211 std.log.err("unable to format '{s}': {s}", .{ file_path, @errorName(err) });229 std.log.err("formatting {q}: {t}", .{ file_path, err });
212 fmt.any_error = true;230 fmt.any_error = true;
213 return;231 return;
214 },232 },
...@@ -244,7 +262,7 @@ fn fmtPathDir(...@@ -244,7 +262,7 @@ fn fmtPathDir(
244 try fmtPathDir(fmt, full_path, check_mode, dir, entry.name);262 try fmtPathDir(fmt, full_path, check_mode, dir, entry.name);
245 } else {263 } else {
246 fmtPathFile(fmt, full_path, check_mode, dir, entry.name) catch |err| {264 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 });
248 fmt.any_error = true;266 fmt.any_error = true;
249 return;267 return;
250 };268 };
...@@ -341,6 +359,12 @@ fn fmtPathFile(...@@ -341,6 +359,12 @@ fn fmtPathFile(
341 }359 }
342 }360 }
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
344 // As a heuristic, we make enough capacity for the same as the input source.368 // As a heuristic, we make enough capacity for the same as the input source.
345 fmt.out_buffer.clearRetainingCapacity();369 fmt.out_buffer.clearRetainingCapacity();
346 try fmt.out_buffer.ensureTotalCapacity(source_code.len);370 try fmt.out_buffer.ensureTotalCapacity(source_code.len);
...@@ -365,13 +389,7 @@ fn fmtPathFile(...@@ -365,13 +389,7 @@ fn fmtPathFile(
365}389}
366390
367/// Provided for debugging/testing purposes; unused by the compiler.391/// Provided for debugging/testing purposes; unused by the compiler.
368pub fn main() !void {392pub fn main(init: process.Init) !void {
369 const gpa = std.heap.smp_allocator;393 const args = try init.minimal.args.toSlice(init.arena.allocator());
370 var arena_instance = std.heap.ArenaAllocator.init(gpa);394 return run(init.gpa, init.arena.allocator(), init.io, args[1..]);
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..]);
377}395}