commit 7db2ef6104921c42ce42e8edda848160eda8973d (tree)
parent 56e313b288928e998e186286a2ddc3211c20aa71
Author: Andrew Kelley <andrew@ziglang.org>
Date: Thu, 25 Jun 2026 13:10:59 -0700
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
Diffstat:
| M | src/fmt.zig | | | 46 | ++++++++++++++++++++++++++++++++-------------- |
1 file changed, 32 insertions(+), 14 deletions(-)
diff --git a/src/fmt.zig b/src/fmt.zig
@@ -23,6 +23,7 @@ const usage_fmt =
\\ --ast-check Run zig ast-check on every file
\\ --exclude [file] Exclude file or directory from formatting
\\ --zon Treat all input files as ZON, regardless of file extension
+ \\ --complexity Print a complexity report for each file as well as total
\\
\\
;
@@ -39,6 +40,10 @@ const Fmt = struct {
out_buffer: std.Io.Writer.Allocating,
stdout_writer: *Io.File.Writer,
+ complexity: bool,
+ total_tokens: u64,
+ total_nodes: u64,
+
const SeenMap = std.AutoHashMap(Io.File.INode, void);
};
@@ -48,8 +53,11 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
var check_flag = false;
var check_ast_flag = false;
var force_zon = false;
+ var complexity = false;
+
var input_files = std.array_list.Managed([]const u8).init(gpa);
defer input_files.deinit();
+
var excluded_files = std.array_list.Managed([]const u8).init(gpa);
defer excluded_files.deinit();
@@ -68,7 +76,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
i += 1;
const next_arg = args[i];
color = std.meta.stringToEnum(Color, next_arg) orelse {
- fatal("expected [auto|on|off] after --color, found '{s}'", .{next_arg});
+ fatal("expected [auto|on|off] after --color, found {q}", .{next_arg});
};
} else if (mem.eql(u8, arg, "--stdin")) {
stdin_flag = true;
@@ -76,6 +84,8 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
check_flag = true;
} else if (mem.eql(u8, arg, "--ast-check")) {
check_ast_flag = true;
+ } else if (mem.eql(u8, arg, "--complexity")) {
+ complexity = true;
} else if (mem.eql(u8, arg, "--exclude")) {
if (i + 1 >= args.len) {
fatal("expected parameter after --exclude", .{});
@@ -86,7 +96,7 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
} else if (mem.eql(u8, arg, "--zon")) {
force_zon = true;
} else {
- fatal("unrecognized parameter: '{s}'", .{arg});
+ fatal("unrecognized parameter: {q}", .{arg});
}
} else {
try input_files.append(arg);
@@ -175,6 +185,9 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
.color = color,
.out_buffer = .init(gpa),
.stdout_writer = &stdout_writer,
+ .complexity = complexity,
+ .total_tokens = 0,
+ .total_nodes = 0,
};
defer fmt.seen.deinit();
defer fmt.out_buffer.deinit();
@@ -198,7 +211,12 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
for (input_files.items) |file_path| {
try fmtPath(&fmt, file_path, check_flag, Io.Dir.cwd(), file_path);
}
- try fmt.stdout_writer.interface.flush();
+
+ if (complexity) {
+ std.log.info("total: tokens={d} nodes={d}", .{ fmt.total_tokens, fmt.total_nodes });
+ }
+
+ try fmt.stdout_writer.flush();
if (fmt.any_error) {
process.exit(1);
}
@@ -208,7 +226,7 @@ fn fmtPath(fmt: *Fmt, file_path: []const u8, check_mode: bool, dir: Io.Dir, sub_
fmtPathFile(fmt, file_path, check_mode, dir, sub_path) catch |err| switch (err) {
error.IsDir, error.AccessDenied => return fmtPathDir(fmt, file_path, check_mode, dir, sub_path),
else => {
- std.log.err("unable to format '{s}': {s}", .{ file_path, @errorName(err) });
+ std.log.err("formatting {q}: {t}", .{ file_path, err });
fmt.any_error = true;
return;
},
@@ -244,7 +262,7 @@ fn fmtPathDir(
try fmtPathDir(fmt, full_path, check_mode, dir, entry.name);
} else {
fmtPathFile(fmt, full_path, check_mode, dir, entry.name) catch |err| {
- std.log.err("unable to format '{s}': {t}", .{ full_path, err });
+ std.log.err("unable to format {q}: {t}", .{ full_path, err });
fmt.any_error = true;
return;
};
@@ -341,6 +359,12 @@ fn fmtPathFile(
}
}
+ if (fmt.complexity) {
+ std.log.info("{s}: tokens={d} nodes={d}", .{ file_path, tree.tokens.len, tree.nodes.len });
+ fmt.total_tokens += tree.tokens.len;
+ fmt.total_nodes += tree.nodes.len;
+ }
+
// As a heuristic, we make enough capacity for the same as the input source.
fmt.out_buffer.clearRetainingCapacity();
try fmt.out_buffer.ensureTotalCapacity(source_code.len);
@@ -365,13 +389,7 @@ fn fmtPathFile(
}
/// Provided for debugging/testing purposes; unused by the compiler.
-pub fn main() !void {
- const gpa = std.heap.smp_allocator;
- var arena_instance = std.heap.ArenaAllocator.init(gpa);
- const arena = arena_instance.allocator();
- const args = try process.argsAlloc(arena);
- var threaded: std.Io.Threaded = .init(gpa, .{});
- defer threaded.deinit();
- const io = threaded.io();
- return run(gpa, arena, io, args[1..]);
+pub fn main(init: process.Init) !void {
+ const args = try init.minimal.args.toSlice(init.arena.allocator());
+ return run(init.gpa, init.arena.allocator(), init.io, args[1..]);
}