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