1 module importsort.sort;
2 
3 import std.algorithm : findSplit, remove, sort;
4 import std.array : split;
5 import std.file : DirEntry, rename;
6 import std.functional : unaryFun;
7 import std.range : ElementType;
8 import std.regex : ctRegex, matchFirst;
9 import std.stdio : File, stderr;
10 import std.string : strip, stripLeft;
11 import std.traits : isIterable;
12 import std.typecons : Yes;
13 
14 /// the pattern to determinate a line is an import or not
15 enum 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]*)$`;
16 
17 /// configuration for sorting imports
18 struct SortConfig {
19 	/// won't format the line, keep it as-is
20 	bool keepLine = false;
21 
22 	/// sort by attributes (public/static first)
23 	bool byAttribute = false;
24 
25 	/// sort by binding instead of the original
26 	bool byBinding = false;
27 
28 	/// print interesting messages (TODO)
29 	bool verbose = false;
30 
31 	/// merges imports of the same source
32 	bool merge = false;
33 }
34 
35 /// helper-struct for identifiers and its bindings
36 struct Identifier {
37 	/// SortConfig::byBinding
38 	bool byBinding;
39 
40 	/// the original e. g. 'std.stdio'
41 	string original;
42 
43 	/// the binding (alias) e. g. 'io = std.stdio'
44 	string binding;
45 
46 	/// wether this import has a binding or not
47 	@property
48 	bool hasBinding() {
49 		return binding != null;
50 	}
51 
52 	/// the string to sort
53 	string sortBy() {
54 		if (byBinding)
55 			return hasBinding ? binding : original;
56 		else
57 			return original;
58 	}
59 }
60 
61 /// the import statement description
62 struct Import {
63 	/// SortConfig::byAttribute
64 	bool byAttribute;
65 
66 	/// the original line (is `null` if merges)
67 	string line;
68 
69 	/// is a public-import
70 	bool public_;
71 
72 	/// is a static-import
73 	bool static_;
74 
75 	/// origin of the import e. g. `import std.stdio : ...;`
76 	Identifier name;
77 
78 	/// symbols of the import e. g. `import ... : File, stderr, in = stdin;`
79 	Identifier[] idents;
80 
81 	/// spaces before the import (indentation)
82 	string begin;
83 
84 	/// the newline
85 	string end;
86 
87 	/// the string to sort
88 	string sortBy() {
89 		if (byAttribute && (public_ || static_))
90 			return '\0' ~ name.sortBy;
91 		return name.sortBy;
92 	}
93 }
94 
95 /// write import-statements to `outfile` with `config`
96 void writeImports(File outfile, SortConfig config, Import[] matches) {
97 	if (!matches)
98 		return;
99 
100 	if (config.merge) {
101 		for (int i = 0; i < matches.length; i++) {
102 			for (int j = i + 1; j < matches.length; j++) {
103 				if (matches[i].name.original == matches[j].name.original
104 					&& matches[i].name.binding == matches[j].name.binding) {
105 
106 					matches[i].line = null;
107 					matches[i].idents ~= matches[j].idents;
108 					matches = matches.remove(j);
109 					j--;
110 				}
111 			}
112 		}
113 	}
114 
115 	matches.sort!((a, b) => a.sortBy < b.sortBy);
116 	bool first;
117 
118 	foreach (m; matches) {
119 		if (config.keepLine && m.line.length > 0) {
120 			outfile.write(m.line);
121 		} else {
122 			outfile.write(m.begin);
123 			if (m.public_)
124 				outfile.write("public ");
125 			if (m.static_)
126 				outfile.write("static ");
127 			if (m.name.hasBinding) {
128 				outfile.writef("import %s = %s", m.name.binding, m.name.original);
129 			} else {
130 				outfile.write("import " ~ m.name.original);
131 			}
132 			first = true;
133 			foreach (ident; m.idents) {
134 				auto begin = first ? " : " : ", ";
135 				first = false;
136 				if (ident.hasBinding) { // hasBinding
137 					outfile.writef("%s%s = %s", begin, ident.binding, ident.original);
138 				} else {
139 					outfile.write(begin ~ ident.original);
140 				}
141 			}
142 			outfile.writef(";", m.end);
143 		}
144 	}
145 }
146 
147 /// sort imports of an entry (file) (entries: DirEntry[])
148 void sortImports(alias P = "true", R)(R entries, SortConfig config)
149 		if (isIterable!R && is(ElementType!R == DirEntry)) {
150 	alias postFunc = unaryFun!P;
151 
152 	File infile, outfile;
153 	foreach (entry; entries) {
154 		stderr.writef("\033[34msorting \033[0;1m%s\033[0m\n", entry.name);
155 
156 		infile = File(entry.name);
157 		outfile = File(entry.name ~ ".new", "w");
158 
159 		sortImports(infile, outfile, config);
160 
161 		infile.close();
162 		outfile.close();
163 
164 		rename(entry.name ~ ".new", entry.name);
165 
166 		cast(void) postFunc(entry.name);
167 	}
168 }
169 
170 /// raw-implementation of sort file (infile -> outfile)
171 void sortImports(File infile, File outfile, SortConfig config) {
172 	string softEnd = null;
173 	Import[] matches;
174 
175 	foreach (line; infile.byLine(Yes.keepTerminator)) {
176 		auto linestr = line.idup;
177 		if (auto match = linestr.matchFirst(PATTERN)) { // is import
178 			if (softEnd) {
179 				if (!matches)
180 					outfile.write(softEnd);
181 				softEnd = null;
182 			}
183 
184 			auto im = Import(config.byAttribute, linestr);
185 			if (match[3]) {
186 				im.name = Identifier(config.byBinding, match[4], match[3]);
187 			} else {
188 				im.name = Identifier(config.byBinding, match[4]);
189 			}
190 			im.begin = match[1];
191 			im.end = match[6];
192 
193 			if (match[2] == "static")
194 				im.static_ = true;
195 			else if (match[2] == "public")
196 				im.public_ = true;
197 
198 			if (match[5]) {
199 				foreach (id; match[5][1 .. $].split(",")) {
200 					if (auto pair = id.findSplit("=")) { // has alias
201 						im.idents ~= Identifier(config.byBinding, pair[2].strip, pair[0].strip);
202 					} else {
203 						im.idents ~= Identifier(config.byBinding, id.strip);
204 					}
205 				}
206 				im.idents.sort!((a, b) => a.sortBy < b.sortBy);
207 			}
208 			matches ~= im;
209 		} else {
210 			if (!softEnd && linestr.stripLeft == "") {
211 				softEnd = linestr;
212 			} else {
213 				if (matches) {
214 					outfile.writeImports(config, matches);
215 					matches = [];
216 				}
217 				if (softEnd) {
218 					outfile.write(softEnd);
219 					softEnd = null;
220 				}
221 				outfile.write(line);
222 			}
223 		}
224 	}
225 	outfile.writeImports(config, matches);
226 }