Merge pull request #22808 from ziglang/fast-gpa
[zig.git] / src / fmt.zig
blobc86fb8533205c147158bd5d472d778ef6b6137e7
1 const usage_fmt =
2     \\Usage: zig fmt [file]...
3     \\
4     \\   Formats the input files and modifies them in-place.
5     \\   Arguments can be files or directories, which are searched
6     \\   recursively.
7     \\
8     \\Options:
9     \\  -h, --help             Print this help and exit
10     \\  --color [auto|off|on]  Enable or disable colored error messages
11     \\  --stdin                Format code from stdin; output to stdout
12     \\  --check                List non-conforming files and exit with an error
13     \\                         if the list is non-empty
14     \\  --ast-check            Run zig ast-check on every file
15     \\  --exclude [file]       Exclude file or directory from formatting
16     \\  --zon                  Treat all input files as ZON, regardless of file extension
17     \\
18     \\
21 const Fmt = struct {
22     seen: SeenMap,
23     any_error: bool,
24     check_ast: bool,
25     force_zon: bool,
26     color: Color,
27     gpa: Allocator,
28     arena: Allocator,
29     out_buffer: std.ArrayList(u8),
31     const SeenMap = std.AutoHashMap(fs.File.INode, void);
34 pub fn run(
35     gpa: Allocator,
36     arena: Allocator,
37     args: []const []const u8,
38 ) !void {
39     var color: Color = .auto;
40     var stdin_flag = false;
41     var check_flag = false;
42     var check_ast_flag = false;
43     var force_zon = false;
44     var input_files = std.ArrayList([]const u8).init(gpa);
45     defer input_files.deinit();
46     var excluded_files = std.ArrayList([]const u8).init(gpa);
47     defer excluded_files.deinit();
49     {
50         var i: usize = 0;
51         while (i < args.len) : (i += 1) {
52             const arg = args[i];
53             if (mem.startsWith(u8, arg, "-")) {
54                 if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
55                     const stdout = std.io.getStdOut().writer();
56                     try stdout.writeAll(usage_fmt);
57                     return process.cleanExit();
58                 } else if (mem.eql(u8, arg, "--color")) {
59                     if (i + 1 >= args.len) {
60                         fatal("expected [auto|on|off] after --color", .{});
61                     }
62                     i += 1;
63                     const next_arg = args[i];
64                     color = std.meta.stringToEnum(Color, next_arg) orelse {
65                         fatal("expected [auto|on|off] after --color, found '{s}'", .{next_arg});
66                     };
67                 } else if (mem.eql(u8, arg, "--stdin")) {
68                     stdin_flag = true;
69                 } else if (mem.eql(u8, arg, "--check")) {
70                     check_flag = true;
71                 } else if (mem.eql(u8, arg, "--ast-check")) {
72                     check_ast_flag = true;
73                 } else if (mem.eql(u8, arg, "--exclude")) {
74                     if (i + 1 >= args.len) {
75                         fatal("expected parameter after --exclude", .{});
76                     }
77                     i += 1;
78                     const next_arg = args[i];
79                     try excluded_files.append(next_arg);
80                 } else if (mem.eql(u8, arg, "--zon")) {
81                     force_zon = true;
82                 } else {
83                     fatal("unrecognized parameter: '{s}'", .{arg});
84                 }
85             } else {
86                 try input_files.append(arg);
87             }
88         }
89     }
91     if (stdin_flag) {
92         if (input_files.items.len != 0) {
93             fatal("cannot use --stdin with positional arguments", .{});
94         }
96         const stdin = std.io.getStdIn();
97         const source_code = std.zig.readSourceFileToEndAlloc(gpa, stdin, null) catch |err| {
98             fatal("unable to read stdin: {}", .{err});
99         };
100         defer gpa.free(source_code);
102         var tree = std.zig.Ast.parse(gpa, source_code, if (force_zon) .zon else .zig) catch |err| {
103             fatal("error parsing stdin: {}", .{err});
104         };
105         defer tree.deinit(gpa);
107         if (check_ast_flag) {
108             if (!force_zon) {
109                 var zir = try std.zig.AstGen.generate(gpa, tree);
110                 defer zir.deinit(gpa);
112                 if (zir.hasCompileErrors()) {
113                     var wip_errors: std.zig.ErrorBundle.Wip = undefined;
114                     try wip_errors.init(gpa);
115                     defer wip_errors.deinit();
116                     try wip_errors.addZirErrorMessages(zir, tree, source_code, "<stdin>");
117                     var error_bundle = try wip_errors.toOwnedBundle("");
118                     defer error_bundle.deinit(gpa);
119                     error_bundle.renderToStdErr(color.renderOptions());
120                     process.exit(2);
121                 }
122             } else {
123                 const zoir = try std.zig.ZonGen.generate(gpa, tree, .{});
124                 defer zoir.deinit(gpa);
126                 if (zoir.hasCompileErrors()) {
127                     var wip_errors: std.zig.ErrorBundle.Wip = undefined;
128                     try wip_errors.init(gpa);
129                     defer wip_errors.deinit();
130                     try wip_errors.addZoirErrorMessages(zoir, tree, source_code, "<stdin>");
131                     var error_bundle = try wip_errors.toOwnedBundle("");
132                     defer error_bundle.deinit(gpa);
133                     error_bundle.renderToStdErr(color.renderOptions());
134                     process.exit(2);
135                 }
136             }
137         } else if (tree.errors.len != 0) {
138             try std.zig.printAstErrorsToStderr(gpa, tree, "<stdin>", color);
139             process.exit(2);
140         }
141         const formatted = try tree.render(gpa);
142         defer gpa.free(formatted);
144         if (check_flag) {
145             const code: u8 = @intFromBool(mem.eql(u8, formatted, source_code));
146             process.exit(code);
147         }
149         return std.io.getStdOut().writeAll(formatted);
150     }
152     if (input_files.items.len == 0) {
153         fatal("expected at least one source file argument", .{});
154     }
156     var fmt: Fmt = .{
157         .gpa = gpa,
158         .arena = arena,
159         .seen = .init(gpa),
160         .any_error = false,
161         .check_ast = check_ast_flag,
162         .force_zon = force_zon,
163         .color = color,
164         .out_buffer = std.ArrayList(u8).init(gpa),
165     };
166     defer fmt.seen.deinit();
167     defer fmt.out_buffer.deinit();
169     // Mark any excluded files/directories as already seen,
170     // so that they are skipped later during actual processing
171     for (excluded_files.items) |file_path| {
172         const stat = fs.cwd().statFile(file_path) catch |err| switch (err) {
173             error.FileNotFound => continue,
174             // On Windows, statFile does not work for directories
175             error.IsDir => dir: {
176                 var dir = try fs.cwd().openDir(file_path, .{});
177                 defer dir.close();
178                 break :dir try dir.stat();
179             },
180             else => |e| return e,
181         };
182         try fmt.seen.put(stat.inode, {});
183     }
185     for (input_files.items) |file_path| {
186         try fmtPath(&fmt, file_path, check_flag, fs.cwd(), file_path);
187     }
188     if (fmt.any_error) {
189         process.exit(1);
190     }
193 const FmtError = error{
194     SystemResources,
195     OperationAborted,
196     IoPending,
197     BrokenPipe,
198     Unexpected,
199     WouldBlock,
200     Canceled,
201     FileClosed,
202     DestinationAddressRequired,
203     DiskQuota,
204     FileTooBig,
205     InputOutput,
206     NoSpaceLeft,
207     AccessDenied,
208     OutOfMemory,
209     RenameAcrossMountPoints,
210     ReadOnlyFileSystem,
211     LinkQuotaExceeded,
212     FileBusy,
213     EndOfStream,
214     Unseekable,
215     NotOpenForWriting,
216     UnsupportedEncoding,
217     ConnectionResetByPeer,
218     SocketNotConnected,
219     LockViolation,
220     NetNameDeleted,
221     InvalidArgument,
222     ProcessNotFound,
223 } || fs.File.OpenError;
225 fn fmtPath(fmt: *Fmt, file_path: []const u8, check_mode: bool, dir: fs.Dir, sub_path: []const u8) FmtError!void {
226     fmtPathFile(fmt, file_path, check_mode, dir, sub_path) catch |err| switch (err) {
227         error.IsDir, error.AccessDenied => return fmtPathDir(fmt, file_path, check_mode, dir, sub_path),
228         else => {
229             std.log.err("unable to format '{s}': {s}", .{ file_path, @errorName(err) });
230             fmt.any_error = true;
231             return;
232         },
233     };
236 fn fmtPathDir(
237     fmt: *Fmt,
238     file_path: []const u8,
239     check_mode: bool,
240     parent_dir: fs.Dir,
241     parent_sub_path: []const u8,
242 ) FmtError!void {
243     var dir = try parent_dir.openDir(parent_sub_path, .{ .iterate = true });
244     defer dir.close();
246     const stat = try dir.stat();
247     if (try fmt.seen.fetchPut(stat.inode, {})) |_| return;
249     var dir_it = dir.iterate();
250     while (try dir_it.next()) |entry| {
251         const is_dir = entry.kind == .directory;
253         if (mem.startsWith(u8, entry.name, ".")) continue;
255         if (is_dir or entry.kind == .file and (mem.endsWith(u8, entry.name, ".zig") or mem.endsWith(u8, entry.name, ".zon"))) {
256             const full_path = try fs.path.join(fmt.gpa, &[_][]const u8{ file_path, entry.name });
257             defer fmt.gpa.free(full_path);
259             if (is_dir) {
260                 try fmtPathDir(fmt, full_path, check_mode, dir, entry.name);
261             } else {
262                 fmtPathFile(fmt, full_path, check_mode, dir, entry.name) catch |err| {
263                     std.log.err("unable to format '{s}': {s}", .{ full_path, @errorName(err) });
264                     fmt.any_error = true;
265                     return;
266                 };
267             }
268         }
269     }
272 fn fmtPathFile(
273     fmt: *Fmt,
274     file_path: []const u8,
275     check_mode: bool,
276     dir: fs.Dir,
277     sub_path: []const u8,
278 ) FmtError!void {
279     const source_file = try dir.openFile(sub_path, .{});
280     var file_closed = false;
281     errdefer if (!file_closed) source_file.close();
283     const stat = try source_file.stat();
285     if (stat.kind == .directory)
286         return error.IsDir;
288     const gpa = fmt.gpa;
289     const source_code = try std.zig.readSourceFileToEndAlloc(
290         gpa,
291         source_file,
292         std.math.cast(usize, stat.size) orelse return error.FileTooBig,
293     );
294     defer gpa.free(source_code);
296     source_file.close();
297     file_closed = true;
299     // Add to set after no longer possible to get error.IsDir.
300     if (try fmt.seen.fetchPut(stat.inode, {})) |_| return;
302     const mode: std.zig.Ast.Mode = mode: {
303         if (fmt.force_zon) break :mode .zon;
304         if (mem.endsWith(u8, sub_path, ".zon")) break :mode .zon;
305         break :mode .zig;
306     };
308     var tree = try std.zig.Ast.parse(gpa, source_code, mode);
309     defer tree.deinit(gpa);
311     if (tree.errors.len != 0) {
312         try std.zig.printAstErrorsToStderr(gpa, tree, file_path, fmt.color);
313         fmt.any_error = true;
314         return;
315     }
317     if (fmt.check_ast) {
318         if (stat.size > std.zig.max_src_size)
319             return error.FileTooBig;
321         switch (mode) {
322             .zig => {
323                 var zir = try std.zig.AstGen.generate(gpa, tree);
324                 defer zir.deinit(gpa);
326                 if (zir.hasCompileErrors()) {
327                     var wip_errors: std.zig.ErrorBundle.Wip = undefined;
328                     try wip_errors.init(gpa);
329                     defer wip_errors.deinit();
330                     try wip_errors.addZirErrorMessages(zir, tree, source_code, file_path);
331                     var error_bundle = try wip_errors.toOwnedBundle("");
332                     defer error_bundle.deinit(gpa);
333                     error_bundle.renderToStdErr(fmt.color.renderOptions());
334                     fmt.any_error = true;
335                 }
336             },
337             .zon => {
338                 var zoir = try std.zig.ZonGen.generate(gpa, tree, .{});
339                 defer zoir.deinit(gpa);
341                 if (zoir.hasCompileErrors()) {
342                     var wip_errors: std.zig.ErrorBundle.Wip = undefined;
343                     try wip_errors.init(gpa);
344                     defer wip_errors.deinit();
345                     try wip_errors.addZoirErrorMessages(zoir, tree, source_code, file_path);
346                     var error_bundle = try wip_errors.toOwnedBundle("");
347                     defer error_bundle.deinit(gpa);
348                     error_bundle.renderToStdErr(fmt.color.renderOptions());
349                     fmt.any_error = true;
350                 }
351             },
352         }
353     }
355     // As a heuristic, we make enough capacity for the same as the input source.
356     fmt.out_buffer.shrinkRetainingCapacity(0);
357     try fmt.out_buffer.ensureTotalCapacity(source_code.len);
359     try tree.renderToArrayList(&fmt.out_buffer, .{});
360     if (mem.eql(u8, fmt.out_buffer.items, source_code))
361         return;
363     if (check_mode) {
364         const stdout = std.io.getStdOut().writer();
365         try stdout.print("{s}\n", .{file_path});
366         fmt.any_error = true;
367     } else {
368         var af = try dir.atomicFile(sub_path, .{ .mode = stat.mode });
369         defer af.deinit();
371         try af.file.writeAll(fmt.out_buffer.items);
372         try af.finish();
373         const stdout = std.io.getStdOut().writer();
374         try stdout.print("{s}\n", .{file_path});
375     }
378 const std = @import("std");
379 const mem = std.mem;
380 const fs = std.fs;
381 const process = std.process;
382 const Allocator = std.mem.Allocator;
383 const Color = std.zig.Color;
384 const fatal = std.process.fatal;