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 }