sort.d (7127B) download
1// (c) 2022-2023 Friedel Schon <[email protected]>
2
3module importsort.sort;
4
5import importsort.main : SortConfig;
6import std.algorithm : canFind, findSplit, remove, sort;
7import std.algorithm.comparison : equal;
8import std.algorithm.searching : all;
9import std.array : split;
10import std.conv : to;
11import std.file : DirEntry, rename;
12import std.functional : unaryFun;
13import std.range : ElementType, empty;
14import std.regex : ctRegex, matchFirst;
15import std.stdio : File, stderr;
16import std.string : strip, stripLeft;
17import std.traits : isIterable;
18import std.typecons : Nullable, Yes, nullable;
19import std.uni : asLowerCase, isSpace, isWhite;
20
21/// the pattern to determinate a line is an import or not
22enum PATTERN = ctRegex!`(?:(public|static)\s+)?import\s+(?:(\w+)\s*=\s*)?([a-zA-Z._]+)\s*(:\s*\w+(?:\s*=\s*\w+)?(?:\s*,\s*\w+(?:\s*=\s*\w+)?)*)?;`;
23
24bool iterableOf(T, E)() {
25 return isIterable!T && is(ElementType!T == E);
26}
27
28T[] uniq(T)(in T[] s) {
29 T[] result;
30 foreach (T c; s)
31 if (!result.canFind(c))
32 result ~= c;
33 return result;
34}
35
36string getLineEnding(string str) {
37 string ending = "";
38 foreach_reverse (chr; str) {
39 if (chr != '\n' && chr != '\r')
40 break;
41 ending = chr ~ ending;
42 }
43 return ending;
44}
45
46/// helper-struct for identifiers and its bindings
47struct Identifier {
48 /// SortConfig::byBinding
49 bool byBinding;
50
51 /// the original e. g. 'std.stdio'
52 string original;
53
54 /// the binding (alias) e. g. 'io = std.stdio'
55 string binding;
56
57 /// wether this import has a binding or not
58 @property bool hasBinding() {
59 return binding != null;
60 }
61
62 /// the string to sort
63 string sortBy() {
64 if (byBinding)
65 return hasBinding ? binding : original;
66 else
67 return original;
68 }
69}
70
71/// the import statement description
72struct Import {
73 /// SortConfig::byAttribute
74 bool byAttribute;
75
76 /// the original line (is `null` if merges)
77 string line;
78
79 /// is a public-import
80 bool public_;
81
82 /// is a static-import
83 bool static_;
84
85 /// origin of the import e. g. `import std.stdio : ...`
86 Identifier name;
87
88 /// symbols of the import e. g. `import ... : File, stderr, in = stdin`
89 Identifier[] idents;
90
91 /// spaces before the import (indentation)
92 string begin;
93
94 /// the newline
95 string end;
96
97 /// the string to sort
98 string sortBy() {
99 if (byAttribute && (public_ || static_))
100 return '\0' ~ name.sortBy;
101 return name.sortBy;
102 }
103}
104
105bool less(SortConfig config, string a, string b) {
106 return config.ignoreCase ? a.asLowerCase.to!string < b.asLowerCase.to!string : a < b;
107}
108
109Import[] sortMatches(SortConfig config, Import[] matches) {
110 if (config.merge) {
111 for (int i = 0; i < matches.length; i++) {
112 for (int j = i + 1; j < matches.length; j++) {
113 if (matches[i].name.original == matches[j].name.original
114 && matches[i].name.binding == matches[j].name.binding) {
115
116 matches[i].line = null;
117 matches[i].idents ~= matches[j].idents;
118 matches = matches.remove(j);
119 j--;
120 }
121 }
122 }
123
124 foreach (ref match; matches)
125 match.idents = uniq(match.idents);
126 }
127
128 matches.sort!((a, b) => less(config, a.sortBy, b.sortBy));
129
130 foreach (m; matches)
131 m.idents.sort!((a, b) => less(config, a.sortBy, b.sortBy));
132
133 return matches;
134}
135
136bool checkChanged(SortConfig config, Import[] matches) {
137 if (!matches)
138 return false;
139
140 auto original = matches.dup;
141
142 matches = sortMatches(config, matches);
143
144 return !equal(original, matches);
145}
146
147/// write import-statements to `outfile` with `config`
148void writeImports(File outfile, SortConfig config, Import[] matches) {
149 if (!matches)
150 return;
151
152 matches = sortMatches(config, matches);
153
154 bool first;
155
156 foreach (m; matches) {
157 if (config.keepLine && m.line.length > 0) {
158 outfile.write(m.line);
159 } else {
160 outfile.write(m.begin);
161 if (m.public_)
162 outfile.write("public ");
163 if (m.static_)
164 outfile.write("static ");
165 if (m.name.hasBinding) {
166 outfile.writef("import %s = %s", m.name.binding, m.name.original);
167 } else {
168 outfile.write("import " ~ m.name.original);
169 }
170 first = true;
171 foreach (ident; m.idents) {
172 auto begin = first ? " : " : ", ";
173 first = false;
174 if (ident.hasBinding) { // hasBinding
175 outfile.writef("%s%s = %s", begin, ident.binding, ident.original);
176 } else {
177 outfile.write(begin ~ ident.original);
178 }
179 }
180 outfile.writef(";%s", m.end);
181 }
182 }
183}
184
185/// sort imports of an entry (file) (entries: DirEntry[])
186void sortImports(R)(R entries, SortConfig config) if (iterableOf!(R, DirEntry)) {
187
188 File infile, outfile;
189 foreach (entry; entries) {
190 infile = File(entry.name);
191
192 if (config.force || sortImports(config, infile, Nullable!(File).init)) { // is changed
193 if (!config.force)
194 infile.seek(0);
195
196 outfile = File(entry.name ~ ".new", "w");
197 sortImports(config, infile, nullable(outfile));
198 rename(entry.name ~ ".new", entry.name);
199 outfile.close();
200
201 stderr.writef("\033[34msorted \033[0;1m%s\033[0m\n", entry.name);
202 } else {
203 stderr.writef("\033[33munchanged \033[0;1m%s\033[0m\n", entry.name);
204 }
205
206 infile.close();
207 }
208}
209
210bool sortImports(SortConfig config, File infile, Nullable!File outfile) {
211 string softEnd = null;
212 Import[] matches;
213
214 foreach (line; infile.byLine(Yes.keepTerminator)) {
215 auto linestr = line.idup;
216
217 parse_match:
218 if (auto match = linestr.matchFirst(PATTERN)) { // is import
219 if (softEnd) {
220 if (!matches && !outfile.isNull)
221 outfile.get().write(softEnd);
222 softEnd = null;
223 }
224
225 auto im = Import(config.byAttribute, linestr);
226
227 if (!match.pre.all!isSpace) {
228 if (matches) {
229 im.begin = matches[$ - 1].begin;
230
231 if (!outfile.isNull)
232 outfile.get().writeImports(config, matches);
233 else if (checkChanged(config, matches))
234 return true;
235
236 matches = [];
237 }
238
239 if (!outfile.isNull)
240 outfile.get().writeln(line);
241 } else {
242 im.begin = match.pre;
243 }
244
245 if (match[2]) {
246 im.name = Identifier(config.byBinding, match[3], match[2]);
247 } else {
248 im.name = Identifier(config.byBinding, match[3]);
249 }
250
251 if (match[1] == "static")
252 im.static_ = true;
253 else if (match[1] == "public")
254 im.public_ = true;
255
256 if (match[4]) {
257 foreach (id; match[4][1 .. $].split(",")) {
258 if (auto pair = id.findSplit("=")) { // has alias
259 im.idents ~= Identifier(config.byBinding, pair[2].strip, pair[0].strip);
260 } else {
261 im.idents ~= Identifier(config.byBinding, id.strip);
262 }
263 }
264 }
265
266 im.end = linestr.getLineEnding;
267 matches ~= im;
268
269 if (!match.post.all!isWhite) {
270 linestr = match.post.idup;
271 goto parse_match;
272 }
273 } else {
274 if (!softEnd && linestr.all!isSpace) {
275 softEnd = linestr;
276 } else {
277 if (matches) {
278 if (!outfile.isNull)
279 outfile.get().writeImports(config, matches);
280 else if (checkChanged(config, matches))
281 return true;
282
283 matches = [];
284 }
285 if (softEnd) {
286 if (!outfile.isNull)
287 outfile.get().write(softEnd);
288 softEnd = null;
289 }
290 if (!outfile.isNull)
291 outfile.get().write(line);
292 }
293 }
294 }
295
296 // flush last imports
297
298 if (!outfile.isNull)
299 outfile.get().writeImports(config, matches);
300 else
301 return checkChanged(config, matches);
302
303 return false;
304}