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 }