Line data Source code
1 : // wok fmt -- the canonical formatter, and the linter that is the same thing
2 : // asking a question.
3 : //
4 : // wok fmt FILE... print the canonical form to stdout
5 : // wok fmt -w FILE... rewrite each file in place
6 : // wok fmt --check FILE... exit non-zero if any file is not canonical
7 : // wok fmt --pipe < FILE source on stdin, canonical form on stdout
8 : // wok fmt --pipe --check ... the verdict only, as an exit code
9 : //
10 : // The tool is one-shot by design. Measured: process creation costs 1.46 ms and
11 : // is indistinguishable from an empty binary; a 750-file project checks in
12 : // 11.7 ms. There is nothing a cache could save that is worth the staleness it
13 : // would risk.
14 :
15 : // The prelude comes FIRST: it carries the POSIX feature-test macros, which
16 : // have no effect once a system header has been read. wok_base.h hard-errors
17 : // if it is reached too late.
18 : #include "wok_base.h"
19 :
20 : #include <stdio.h>
21 : #include <stdlib.h>
22 : #include <string.h>
23 :
24 : #include "../wok_arena.h"
25 : #include "../wok_ast.h"
26 : #include "../wok_diag.h"
27 : #include "../wok_parse.h"
28 : #include "../wok_print.h"
29 : #include "../wok_sexpr.h"
30 : #include "../wok_shape.h"
31 : #include "../wok_write.h"
32 : #include "../wok_token.h"
33 :
34 : typedef enum { M_STDOUT, M_WRITE, M_CHECK } Mode;
35 :
36 94 : static char *slurp(const char *path, usize *n) {
37 94 : FILE *fp = fopen(path, "rb");
38 94 : if (!fp) return nullptr;
39 94 : if (fseek(fp, 0, SEEK_END) != 0) { fclose(fp); return nullptr; }
40 94 : long sz = ftell(fp);
41 94 : if (sz < 0) { fclose(fp); return nullptr; }
42 94 : rewind(fp);
43 94 : char *b = (char *)malloc((usize)sz + 1);
44 94 : if (!b) { fclose(fp); return nullptr; }
45 94 : usize got = fread(b, 1, (usize)sz, fp);
46 94 : b[got] = '\0';
47 94 : *n = got;
48 94 : fclose(fp);
49 94 : return b;
50 : }
51 :
52 :
53 : // `stamp` is nullptr in PIPE mode: there is no file to guard against a
54 : // concurrent edit and none to replace, so interlock B does not apply. Every
55 : // other check does -- a pipe is not an excuse to skip the safety properties.
56 335 : static int process_text(const char *path, const char *src, usize n, Mode mode,
57 : const WokFileStamp *stamp) {
58 :
59 335 : WokArena *a = wok_arena_new(0);
60 335 : WokDiagSink *d = wok_diag_new(a, path, src, n);
61 335 : WokNode *tree = wok_parse_source(src, n, a, d);
62 335 : if (wok_diag_count(d) != 0) {
63 1 : wok_diag_render(d, stderr);
64 1 : wok_arena_free(a);
65 1 : return 1; // input that does not parse has no canonical form
66 : }
67 :
68 334 : wok_print_reports_reset();
69 334 : char *formatted = wok_print_string_named(tree, src, a, path);
70 334 : usize fn = strlen(formatted);
71 :
72 : // INTERLOCK A0: the printer's own safety property. A line break that is not
73 : // a continuation line changes the tree rather than the text, and the printer
74 : // says so at the moment it writes one.
75 334 : int rc = 0;
76 : // INTERLOCK A0b: a node with no canonical form at all. Until this counter
77 : // existed the only guard was that such a tree can reach here solely from a
78 : // parse that already reported a diagnostic -- true, but by ORDERING rather
79 : // than by check, so it held only as long as nobody added a second way in.
80 334 : if (wok_print_unprintable_count() != 0) {
81 0 : fprintf(stderr, "wok fmt: INTERNAL: %s holds a node with no canonical "
82 : "form; refusing to write\n", path);
83 0 : rc = 2;
84 : }
85 334 : if (rc == 0 && wok_print_fault_count() != 0) {
86 0 : fprintf(stderr, "wok fmt: INTERNAL: %s was filled with an unsafe line "
87 : "break; refusing to write\n", path);
88 0 : rc = 2;
89 : }
90 :
91 : // INTERLOCK A: did formatting change the PROGRAM? The dump is the canonical,
92 : // position-free identity of a program, so comparing dumps answers exactly
93 : // that -- and unlike a fingerprint, it can say what differed.
94 334 : WokDiagSink *rd = wok_diag_new(a, path, formatted, fn);
95 334 : WokNode *reparsed = wok_parse_source(formatted, fn, a, rd);
96 334 : if (rc != 0) {
97 : // already refused
98 334 : } else if (wok_diag_count(rd) != 0) {
99 0 : fprintf(stderr, "wok fmt: INTERNAL: %s does not re-parse after formatting; "
100 : "refusing to write\n", path);
101 0 : wok_diag_render(rd, stderr);
102 0 : rc = 2;
103 : } else {
104 334 : char *before = wok_sexpr_dump_string(tree, src, a);
105 334 : char *after = wok_sexpr_dump_string(reparsed, formatted, a);
106 334 : if (!before || !after || strcmp(before, after) != 0) {
107 0 : fprintf(stderr, "wok fmt: INTERNAL: formatting %s would CHANGE THE "
108 : "PROGRAM; refusing to write\n", path);
109 0 : rc = 2;
110 : }
111 : }
112 :
113 0 : if (rc == 0) {
114 334 : if (mode == M_CHECK) {
115 97 : WokDiagSink *sd = wok_diag_new(a, path, src, n);
116 97 : char *want = wok_shape(formatted, fn, a, sd);
117 97 : char *have = wok_shape(src, n, a, sd);
118 97 : if (strcmp(want, have) != 0) {
119 : // In pipe mode stdout is the payload channel, so the name would be
120 : // noise: an agent reads the exit code.
121 25 : if (stamp != nullptr) printf("%s\n", path);
122 : rc = 1;
123 : }
124 237 : } else if (mode == M_STDOUT) {
125 237 : fwrite(formatted, 1, fn, stdout);
126 : } else {
127 : // INTERLOCK B, the owner's rule, plus an atomic replace: the file is
128 : // written to a temporary and renamed, so no crash or full disk can
129 : // leave the user's source truncated, and a concurrent save is refused
130 : // rather than clobbered.
131 0 : switch (wok_write_atomic(path, formatted, fn, *stamp)) {
132 : case WOK_WRITE_OK:
133 : case WOK_WRITE_UNCHANGED:
134 : break;
135 0 : case WOK_WRITE_MOVED:
136 0 : fprintf(stderr, "wok fmt: %s changed on disk while formatting; "
137 : "not written\n", path);
138 0 : rc = 2;
139 0 : break;
140 0 : case WOK_WRITE_FAILED:
141 0 : fprintf(stderr, "wok fmt: could not write %s; it is unchanged\n", path);
142 0 : rc = 2;
143 0 : break;
144 : }
145 : }
146 : }
147 :
148 334 : wok_arena_free(a);
149 334 : return rc;
150 : }
151 :
152 94 : static int process_file(const char *path, Mode mode) {
153 94 : usize n = 0;
154 94 : char *src = slurp(path, &n);
155 94 : if (!src) {
156 0 : fprintf(stderr, "wok fmt: cannot read %s\n", path);
157 0 : return 2;
158 : }
159 94 : WokFileStamp at_read = wok_file_stamp(path);
160 94 : int rc = process_text(path, src, n, mode, &at_read);
161 94 : free(src);
162 94 : return rc;
163 : }
164 :
165 : // --pipe: source on stdin, canonical form on stdout. For an agent or an editor
166 : // that has the text in hand and does not want to invent a temporary file.
167 241 : static int process_stdin(Mode mode) {
168 241 : usize cap = 1 << 16, n = 0;
169 241 : char *src = (char *)malloc(cap);
170 241 : if (!src) return 2;
171 481 : for (;;) {
172 481 : if (n == cap) {
173 0 : cap *= 2;
174 0 : char *bigger = (char *)realloc(src, cap);
175 0 : if (!bigger) { free(src); return 2; }
176 : src = bigger;
177 : }
178 481 : usize got = fread(src + n, 1, cap - n, stdin);
179 481 : n += got;
180 481 : if (got == 0) break;
181 : }
182 241 : src[n < cap ? n : cap - 1] = '\0';
183 241 : int rc = process_text("<stdin>", src, n, mode, nullptr);
184 241 : free(src);
185 241 : return rc;
186 : }
187 :
188 337 : int main(int argc, char **argv) {
189 337 : Mode mode = M_STDOUT;
190 337 : bool pipe_mode = false;
191 337 : int i = 1;
192 678 : for (; i < argc && argv[i][0] == '-' && argv[i][1] != '\0'; i++) {
193 341 : if (strcmp(argv[i], "-w") == 0) mode = M_WRITE;
194 340 : else if (strcmp(argv[i], "--check") == 0) mode = M_CHECK;
195 243 : else if (strcmp(argv[i], "--pipe") == 0) pipe_mode = true;
196 : else {
197 0 : fprintf(stderr, "wok fmt: unknown flag %s\n", argv[i]);
198 0 : return 2;
199 : }
200 : }
201 :
202 337 : if (pipe_mode) {
203 243 : if (mode == M_WRITE) {
204 1 : fprintf(stderr, "wok fmt: --pipe has no file to write; use --pipe alone "
205 : "for the formatted text, or --pipe --check for the "
206 : "verdict\n");
207 1 : return 2;
208 : }
209 242 : if (i < argc) {
210 1 : fprintf(stderr, "wok fmt: --pipe reads stdin; do not also name files\n");
211 1 : return 2;
212 : }
213 241 : return process_stdin(mode);
214 : }
215 :
216 94 : if (i >= argc) {
217 0 : fprintf(stderr, "usage: wokfmt [-w|--check] FILE...\n"
218 : " wokfmt --pipe [--check] < FILE\n");
219 0 : return 2;
220 : }
221 : int worst = 0;
222 188 : for (; i < argc; i++) {
223 94 : int rc = process_file(argv[i], mode);
224 94 : if (rc > worst) worst = rc;
225 : }
226 : return worst;
227 : }
|