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}