1const std = @import("std");
2const Io = std.Io;
3const mem = std.mem;
4const fs = std.fs;
5const process = std.process;
6const Allocator = std.mem.Allocator;
7const Color = std.zig.Color;
8const fatal = std.process.fatal;
9
10const usage_fmt =
11 \\Usage: zig fmt [file]...
12 \\
13 \\ Formats the input files and modifies them in-place.
14 \\ Arguments can be files or directories, which are searched
15 \\ recursively.
16 \\
17 \\Options:
18 \\ -h, --help Print this help and exit
19 \\ --color [auto|off|on] Enable or disable colored error messages
20 \\ --stdin Format code from stdin; output to stdout
21 \\ --check List non-conforming files and exit with an error
22 \\ if the list is non-empty
23 \\ --ast-check Run zig ast-check on every file
24 \\ --exclude [file] Exclude file or directory from formatting
25 \\ --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 \\
28 \\
29;
30
31const Fmt = struct {
32 seen: SeenMap,
33 any_error: bool,
34 check_ast: bool,
35 force_zon: bool,
36 color: Color,
37 gpa: Allocator,
38 arena: Allocator,
39 io: Io,
40 out_buffer: std.Io.Writer.Allocating,
41 stdout_writer: *Io.File.Writer,
42
43 complexity: bool,
44 total_tokens: u64,
45 total_nodes: u64,
46
47 const SeenMap = std.AutoHashMap(Io.File.INode, void);
48};
49
50pub fn run(gpa: Allocator, arena: Allocator, io: Io, args: []const []const u8) !void {
51 var color: Color = .auto;
52 var stdin_flag = false;
53 var check_flag = false;
54 var check_ast_flag = false;
55 var force_zon = false;
56 var complexity = false;
57
58 var input_files = std.array_list.Managed([]const u8).init(gpa);
59 defer input_files.deinit();
60
61 var excluded_files = std.array_list.Managed([]const u8).init(gpa);
62 defer excluded_files.deinit();
63
64 {
65 var i: usize = 0;
66 while (i < args.len) : (i += 1) {
67 const arg = args[i];
68 if (mem.startsWith(u8, arg, "-")) {
69 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
70 try Io.File.stdout().writeStreamingAll(io, usage_fmt);
71 return process.cleanExit(io);
72 } else if (mem.eql(u8, arg, "--color")) {
73 if (i + 1 >= args.len) {
74 fatal("expected [auto|on|off] after --color", .{});
75 }
76 i += 1;
77 const next_arg = args[i];
78 color = std.meta.stringToEnum(Color, next_arg) orelse {
79 fatal("expected [auto|on|off] after --color, found {q}", .{next_arg});
80 };
81 } else if (mem.eql(u8, arg, "--stdin")) {
82 stdin_flag = true;
83 } else if (mem.eql(u8, arg, "--check")) {
84 check_flag = true;
85 } else if (mem.eql(u8, arg, "--ast-check")) {
86 check_ast_flag = true;
87 } else if (mem.eql(u8, arg, "--complexity")) {
88 complexity = true;
89 } else if (mem.eql(u8, arg, "--exclude")) {
90 if (i + 1 >= args.len) {
91 fatal("expected parameter after --exclude", .{});
92 }
93 i += 1;
94 const next_arg = args[i];
95 try excluded_files.append(next_arg);
96 } else if (mem.eql(u8, arg, "--zon")) {
97 force_zon = true;
98 } else {
99 fatal("unrecognized parameter: {q}", .{arg});
100 }
101 } else {
102 try input_files.append(arg);
103 }
104 }
105 }
106
107 if (stdin_flag) {
108 if (input_files.items.len != 0) {
109 fatal("cannot use --stdin with positional arguments", .{});
110 }
111
112 const stdin: Io.File = .stdin();
113 var stdio_buffer: [1024]u8 = undefined;
114 var file_reader: Io.File.Reader = stdin.reader(io, &stdio_buffer);
115 const source_code = std.zig.readSourceFileToEndAlloc(gpa, &file_reader) catch |err| {
116 fatal("unable to read stdin: {}", .{err});
117 };
118 defer gpa.free(source_code);
119
120 var tree = std.zig.Ast.parse(gpa, source_code, .{
121 .mode = if (force_zon) .zon else .zig,
122 }) catch |err| {
123 fatal("error parsing stdin: {}", .{err});
124 };
125 defer tree.deinit(gpa);
126
127 if (check_ast_flag) {
128 if (!force_zon) {
129 var zir = try std.zig.AstGen.generate(gpa, tree);
130 defer zir.deinit(gpa);
131
132 if (zir.hasCompileErrors()) {
133 var wip_errors: std.zig.ErrorBundle.Wip = undefined;
134 try wip_errors.init(gpa);
135 defer wip_errors.deinit();
136 try wip_errors.addZirErrorMessages(zir, tree, source_code, "<stdin>");
137 var error_bundle = try wip_errors.toOwnedBundle("");
138 defer error_bundle.deinit(gpa);
139 error_bundle.renderToStderr(io, .{}, color) catch {};
140 process.exit(2);
141 }
142 } else {
143 const zoir = try std.zig.ZonGen.generate(gpa, tree, .{});
144 defer zoir.deinit(gpa);
145
146 if (zoir.hasCompileErrors()) {
147 var wip_errors: std.zig.ErrorBundle.Wip = undefined;
148 try wip_errors.init(gpa);
149 defer wip_errors.deinit();
150 try wip_errors.addZoirErrorMessages(zoir, tree, source_code, "<stdin>");
151 var error_bundle = try wip_errors.toOwnedBundle("");
152 defer error_bundle.deinit(gpa);
153 error_bundle.renderToStderr(io, .{}, color) catch {};
154 process.exit(2);
155 }
156 }
157 } else if (tree.errors.len != 0) {
158 std.zig.printAstErrorsToStderr(gpa, io, tree, "<stdin>", color) catch {};
159 process.exit(2);
160 }
161 const formatted = try tree.renderAlloc(gpa);
162 defer gpa.free(formatted);
163
164 if (check_flag) {
165 const code: u8 = @intFromBool(!mem.eql(u8, formatted, source_code));
166 process.exit(code);
167 }
168
169 return Io.File.stdout().writeStreamingAll(io, formatted);
170 }
171
172 if (input_files.items.len == 0) {
173 fatal("expected at least one file or directory argument", .{});
174 }
175
176 var stdout_buffer: [4096]u8 = undefined;
177 var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer);
178
179 var fmt: Fmt = .{
180 .gpa = gpa,
181 .arena = arena,
182 .io = io,
183 .seen = .init(gpa),
184 .any_error = false,
185 .check_ast = check_ast_flag,
186 .force_zon = force_zon,
187 .color = color,
188 .out_buffer = .init(gpa),
189 .stdout_writer = &stdout_writer,
190 .complexity = complexity,
191 .total_tokens = 0,
192 .total_nodes = 0,
193 };
194 defer fmt.seen.deinit();
195 defer fmt.out_buffer.deinit();
196
197 // Mark any excluded files/directories as already seen,
198 // so that they are skipped later during actual processing
199 for (excluded_files.items) |file_path| {
200 const stat = Io.Dir.cwd().statFile(io, file_path, .{}) catch |err| switch (err) {
201 error.FileNotFound => continue,
202 // On Windows, statFile does not work for directories
203 error.IsDir => dir: {
204 var dir = try Io.Dir.cwd().openDir(io, file_path, .{});
205 defer dir.close(io);
206 break :dir try dir.stat(io);
207 },
208 else => |e| return e,
209 };
210 try fmt.seen.put(stat.inode, {});
211 }
212
213 for (input_files.items) |file_path| {
214 try fmtPath(&fmt, file_path, check_flag, Io.Dir.cwd(), file_path);
215 }
216
217 if (complexity) {
218 std.log.info("total: tokens={d} nodes={d}", .{ fmt.total_tokens, fmt.total_nodes });
219 }
220
221 try fmt.stdout_writer.flush();
222 if (fmt.any_error) {
223 process.exit(1);
224 }
225}
226
227fn fmtPath(fmt: *Fmt, file_path: []const u8, check_mode: bool, dir: Io.Dir, sub_path: []const u8) !void {
228 fmtPathFile(fmt, file_path, check_mode, dir, sub_path) catch |err| switch (err) {
229 error.IsDir, error.AccessDenied => return fmtPathDir(fmt, file_path, check_mode, dir, sub_path),
230 else => {
231 std.log.err("formatting {q}: {t}", .{ file_path, err });
232 fmt.any_error = true;
233 return;
234 },
235 };
236}
237
238fn fmtPathDir(
239 fmt: *Fmt,
240 file_path: []const u8,
241 check_mode: bool,
242 parent_dir: Io.Dir,
243 parent_sub_path: []const u8,
244) !void {
245 const io = fmt.io;
246
247 var dir = try parent_dir.openDir(io, parent_sub_path, .{ .iterate = true });
248 defer dir.close(io);
249
250 const stat = try dir.stat(io);
251 if (try fmt.seen.fetchPut(stat.inode, {})) |_| return;
252
253 var dir_it = dir.iterate();
254 while (try dir_it.next(io)) |entry| {
255 const is_dir = entry.kind == .directory;
256
257 if (mem.startsWith(u8, entry.name, ".")) continue;
258
259 if (is_dir or entry.kind == .file and (mem.endsWith(u8, entry.name, ".zig") or mem.endsWith(u8, entry.name, ".zon"))) {
260 const full_path = try fs.path.join(fmt.gpa, &[_][]const u8{ file_path, entry.name });
261 defer fmt.gpa.free(full_path);
262
263 if (is_dir) {
264 try fmtPathDir(fmt, full_path, check_mode, dir, entry.name);
265 } else {
266 fmtPathFile(fmt, full_path, check_mode, dir, entry.name) catch |err| {
267 std.log.err("unable to format {q}: {t}", .{ full_path, err });
268 fmt.any_error = true;
269 return;
270 };
271 }
272 }
273 }
274}
275
276fn fmtPathFile(
277 fmt: *Fmt,
278 file_path: []const u8,
279 check_mode: bool,
280 dir: Io.Dir,
281 sub_path: []const u8,
282) !void {
283 const io = fmt.io;
284
285 const source_file = try dir.openFile(io, sub_path, .{});
286 var file_closed = false;
287 errdefer if (!file_closed) source_file.close(io);
288
289 const stat = try source_file.stat(io);
290
291 if (stat.kind == .directory)
292 return error.IsDir;
293
294 var read_buffer: [1024]u8 = undefined;
295 var file_reader: Io.File.Reader = source_file.reader(io, &read_buffer);
296 file_reader.size = stat.size;
297
298 const gpa = fmt.gpa;
299 const source_code = std.zig.readSourceFileToEndAlloc(gpa, &file_reader) catch |err| switch (err) {
300 error.ReadFailed => return file_reader.err.?,
301 else => |e| return e,
302 };
303 defer gpa.free(source_code);
304
305 source_file.close(io);
306 file_closed = true;
307
308 // Add to set after no longer possible to get error.IsDir.
309 if (try fmt.seen.fetchPut(stat.inode, {})) |_| return;
310
311 const mode: std.zig.Ast.Mode = mode: {
312 if (fmt.force_zon) break :mode .zon;
313 if (mem.endsWith(u8, sub_path, ".zon")) break :mode .zon;
314 break :mode .zig;
315 };
316
317 var tree = try std.zig.Ast.parse(gpa, source_code, .{ .mode = mode });
318 defer tree.deinit(gpa);
319
320 if (tree.errors.len != 0) {
321 try std.zig.printAstErrorsToStderr(gpa, io, tree, file_path, fmt.color);
322 fmt.any_error = true;
323 return;
324 }
325
326 if (fmt.check_ast) {
327 if (stat.size > std.zig.max_src_size)
328 return error.FileTooBig;
329
330 switch (mode) {
331 .zig => {
332 var zir = try std.zig.AstGen.generate(gpa, tree);
333 defer zir.deinit(gpa);
334
335 if (zir.hasCompileErrors()) {
336 var wip_errors: std.zig.ErrorBundle.Wip = undefined;
337 try wip_errors.init(gpa);
338 defer wip_errors.deinit();
339 try wip_errors.addZirErrorMessages(zir, tree, source_code, file_path);
340 var error_bundle = try wip_errors.toOwnedBundle("");
341 defer error_bundle.deinit(gpa);
342 try error_bundle.renderToStderr(io, .{}, fmt.color);
343 fmt.any_error = true;
344 }
345 },
346 .zon => {
347 var zoir = try std.zig.ZonGen.generate(gpa, tree, .{});
348 defer zoir.deinit(gpa);
349
350 if (zoir.hasCompileErrors()) {
351 var wip_errors: std.zig.ErrorBundle.Wip = undefined;
352 try wip_errors.init(gpa);
353 defer wip_errors.deinit();
354 try wip_errors.addZoirErrorMessages(zoir, tree, source_code, file_path);
355 var error_bundle = try wip_errors.toOwnedBundle("");
356 defer error_bundle.deinit(gpa);
357 try error_bundle.renderToStderr(io, .{}, fmt.color);
358 fmt.any_error = true;
359 }
360 },
361 }
362 }
363
364 if (fmt.complexity) {
365 std.log.info("{s}: tokens={d} nodes={d}", .{ file_path, tree.tokens.len, tree.nodes.len });
366 fmt.total_tokens += tree.tokens.len;
367 fmt.total_nodes += tree.nodes.len;
368 }
369
370 // As a heuristic, we make enough capacity for the same as the input source.
371 fmt.out_buffer.clearRetainingCapacity();
372 try fmt.out_buffer.ensureTotalCapacity(source_code.len);
373
374 tree.render(gpa, &fmt.out_buffer.writer, .{}) catch |err| switch (err) {
375 error.WriteFailed, error.OutOfMemory => return error.OutOfMemory,
376 };
377 if (mem.eql(u8, fmt.out_buffer.written(), source_code))
378 return;
379
380 if (check_mode) {
381 try fmt.stdout_writer.interface.print("{s}\n", .{file_path});
382 fmt.any_error = true;
383 } else {
384 var af = try dir.createFileAtomic(io, sub_path, .{ .permissions = stat.permissions, .replace = true });
385 defer af.deinit(io);
386
387 try af.file.writeStreamingAll(io, fmt.out_buffer.written());
388 try af.replace(io);
389 try fmt.stdout_writer.interface.print("{s}\n", .{file_path});
390 }
391}
392
393/// Provided for debugging/testing purposes; unused by the compiler.
394pub fn main(init: process.Init) !void {
395 const args = try init.minimal.args.toSlice(init.arena.allocator());
396 return run(init.gpa, init.arena.allocator(), init.io, args[1..]);
397}