importsort-d

Sort and format imports in DLang
Log | Files | Refs | README

sort.d (7127B)


      1 // (c) 2022-2023 Friedel Schon <[email protected]>
      2 
      3 module importsort.sort;
      4 
      5 import importsort.main : SortConfig;
      6 import std.algorithm : canFind, findSplit, remove, sort;
      7 import std.algorithm.comparison : equal;
      8 import std.algorithm.searching : all;
      9 import std.array : split;
     10 import std.conv : to;
     11 import std.file : DirEntry, rename;
     12 import std.functional : unaryFun;
     13 import std.range : ElementType, empty;
     14 import std.regex : ctRegex, matchFirst;
     15 import std.stdio : File, stderr;
     16 import std.string : strip, stripLeft;
     17 import std.traits : isIterable;
     18 import std.typecons : Nullable, Yes, nullable;
     19 import std.uni : asLowerCase, isSpace, isWhite;
     20 
     21 /// the pattern to determinate a line is an import or not
     22 enum 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 
     24 bool iterableOf(T, E)() {
     25 	return isIterable!T && is(ElementType!T == E);
     26 }
     27 
     28 T[] 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 
     36 string 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
     47 struct 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
     72 struct 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 
    105 bool less(SortConfig config, string a, string b) {
    106 	return config.ignoreCase ? a.asLowerCase.to!string < b.asLowerCase.to!string : a < b;
    107 }
    108 
    109 Import[] 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 
    136 bool 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`
    148 void 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[])
    186 void 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 
    210 bool 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 }