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 FileChanges[] changes; 38 bool foundModule = false; 39 auto from = mod.split('.'); 40 auto to = rename.split('.'); 41 foreach (file; dirEntries(instance.cwd, SpanMode.depth)) 42 { 43 if (file.extension != ".d") 44 continue; 45 string code = readText(file); 46 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 47 auto parsed = parseModule(tokens, file, &rba); 48 auto reader = new ModuleChangerVisitor(file, from, to, renameSubmodules); 49 reader.visit(parsed); 50 if (reader.changes.replacements.length) 51 changes ~= reader.changes; 52 if (reader.foundModule) 53 foundModule = true; 54 } 55 if (!foundModule) 56 return []; 57 return changes; 58 } 59 60 /// Renames/adds/removes a module from a file to match the majority of files in the folder. 61 /// Params: 62 /// file: File path to the file to normalize 63 /// code: Current code inside the text buffer 64 CodeReplacement[] normalizeModules(scope const(char)[] file, scope const(char)[] code) 65 { 66 if (!refInstance) 67 throw new Exception("moduleman.normalizeModules requires to be instanced"); 68 69 int[string] modulePrefixes; 70 modulePrefixes[""] = 0; 71 auto modName = file.replace("\\", "/").stripExtension; 72 if (modName.baseName == "package") 73 modName = modName.dirName; 74 if (modName.startsWith(instance.cwd.replace("\\", "/"))) 75 modName = modName[instance.cwd.length .. $]; 76 modName = modName.stripLeft('/'); 77 auto longest = modName; 78 foreach (imp; importPaths) 79 { 80 imp = imp.replace("\\", "/"); 81 if (imp.startsWith(instance.cwd.replace("\\", "/"))) 82 imp = imp[instance.cwd.length .. $]; 83 imp = imp.stripLeft('/'); 84 if (longest.startsWith(imp)) 85 { 86 auto shortened = longest[imp.length .. $]; 87 if (shortened.length < modName.length) 88 modName = shortened; 89 } 90 } 91 auto sourcePos = (modName ~ '/').indexOf("/source/"); 92 if (sourcePos != -1) 93 modName = modName[sourcePos + "/source".length .. $]; 94 modName = modName.stripLeft('/').replace("/", "."); 95 if (!modName.length) 96 return []; 97 auto existing = describeModule(code); 98 if (modName == existing.moduleName) 99 { 100 return []; 101 } 102 else 103 { 104 if (modName == "") 105 return [CodeReplacement([existing.outerFrom, existing.outerTo], "")]; 106 else 107 return [ 108 CodeReplacement([existing.outerFrom, existing.outerTo], text("module ", 109 modName, (existing.outerTo == existing.outerFrom ? ";\n\n" : ";"))) 110 ]; 111 } 112 } 113 114 /// Returns the module name parts of a D code 115 const(string)[] getModule(scope const(char)[] code) 116 { 117 return describeModule(code).raw; 118 } 119 120 /// Returns the normalized module name as string of a D code 121 string moduleName(scope const(char)[] code) 122 { 123 return describeModule(code).moduleName; 124 } 125 126 /// 127 FileModuleInfo describeModule(scope const(char)[] code) 128 { 129 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 130 ptrdiff_t start = -1; 131 size_t from, to; 132 size_t outerFrom, outerTo; 133 134 foreach (i, Token t; tokens) 135 { 136 if (t.type == tok!"module") 137 { 138 start = i; 139 outerFrom = t.index; 140 break; 141 } 142 } 143 144 if (start == -1) 145 return FileModuleInfo.init; 146 147 const(string)[] raw; 148 string moduleName; 149 foreach (t; tokens[start + 1 .. $]) 150 { 151 if (t.type == tok!";") 152 { 153 outerTo = t.index + 1; 154 break; 155 } 156 if (t.type == tok!"identifier") 157 { 158 if (from == 0) 159 from = t.index; 160 moduleName ~= t.text; 161 to = t.index + t.text.length; 162 raw ~= t.text; 163 } 164 if (t.type == tok!".") 165 { 166 moduleName ~= "."; 167 } 168 } 169 return FileModuleInfo(raw, moduleName, from, to, outerFrom, outerTo); 170 } 171 172 private: 173 RollbackAllocator rba; 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 }