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