| 1 | const std = @import("std"); |
| 2 | const Io = std.Io; |
| 3 | const mem = std.mem; |
| 4 | const fs = std.fs; |
| 5 | const process = std.process; |
| 6 | const Allocator = std.mem.Allocator; |
| 7 | const Color = std.zig.Color; |
| 8 | const fatal = std.process.fatal; |
| 9 | |
| 10 | const 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 | |
| 31 | const 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 | |
| 50 | pub 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 | |
| 227 | fn 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 | |
| 238 | fn 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 | |
| 276 | fn 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. |
| 394 | pub 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 | } |