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