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