main.d (8083B) download
1// (c) 2022 Friedel Schon <[email protected]>
2
3module test.main;
4
5import core.stdc.stdlib : exit;
6import core.thread : Thread;
7import core.time : Duration = dur;
8import std.algorithm : canFind, each, endsWith, filter, findSplit, map, sort;
9import std.array : array, replace;
10import std.conv : ConvException, parse;
11import std.datetime : SysTime;
12import std.file : DirEntry, SpanMode, dirEntries, exists, isDir, isFile, rename, timeLastModified;
13import std.functional : unaryFun;
14import std.range : ElementType, empty;
15import std.regex : ctRegex, matchFirst;
16import std.stdio : File, stderr, stdin, stdout;
17import std.string : format, indexOf, split, strip, stripLeft;
18import std.traits : isIterable;
19import std.typecons : Tuple, Yes, tuple;
20
21struct Identifier {
22 bool byBinding;
23 string original;
24 string binding;
25
26 string sortBy() {
27 if (byBinding)
28 return hasBinding ? binding : original;
29 else
30 return original;
31 }
32
33 bool hasBinding() {
34 return binding != null;
35 }
36}
37
38struct Import {
39 bool byAttribute;
40 string line;
41
42 bool public_;
43 bool static_;
44 Identifier name;
45 Identifier[] idents;
46 string begin;
47 string end;
48
49 string sortBy() {
50 if (byAttribute && (public_ || static_))
51 return '\0' ~ name.sortBy;
52 return name.sortBy;
53 }
54}
55
56enum PATTERN = ctRegex!`^(\s*)(?:(public|static)\s+)?import\s+(?:(\w+)\s*=\s*)?([a-zA-Z._]+)\s*(:\s*\w+(?:\s*=\s*\w+)?(?:\s*,\s*\w+(?:\s*=\s*\w+)?)*)?\s*;[ \t]*([\n\r]*)$`;
57
58enum BINARY = "importsort-d";
59enum VERSION = "0.1.0";
60enum HELP = import("help.txt")
61 .replace("{binary}", BINARY)
62 .replace("{version}", VERSION);
63
64struct SortConfig {
65 bool keepLine = false;
66 bool byAttribute = false;
67 bool byBinding = false;
68 bool verbose = false;
69}
70
71void writeImports(File outfile, SortConfig config, Import[] matches) {
72 if (!matches)
73 return;
74
75 matches.sort!((a, b) => a.sortBy < b.sortBy);
76 foreach (m; matches) {
77 if (config.keepLine) {
78 outfile.write(m.line);
79 } else {
80 outfile.write(m.begin);
81 if (m.public_)
82 outfile.write("public ");
83 if (m.static_)
84 outfile.write("static ");
85 if (m.name.hasBinding) {
86 outfile.writef("import %s = %s", m.name.binding, m.name.original);
87 } else {
88 outfile.write("import " ~ m.name.original);
89 }
90 foreach (i, ident; m.idents) {
91 auto begin = i == 0 ? " : " : ", ";
92 if (ident.hasBinding) { // hasBinding
93 outfile.writef("%s%s = %s", begin, ident.binding, ident.original);
94 } else {
95 outfile.write(begin ~ ident.original);
96 }
97 }
98 outfile.writef(";%s", m.end);
99 }
100 }
101}
102
103void sortImports(alias P = "true", R)(R entries, SortConfig config)
104 if (isIterable!R && is(ElementType!R == DirEntry)) {
105 alias postFunc = unaryFun!P;
106
107 File infile, outfile;
108 foreach (entry; entries) {
109 stderr.writef("\033[34msorting \033[0;1m%s\033[0m\n", entry.name);
110
111 infile = File(entry.name);
112 outfile = File(entry.name ~ ".new", "w");
113
114 sortImports(infile, outfile, config);
115
116 infile.close();
117 outfile.close();
118
119 rename(entry.name ~ ".new", entry.name);
120
121 cast(void) postFunc(entry.name);
122 }
123}
124
125void sortImports(File infile, File outfile, SortConfig config) {
126 string softEnd = null;
127 Import[] matches;
128
129 foreach (line; infile.byLine(Yes.keepTerminator)) {
130 auto linestr = line.idup;
131 if (auto match = linestr.matchFirst(PATTERN)) { // is import
132 if (softEnd) {
133 if (!matches)
134 outfile.write(softEnd);
135 softEnd = null;
136 }
137
138 auto im = Import(config.byAttribute, linestr);
139 if (match[3]) {
140 im.name = Identifier(config.byBinding, match[4], match[3]);
141 } else {
142 im.name = Identifier(config.byBinding, match[4]);
143 }
144 im.begin = match[1];
145 im.end = match[6];
146
147 if (match[2] == "static")
148 im.static_ = true;
149 else if (match[2] == "public")
150 im.public_ = true;
151
152 if (match[5]) {
153 foreach (id; match[5][1 .. $].split(",")) {
154 if (auto pair = id.findSplit("=")) { // has alias
155 im.idents ~= Identifier(config.byBinding, pair[2].strip, pair[0].strip);
156 } else {
157 im.idents ~= Identifier(config.byBinding, id.strip);
158 }
159 }
160 im.idents.sort!((a, b) => a.sortBy < b.sortBy);
161 }
162 matches ~= im;
163 } else {
164 if (!softEnd && linestr.stripLeft == "") {
165 softEnd = linestr;
166 } else {
167 if (matches) {
168 outfile.writeImports(config, matches);
169 matches = [];
170 }
171 if (softEnd) {
172 outfile.write(softEnd);
173 softEnd = null;
174 }
175 outfile.write(line);
176 }
177 }
178 }
179 outfile.writeImports(config, matches);
180}
181
182DirEntry[] listEntries(alias F = "true")(string[] input, bool recursive) {
183 alias filterFunc = unaryFun!F;
184
185 DirEntry[] entries;
186
187 foreach (path; input) {
188 if (!exists(path)) {
189 stderr.writef("error: '%s' does not exist\n", path);
190 exit(1);
191 } else if (isDir(path)) {
192 foreach (entry; dirEntries(path, recursive ? SpanMode.depth : SpanMode.shallow)) {
193 if (entry.isFile && entry.name.endsWith(".d") && filterFunc(entry.name))
194 entries ~= entry;
195 }
196 } else if (isFile(path)) {
197 if (!path.endsWith(".d")) {
198 stderr.writef("error: '%s' is not a .d-file\n", path);
199 exit(1);
200 }
201 if (filterFunc(path))
202 entries ~= DirEntry(path);
203 } else {
204 stderr.writef("error: '%s' is not a file or directory\n", path);
205 exit(1);
206 }
207 }
208 return entries;
209}
210
211void main(string[] args) {
212 SortConfig config;
213 bool inline;
214 string output;
215 string[] input;
216 bool watcher;
217 bool watcherDelaySet;
218 double watcherDelay = 0.1; // sec
219 bool recursive;
220
221 // -*- option parser -*-
222
223 bool nextOutput;
224 bool nextWatcherDelay;
225 foreach (arg; args[1 .. $]) {
226 if (nextOutput) {
227 output = arg;
228 nextOutput = false;
229 } else if (nextWatcherDelay) {
230 try {
231 watcherDelay = parse!double(arg);
232 } catch (ConvException) {
233 stderr.writef("error: cannot parse delay '%s' to an integer\n", arg);
234 exit(1);
235 }
236 watcherDelaySet = true;
237 nextWatcherDelay = false;
238 } else if (arg == "--help" || arg == "-h") {
239 stdout.writeln(HELP);
240 return;
241 } else if (arg == "--verbose" || arg == "-v") {
242 config.verbose = true;
243 } else if (arg == "--keep" || arg == "-k") {
244 config.keepLine = true;
245 } else if (arg == "--attribute" || arg == "-a") {
246 config.byAttribute = true;
247 } else if (arg == "--binding" || arg == "-b") {
248 config.byBinding = true;
249 } else if (arg == "--inline" || arg == "-i") {
250 inline = true;
251 } else if (arg == "--recursive" || arg == "-r") {
252 recursive = true;
253 // TODO: --watch
254 /*} else if (arg == "--watch" || arg == "-w") {
255 watcher = true;
256 } else if (arg == "--delay" || arg == "-d") {
257 if (watcherDelaySet) {
258 stderr.writeln("error: watcher-delay already specified");
259 stderr.writeln(HELP);
260 exit(1);
261 }
262 nextWatcherDelay = true;*/
263 } else if (arg == "--output" || arg == "-o") {
264 if (output != null) {
265 stderr.writeln("error: output already specified");
266 stderr.writeln(HELP);
267 exit(1);
268 }
269 nextOutput = true;
270 } else if (arg[0] == '-') {
271 stderr.writef("error: unknown option '%s'\n", arg);
272 stderr.writeln(HELP);
273 exit(1);
274 } else {
275 input ~= arg;
276 }
277 }
278 if (recursive && input.length == 0) {
279 stderr.writeln("error: cannot use '--recursive' and specify no input");
280 exit(1);
281 }
282 if (inline && input.length == 0) {
283 stderr.writeln("error: cannot use '--inline' and read from stdin");
284 exit(1);
285 }
286 if ((!inline || output.length > 0) && input.length > 0) {
287 stderr.writeln("error: if you use inputs you must use '--inline'");
288 exit(1);
289 }
290 // -*- operation -*-
291
292 /* if (watcher) {
293 stderr.writeln("\033[1;34mwatching files...\033[0m");
294 SysTime[string] lastModified;
295 for (;;) {
296 auto entries = listEntries!(x => x !in lastModified
297 || lastModified[x] != x.timeLastModified)(input, recursive);
298
299 foreach (entry; entries) {
300 lastModified[entry.name] = entry.timeLastModified;
301 }
302 entries.sortImports(config);
303 Thread.sleep(Duration!"msecs"(cast(long) watcherDelay * 1000));
304 }
305 } else
306 */
307 if (input == null) {
308 File outfile = (output == null) ? stdout : File(output);
309
310 sortImports(stdin, outfile, config);
311 if (output)
312 outfile.close();
313 } else {
314 listEntries(input, recursive).sortImports(config);
315 }
316}