authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-10-09 10:05:48+02:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2026-10-09 10:05:48+02:00
log18d9b2850ecba8fca14b11bd18111c17f0415aa9
treed191b5f0fccf877ba07d50dd061b807a86df5768
parent35accc06ed41d5c1d4f510f60058e47ce51ae033
parentcd00ebe2a7913927687336a80febaffa6beeb246

Merge pull request 'zig fmt: upgrade `--complexity` feature into `--stats` feature' (#37151) from zig-fmt-stats into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/37151

1 files changed, 170 insertions(+), 29 deletions(-)

src/fmt.zig+170-29
......@@ -6,6 +6,7 @@ const process = std.process;
66const Allocator = std.mem.Allocator;
77const Color = std.zig.Color;
88const fatal = std.process.fatal;
9const Serializer = std.zon.Serializer;
910
1011const usage_fmt =
1112 \\Usage: zig fmt [file]...
......@@ -23,7 +24,7 @@ const usage_fmt =
2324 \\ --ast-check Run zig ast-check on every file
2425 \\ --exclude [file] Exclude file or directory from formatting
2526 \\ --zon Treat all input files as ZON, regardless of file extension
26 \\ --complexity Print a complexity report for each file as well as total
27 \\ --stats Print aggregated statistics in ZON format to stdout
2728 \\
2829 \\
2930;
......@@ -39,21 +40,32 @@ const Fmt = struct {
3940 io: Io,
4041 out_buffer: std.Io.Writer.Allocating,
4142 stdout_writer: *Io.File.Writer,
42
43 complexity: bool,
44 total_tokens: u64,
45 total_nodes: u64,
43 stats: ?*Stats,
4644
4745 const SeenMap = std.AutoHashMap(Io.File.INode, void);
4846};
4947
48const Stats = struct {
49 serializer: Serializer,
50 root_struct: Serializer.Struct,
51
52 files: u32,
53 tokens: u64,
54 nodes: u64,
55 source_size: u64,
56 /// Count per token tag index
57 token_stats: []u64,
58 /// Count per node tag index
59 node_stats: []u64,
60};
61
5062pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !void {
5163 var color: Color = .auto;
5264 var stdin_flag = false;
5365 var check_flag = false;
5466 var check_ast_flag = false;
5567 var force_zon = false;
56 var complexity = false;
68 var enable_stats = false;
5769
5870 var input_files = std.array_list.Managed([]const u8).init(gpa);
5971 defer input_files.deinit();
......@@ -84,8 +96,8 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
8496 check_flag = true;
8597 } else if (mem.eql(u8, arg, "--ast-check")) {
8698 check_ast_flag = true;
87 } else if (mem.eql(u8, arg, "--complexity")) {
88 complexity = true;
99 } else if (mem.eql(u8, arg, "--stats")) {
100 enable_stats = true;
89101 } else if (mem.eql(u8, arg, "--exclude")) {
90102 if (i + 1 >= args.len) {
91103 fatal("expected parameter after --exclude", .{});
......@@ -185,13 +197,28 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
185197 .color = color,
186198 .out_buffer = .init(gpa),
187199 .stdout_writer = &stdout_writer,
188 .complexity = complexity,
189 .total_tokens = 0,
190 .total_nodes = 0,
200 .stats = null,
191201 };
192202 defer fmt.seen.deinit();
193203 defer fmt.out_buffer.deinit();
194204
205 if (enable_stats) {
206 const stats = try arena.create(Stats);
207 fmt.stats = stats;
208 stats.* = .{
209 .files = 0,
210 .tokens = 0,
211 .nodes = 0,
212 .source_size = 0,
213 .serializer = .{ .writer = &stdout_writer.interface },
214 .root_struct = try stats.serializer.beginStruct(.{}),
215 .token_stats = try arena.alloc(u64, @typeInfo(std.zig.Token.Tag).@"enum".field_names.len),
216 .node_stats = try arena.alloc(u64, @typeInfo(std.zig.Ast.Node.Tag).@"enum".field_names.len),
217 };
218 @memset(stats.token_stats, 0);
219 @memset(stats.node_stats, 0);
220 }
221
195222 // Mark any excluded files/directories as already seen,
196223 // so that they are skipped later during actual processing
197224 for (excluded_files.items) |file_path| {
......@@ -212,8 +239,50 @@ pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !
212239 try fmtPath(&fmt, file_path, check_flag, Io.Dir.cwd(), file_path);
213240 }
214241
215 if (complexity) {
216 std.log.info("total: tokens={d} nodes={d}", .{ fmt.total_tokens, fmt.total_nodes });
242 if (fmt.stats) |stats| {
243 if (stats.files > 1) {
244 var agg_struct = try stats.root_struct.beginStructField("aggregate", .{});
245 try agg_struct.field("file_count", stats.files, .{});
246 try agg_struct.field("token_count", stats.tokens, .{});
247 try agg_struct.field("node_count", stats.nodes, .{});
248 try agg_struct.field("source_size", stats.source_size, .{});
249
250 const token_names = @typeInfo(std.zig.Token.Tag).@"enum".field_names;
251 const node_names = @typeInfo(std.zig.Ast.Node.Tag).@"enum".field_names;
252
253 var token_order: [token_names.len]usize = undefined;
254 var node_order: [node_names.len]usize = undefined;
255 {
256 for (&token_order, 0..) |*elem, i| elem.* = i;
257 std.mem.sortContext(0, token_names.len, @as(SortContext, .{
258 .stats = stats.token_stats,
259 .order = &token_order,
260 }));
261 var tokens_struct = try agg_struct.beginStructField("tokens", .{});
262 for (&token_order) |i| {
263 const n = stats.token_stats[i];
264 if (n != 0) try tokens_struct.field(token_names[i], n, .{});
265 }
266 try tokens_struct.end();
267 }
268 {
269 for (&node_order, 0..) |*elem, i| elem.* = i;
270 std.mem.sortContext(0, node_names.len, @as(SortContext, .{
271 .stats = stats.node_stats,
272 .order = &node_order,
273 }));
274 var nodes_struct = try agg_struct.beginStructField("nodes", .{});
275 for (&node_order) |i| {
276 const n = stats.node_stats[i];
277 if (n != 0) try nodes_struct.field(node_names[i], n, .{});
278 }
279 try nodes_struct.end();
280 }
281
282 try agg_struct.end();
283 }
284 try stats.root_struct.end();
285 try fmt.stdout_writer.interface.writeByte('\n');
217286 }
218287
219288 try fmt.stdout_writer.flush();
......@@ -357,10 +426,65 @@ fn fmtPathFile(
357426 }
358427 }
359428
360 if (fmt.complexity) {
361 std.log.info("{s}: tokens={d} nodes={d}", .{ file_path, tree.tokens.len, tree.nodes.len });
362 fmt.total_tokens += tree.tokens.len;
363 fmt.total_nodes += tree.nodes.len;
429 var zon_struct: Serializer.Struct = undefined;
430
431 if (fmt.stats) |stats| {
432 zon_struct = try stats.root_struct.beginStructField(file_path, .{});
433
434 try zon_struct.field("token_count", tree.tokens.len, .{});
435 try zon_struct.field("node_count", tree.nodes.len, .{});
436 try zon_struct.field("source_size", stat.size, .{});
437
438 const token_names = @typeInfo(std.zig.Token.Tag).@"enum".field_names;
439 const node_names = @typeInfo(std.zig.Ast.Node.Tag).@"enum".field_names;
440
441 var token_stats: [token_names.len]u64 = @splat(0);
442 var token_order: [token_names.len]usize = undefined;
443 var node_stats: [node_names.len]u64 = @splat(0);
444 var node_order: [node_names.len]usize = undefined;
445
446 for (tree.tokens.items(.tag)) |tag| {
447 stats.token_stats[@backingInt(tag)] += 1;
448 token_stats[@backingInt(tag)] += 1;
449 }
450
451 for (tree.nodes.items(.tag)) |tag| {
452 stats.node_stats[@backingInt(tag)] += 1;
453 node_stats[@backingInt(tag)] += 1;
454 }
455
456 {
457 for (&token_order, 0..) |*elem, i| elem.* = i;
458 std.mem.sortContext(0, token_names.len, @as(SortContext, .{
459 .stats = &token_stats,
460 .order = &token_order,
461 }));
462 var s = try zon_struct.beginStructField("tokens", .{});
463 for (&token_order) |i| {
464 const n = token_stats[i];
465 if (n != 0) try s.field(token_names[i], n, .{});
466 }
467 try s.end();
468 }
469
470 {
471 for (&node_order, 0..) |*elem, i| elem.* = i;
472 std.mem.sortContext(0, node_names.len, @as(SortContext, .{
473 .stats = &node_stats,
474 .order = &node_order,
475 }));
476 var s = try zon_struct.beginStructField("nodes", .{});
477 for (&node_order) |i| {
478 const n = node_stats[i];
479 if (n != 0) try s.field(node_names[i], n, .{});
480 }
481 try s.end();
482 }
483
484 stats.tokens += tree.tokens.len;
485 stats.nodes += tree.nodes.len;
486 stats.files += 1;
487 stats.source_size += stat.size;
364488 }
365489
366490 // As a heuristic, we make enough capacity for the same as the input source.
......@@ -370,22 +494,39 @@ fn fmtPathFile(
370494 tree.render(gpa, &fmt.out_buffer.writer, .{}) catch |err| switch (err) {
371495 error.WriteFailed, error.OutOfMemory => return error.OutOfMemory,
372496 };
373 if (mem.eql(u8, fmt.out_buffer.written(), source_code))
374 return;
375
376 if (check_mode) {
377 try fmt.stdout_writer.interface.print("{s}\n", .{file_path});
378 fmt.any_error = true;
379 } else {
380 var af = try dir.createFileAtomic(io, sub_path, .{ .permissions = stat.permissions, .replace = true });
381 defer af.deinit(io);
497 if (!mem.eql(u8, fmt.out_buffer.written(), source_code)) {
498 if (check_mode) {
499 fmt.any_error = true;
500 } else {
501 var af = try dir.createFileAtomic(io, sub_path, .{ .permissions = stat.permissions, .replace = true });
502 defer af.deinit(io);
382503
383 try af.file.writeStreamingAll(io, fmt.out_buffer.written());
384 try af.replace(io);
385 try fmt.stdout_writer.interface.print("{s}\n", .{file_path});
504 try af.file.writeStreamingAll(io, fmt.out_buffer.written());
505 try af.replace(io);
506 }
507 if (fmt.stats != null) {
508 try zon_struct.field("dirty", true, .{});
509 } else {
510 try fmt.stdout_writer.interface.print("{s}\n", .{file_path});
511 }
386512 }
513
514 if (fmt.stats != null) try zon_struct.end();
387515}
388516
517const SortContext = struct {
518 stats: []const u64,
519 order: []usize,
520
521 pub fn lessThan(this: @This(), a_index: usize, b_index: usize) bool {
522 return this.stats[this.order[b_index]] < this.stats[this.order[a_index]];
523 }
524
525 pub fn swap(this: @This(), a_index: usize, b_index: usize) void {
526 std.mem.swap(usize, &this.order[a_index], &this.order[b_index]);
527 }
528};
529
389530/// Provided for debugging/testing purposes; unused by the compiler.
390531pub fn main(init: process.Init) !void {
391532 const args = try init.minimal.args.toSlice(init.arena.allocator());