|
1 | 1 | //! Formatting logic for Roc modules.
|
2 | 2 |
|
3 | 3 | const std = @import("std");
|
| 4 | +const parse = @import("check/parse.zig").parse; |
4 | 5 | const IR = @import("check/parse/IR.zig");
|
5 | 6 | const Node = IR.Node;
|
| 7 | +const Filesystem = @import("coordinate/Filesystem.zig"); |
6 | 8 | const tokenizer = @import("check/parse/tokenize.zig");
|
7 | 9 | const TokenizedBuffer = tokenizer.TokenizedBuffer;
|
8 | 10 | const TokenIdx = tokenizer.Token.Idx;
|
9 | 11 | const exitOnOom = @import("./collections/utils.zig").exitOnOom;
|
| 12 | +const fatal = @import("./collections/utils.zig").fatal; |
10 | 13 | const base = @import("base.zig");
|
11 | 14 |
|
12 | 15 | const NodeStore = IR.NodeStore;
|
@@ -42,6 +45,75 @@ pub fn resetWith(fmt: *Formatter, ast: IR) void {
|
42 | 45 | fmt.ast = ast;
|
43 | 46 | }
|
44 | 47 |
|
| 48 | +/// Formats all roc files in the specified path. |
| 49 | +/// Handles both single files and directories |
| 50 | +/// Returns the number of files formatted. |
| 51 | +pub fn formatPath(gpa: std.mem.Allocator, base_dir: std.fs.Dir, path: []const u8) !usize { |
| 52 | + // TODO: update this to use the filesystem abstraction |
| 53 | + // When doing so, add a mock filesystem and some tests. |
| 54 | + var count: usize = 0; |
| 55 | + // First try as a directory. |
| 56 | + if (base_dir.openDir(path, .{ .iterate = true })) |const_dir| { |
| 57 | + var dir = const_dir; |
| 58 | + defer dir.close(); |
| 59 | + // Walk is recursive. |
| 60 | + var walker = try dir.walk(gpa); |
| 61 | + defer walker.deinit(); |
| 62 | + while (try walker.next()) |entry| { |
| 63 | + if (entry.kind == .file) { |
| 64 | + if (try formatFilePath(gpa, entry.dir, entry.basename)) { |
| 65 | + count += 1; |
| 66 | + } |
| 67 | + } |
| 68 | + } |
| 69 | + } else |_| { |
| 70 | + if (try formatFilePath(gpa, base_dir, path)) { |
| 71 | + count += 1; |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | + return count; |
| 76 | +} |
| 77 | + |
| 78 | +fn formatFilePath(gpa: std.mem.Allocator, base_dir: std.fs.Dir, path: []const u8) !bool { |
| 79 | + // Skip non ".roc" files. |
| 80 | + if (!std.mem.eql(u8, std.fs.path.extension(path), ".roc")) { |
| 81 | + return false; |
| 82 | + } |
| 83 | + |
| 84 | + const input_file = try base_dir.openFile(path, .{ .mode = .read_only }); |
| 85 | + defer input_file.close(); |
| 86 | + |
| 87 | + const contents = try input_file.reader().readAllAlloc(gpa, Filesystem.max_file_size); |
| 88 | + defer gpa.free(contents); |
| 89 | + |
| 90 | + var module_env = base.ModuleEnv.init(gpa); |
| 91 | + defer module_env.deinit(); |
| 92 | + |
| 93 | + var parse_ast = parse(&module_env, contents); |
| 94 | + defer parse_ast.deinit(); |
| 95 | + if (parse_ast.errors.len > 0) { |
| 96 | + // TODO: pretty print the parse failures. |
| 97 | + const stderr = std.io.getStdErr().writer(); |
| 98 | + try stderr.print("Failed to parse '{s}' for formatting.\nErrors:\n", .{path}); |
| 99 | + for (parse_ast.errors) |err| { |
| 100 | + try stderr.print("\t{s}\n", .{@tagName(err.tag)}); |
| 101 | + } |
| 102 | + fatal("\n", .{}); |
| 103 | + } |
| 104 | + |
| 105 | + var formatter = init(parse_ast); |
| 106 | + defer formatter.deinit(); |
| 107 | + |
| 108 | + const formatted_output = formatter.formatFile(); |
| 109 | + defer gpa.free(formatted_output); |
| 110 | + |
| 111 | + const output_file = try base_dir.createFile(path, .{}); |
| 112 | + defer output_file.close(); |
| 113 | + try output_file.writeAll(formatted_output); |
| 114 | + return true; |
| 115 | +} |
| 116 | + |
45 | 117 | /// Emits a string containing the well-formed source of a Roc parse IR (AST).
|
46 | 118 | /// The resulting string is owned by the caller.
|
47 | 119 | pub fn formatFile(fmt: *Formatter) []const u8 {
|
@@ -728,8 +800,6 @@ fn pushTokenText(fmt: *Formatter, ti: TokenIdx) void {
|
728 | 800 | }
|
729 | 801 |
|
730 | 802 | fn moduleFmtsSame(source: []const u8) !void {
|
731 |
| - const parse = @import("check/parse.zig").parse; |
732 |
| - |
733 | 803 | const gpa = std.testing.allocator;
|
734 | 804 |
|
735 | 805 | var env = base.ModuleEnv.init(gpa);
|
@@ -836,8 +906,6 @@ pub fn moduleFmtsStable(gpa: std.mem.Allocator, input: []const u8, debug: bool)
|
836 | 906 | }
|
837 | 907 |
|
838 | 908 | fn parseAndFmt(gpa: std.mem.Allocator, input: []const u8, debug: bool) ![]const u8 {
|
839 |
| - const parse = @import("check/parse.zig").parse; |
840 |
| - |
841 | 909 | var module_env = base.ModuleEnv.init(gpa);
|
842 | 910 | defer module_env.deinit();
|
843 | 911 |
|
@@ -1064,3 +1132,32 @@ test "Dot access super test" {
|
1064 | 1132 | const expr = "some_fn(arg1)?.static_dispatch_method()?.next_static_dispatch_method()?.record_field?";
|
1065 | 1133 | try exprFmtsSame(expr, .no_debug);
|
1066 | 1134 | }
|
| 1135 | + |
| 1136 | +// TODO: replace this test with one that doesn't interact with the real filesystem. |
| 1137 | +test "format single file" { |
| 1138 | + const gpa = std.testing.allocator; |
| 1139 | + const roc_filename = "test.roc"; |
| 1140 | + |
| 1141 | + const roc_file = try std.fs.cwd().createFile(roc_filename, .{ .read = true }); |
| 1142 | + defer roc_file.close(); |
| 1143 | + try roc_file.writeAll( |
| 1144 | + \\module [] |
| 1145 | + \\ |
| 1146 | + \\foo = "bar" |
| 1147 | + ); |
| 1148 | + defer std.fs.cwd().deleteFile(roc_filename) catch std.debug.panic("Failed to clean up test.roc", .{}); |
| 1149 | + |
| 1150 | + const count = try formatPath(gpa, std.fs.cwd(), roc_filename); |
| 1151 | + try std.testing.expectEqual(1, count); |
| 1152 | + |
| 1153 | + // Reset file position to read formatted roc code |
| 1154 | + try roc_file.seekTo(0); |
| 1155 | + const formatted_code = try roc_file.reader().readAllAlloc(gpa, Filesystem.max_file_size); |
| 1156 | + defer gpa.free(formatted_code); |
| 1157 | + |
| 1158 | + try std.testing.expectEqualStrings( |
| 1159 | + \\module [] |
| 1160 | + \\ |
| 1161 | + \\foo = "bar" |
| 1162 | + , formatted_code); |
| 1163 | +} |
0 commit comments