1 module workspaced.com.moduleman; 2 3 import dparse.ast; 4 import dparse.lexer; 5 import dparse.parser; 6 import dparse.rollback_allocator; 7 8 import std.algorithm; 9 import std.array; 10 import std.conv; 11 import std.file; 12 import std.format; 13 import std.functional; 14 import std.path; 15 import std.string; 16 17 import workspaced.api; 18 19 @component("moduleman") 20 class ModulemanComponent : ComponentWrapper 21 { 22 mixin DefaultComponentWrapper; 23 24 protected void load() 25 { 26 config.stringBehavior = StringBehavior.source; 27 } 28 29 /// Renames a module to something else (only in the project root). 30 /// Params: 31 /// renameSubmodules: when `true`, this will rename submodules of the module too. For example when renaming `lib.com` to `lib.coms` this will also rename `lib.com.*` to `lib.coms.*` 32 /// Returns: all changes that need to happen to rename the module. If no module statement could be found this will return an empty array. 33 FileChanges[] rename(string mod, string rename, bool renameSubmodules = true) 34 { 35 if (!refInstance) 36 throw new Exception("moduleman.rename requires to be instanced"); 37 38 RollbackAllocator rba; 39 FileChanges[] changes; 40 bool foundModule = false; 41 auto from = mod.split('.'); 42 auto to = rename.split('.'); 43 foreach (file; dirEntries(instance.cwd, SpanMode.depth)) 44 { 45 if (file.extension != ".d") 46 continue; 47 string code = readText(file); 48 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 49 auto parsed = parseModule(tokens, file, &rba); 50 auto reader = new ModuleChangerVisitor(file, from, to, renameSubmodules); 51 reader.visit(parsed); 52 if (reader.changes.replacements.length) 53 changes ~= reader.changes; 54 if (reader.foundModule) 55 foundModule = true; 56 } 57 if (!foundModule) 58 return []; 59 return changes; 60 } 61 62 /// Renames/adds/removes a module from a file to match the majority of files in the folder. 63 /// Params: 64 /// file: File path to the file to normalize 65 /// code: Current code inside the text buffer 66 CodeReplacement[] normalizeModules(scope const(char)[] file, scope const(char)[] code) 67 { 68 if (!refInstance) 69 throw new Exception("moduleman.normalizeModules requires to be instanced"); 70 71 int[string] modulePrefixes; 72 modulePrefixes[""] = 0; 73 auto modName = file.replace("\\", "/").stripExtension; 74 if (modName.baseName == "package") 75 modName = modName.dirName; 76 if (modName.startsWith(instance.cwd.replace("\\", "/"))) 77 modName = modName[instance.cwd.length .. $]; 78 modName = modName.stripLeft('/'); 79 auto longest = modName; 80 foreach (imp; importPaths) 81 { 82 imp = imp.replace("\\", "/"); 83 if (imp.startsWith(instance.cwd.replace("\\", "/"))) 84 imp = imp[instance.cwd.length .. $]; 85 imp = imp.stripLeft('/'); 86 if (longest.startsWith(imp)) 87 { 88 auto shortened = longest[imp.length .. $]; 89 if (shortened.length < modName.length) 90 modName = shortened; 91 } 92 } 93 auto sourcePos = (modName ~ '/').indexOf("/source/"); 94 if (sourcePos != -1) 95 modName = modName[sourcePos + "/source".length .. $]; 96 modName = modName.stripLeft('/').replace("/", "."); 97 if (!modName.length) 98 return []; 99 auto existing = describeModule(code); 100 if (modName == existing.moduleName) 101 { 102 return []; 103 } 104 else 105 { 106 if (modName == "") 107 { 108 return [CodeReplacement([existing.outerFrom, existing.outerTo], "")]; 109 } 110 else 111 { 112 const trailing = code[min(existing.outerTo, $) .. $]; 113 // determine number of new lines to insert after full module name + semicolon 114 string semicolonNewlines = trailing.startsWith("\n\n", "\r\r", "\r\n\r\n") ? ";" 115 : trailing.startsWith("\n", "\r") ? ";\n" 116 : ";\n\n"; 117 return [ 118 CodeReplacement([existing.outerFrom, existing.outerTo], text("module ", 119 modName, (existing.outerTo == existing.outerFrom ? semicolonNewlines : ";"))) 120 ]; 121 } 122 } 123 } 124 125 /// Returns the module name parts of a D code 126 const(string)[] getModule(scope const(char)[] code) 127 { 128 return describeModule(code).raw; 129 } 130 131 /// Returns the normalized module name as string of a D code 132 string moduleName(scope const(char)[] code) 133 { 134 return describeModule(code).moduleName; 135 } 136 137 /// 138 FileModuleInfo describeModule(scope const(char)[] code) 139 { 140 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 141 ptrdiff_t start = -1; 142 size_t from, to; 143 size_t outerFrom, outerTo; 144 145 foreach (i, Token t; tokens) 146 { 147 if (t.type == tok!"module") 148 { 149 start = i; 150 outerFrom = t.index; 151 break; 152 } 153 } 154 155 if (start == -1) 156 { 157 FileModuleInfo ret; 158 start = 0; 159 if (tokens.length && tokens[0].type == tok!"scriptLine") 160 { 161 start = code.indexOfAny("\r\n"); 162 if (start == -1) 163 { 164 start = 0; 165 } 166 else 167 { 168 if (start + 1 < code.length && code[start] == '\r' && code[start + 1] == '\n') 169 start += 2; 170 else 171 start++; 172 // support /+ dub.sdl: lines or similar starts directly after a script line 173 auto leading = code[start .. $].stripLeft; 174 if (leading.startsWith("/+", "/*")) 175 { 176 size_t end = code.indexOf(leading[1] == '+' ? "+/" : "*/", start); 177 if (end != -1) 178 { 179 start = end + 2; 180 if (code[start .. $].startsWith("\r\n")) 181 start += 2; 182 else if (code[start .. $].startsWith("\r", "\n")) 183 start += 1; 184 } 185 } 186 } 187 } 188 ret.outerFrom = ret.outerTo = ret.from = ret.to = start; 189 return ret; 190 } 191 192 const(string)[] raw; 193 string moduleName; 194 foreach (t; tokens[start + 1 .. $]) 195 { 196 if (t.type == tok!";") 197 { 198 outerTo = t.index + 1; 199 break; 200 } 201 if (t.type == tok!"identifier") 202 { 203 if (from == 0) 204 from = t.index; 205 moduleName ~= t.text; 206 to = t.index + t.text.length; 207 raw ~= t.text; 208 } 209 if (t.type == tok!".") 210 { 211 moduleName ~= "."; 212 } 213 } 214 return FileModuleInfo(raw, moduleName, from, to, outerFrom, outerTo); 215 } 216 217 private: 218 LexerConfig config; 219 } 220 221 /// Represents a module statement in a file. 222 struct FileModuleInfo 223 { 224 /// Parts of the module name as array. 225 const(string)[] raw; 226 /// Whole modulename as normalized string in form a.b.c etc. 227 string moduleName = ""; 228 /// Code index of the moduleName 229 size_t from, to; 230 /// Code index of the whole module statement starting right at module and ending right after the semicolon. 231 size_t outerFrom, outerTo; 232 233 string toString() const 234 { 235 return format!"FileModuleInfo[%s..%s](name[%s..%s]=%s)"(outerFrom, outerTo, from, to, moduleName); 236 } 237 } 238 239 private: 240 241 class ModuleChangerVisitor : ASTVisitor 242 { 243 this(string file, string[] from, string[] to, bool renameSubmodules) 244 { 245 changes.file = file; 246 this.from = from; 247 this.to = to; 248 this.renameSubmodules = renameSubmodules; 249 } 250 251 alias visit = ASTVisitor.visit; 252 253 override void visit(const ModuleDeclaration decl) 254 { 255 auto mod = decl.moduleName.identifiers.map!(a => a.text).array; 256 auto orig = mod; 257 if (mod.startsWith(from) && renameSubmodules) 258 mod = to ~ mod[from.length .. $]; 259 else if (mod == from) 260 mod = to; 261 if (mod != orig) 262 { 263 foundModule = true; 264 changes.replacements ~= CodeReplacement([ 265 decl.moduleName.identifiers[0].index, 266 decl.moduleName.identifiers[$ - 1].index + decl.moduleName.identifiers[$ - 1].text.length 267 ], mod.join('.')); 268 } 269 } 270 271 override void visit(const SingleImport imp) 272 { 273 auto mod = imp.identifierChain.identifiers.map!(a => a.text).array; 274 auto orig = mod; 275 if (mod.startsWith(from) && renameSubmodules) 276 mod = to ~ mod[from.length .. $]; 277 else if (mod == from) 278 mod = to; 279 if (mod != orig) 280 { 281 changes.replacements ~= CodeReplacement([ 282 imp.identifierChain.identifiers[0].index, 283 imp.identifierChain.identifiers[$ - 1].index 284 + imp.identifierChain.identifiers[$ - 1].text.length 285 ], mod.join('.')); 286 } 287 } 288 289 override void visit(const ImportDeclaration decl) 290 { 291 if (decl) 292 { 293 return decl.accept(this); 294 } 295 } 296 297 override void visit(const BlockStatement content) 298 { 299 if (content) 300 { 301 return content.accept(this); 302 } 303 } 304 305 string[] from, to; 306 FileChanges changes; 307 bool renameSubmodules, foundModule; 308 } 309 310 unittest 311 { 312 scope backend = new WorkspaceD(); 313 auto workspace = makeTemporaryTestingWorkspace; 314 workspace.createDir("source/newmod"); 315 workspace.createDir("unregistered/source"); 316 workspace.writeFile("source/newmod/color.d", "module oldmod.color;void foo(){}"); 317 workspace.writeFile("source/newmod/render.d", "module oldmod.render;import std.color,oldmod.color;import oldmod.color.oldmod:a=b, c;import a=oldmod.a;void bar(){}"); 318 workspace.writeFile("source/newmod/display.d", "module newmod.displaf;"); 319 workspace.writeFile("source/newmod/input.d", ""); 320 workspace.writeFile("source/newmod/package.d", ""); 321 workspace.writeFile("unregistered/source/package.d", ""); 322 workspace.writeFile("unregistered/source/app.d", ""); 323 auto instance = backend.addInstance(workspace.directory); 324 backend.register!ModulemanComponent; 325 auto mod = backend.get!ModulemanComponent(workspace.directory); 326 327 instance.importPathProvider = () => ["source", "source/deeply/nested/source"]; 328 329 FileChanges[] changes = mod.rename("oldmod", "newmod").sort!"a.file < b.file".array; 330 331 assert(changes.length == 2); 332 assert(changes[0].file.endsWith("color.d")); 333 assert(changes[1].file.endsWith("render.d")); 334 335 assert(changes[0].replacements == [CodeReplacement([7, 19], "newmod.color")]); 336 assert(changes[1].replacements == [ 337 CodeReplacement([7, 20], "newmod.render"), 338 CodeReplacement([38, 50], "newmod.color"), 339 CodeReplacement([58, 77], "newmod.color.oldmod"), 340 CodeReplacement([94, 102], "newmod.a") 341 ]); 342 343 foreach (change; changes) 344 { 345 string code = readText(change.file); 346 foreach_reverse (op; change.replacements) 347 code = op.apply(code); 348 std.file.write(change.file, code); 349 } 350 351 auto nrm = mod.normalizeModules(workspace.getPath("source/newmod/input.d"), ""); 352 assert(nrm == [CodeReplacement([0, 0], "module newmod.input;\n\n")]); 353 354 nrm = mod.normalizeModules(workspace.getPath("source/newmod/package.d"), ""); 355 assert(nrm == [CodeReplacement([0, 0], "module newmod;\n\n")]); 356 357 nrm = mod.normalizeModules(workspace.getPath("source/newmod/display.d"), 358 "module oldmod.displaf;"); 359 assert(nrm == [CodeReplacement([0, 22], "module newmod.display;")]); 360 361 nrm = mod.normalizeModules(workspace.getPath("unregistered/source/app.d"), ""); 362 assert(nrm == [CodeReplacement([0, 0], "module app;\n\n")]); 363 364 nrm = mod.normalizeModules(workspace.getPath("unregistered/source/package.d"), ""); 365 assert(nrm == []); 366 367 nrm = mod.normalizeModules(workspace.getPath("source/deeply/nested/source/pkg/test.d"), ""); 368 assert(nrm == [CodeReplacement([0, 0], "module pkg.test;\n\n")]); 369 370 auto fetched = mod.describeModule("/* hello world */ module\nfoo . \nbar ;\n\nvoid foo() {"); 371 assert(fetched == FileModuleInfo(["foo", "bar"], "foo.bar", 25, 35, 18, 38)); 372 373 fetched = mod.describeModule(`#!/usr/bin/env dub 374 /+ dub.sdl: 375 name "hello" 376 +/ 377 void main() {}`); 378 assert(fetched == FileModuleInfo([], "", 48, 48, 48, 48)); 379 380 fetched = mod.describeModule("#!/usr/bin/rdmd\r\n"); 381 assert(fetched == FileModuleInfo([], "", 17, 17, 17, 17)); 382 383 fetched = mod.describeModule("#!/usr/bin/rdmd\n"); 384 assert(fetched == FileModuleInfo([], "", 16, 16, 16, 16)); 385 }