misc/importsort-d

src/sort.d in master
Repositories | Summary | Log | Files | README.md

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}