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 = fetchModule(file, 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 of a D code 106 const(string)[] getModule(string code) 107 { 108 return fetchModule("", code).raw; 109 } 110 111 private: 112 RollbackAllocator rba; 113 LexerConfig config; 114 115 ModuleFetchVisitor fetchModule(string file, string code) 116 { 117 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 118 auto parsed = parseModule(tokens, file, &rba, (&doNothing).toDelegate); 119 auto reader = new ModuleFetchVisitor(); 120 reader.visit(parsed); 121 return reader; 122 } 123 } 124 125 private: 126 127 class ModuleFetchVisitor : ASTVisitor 128 { 129 alias visit = ASTVisitor.visit; 130 131 override void visit(const ModuleDeclaration decl) 132 { 133 outerFrom = decl.startLocation; 134 outerTo = decl.endLocation + 1; // + semicolon 135 136 raw = decl.moduleName.identifiers.map!(a => a.text).array; 137 moduleName = raw.join("."); 138 from = decl.moduleName.identifiers[0].index; 139 to = decl.moduleName.identifiers[$ - 1].index + decl.moduleName.identifiers[$ - 1].text.length; 140 } 141 142 const(string)[] raw; 143 string moduleName = ""; 144 Token fileName; 145 size_t from, to; 146 size_t outerFrom, outerTo; 147 } 148 149 class ModuleChangerVisitor : ASTVisitor 150 { 151 this(string file, string[] from, string[] to, bool renameSubmodules) 152 { 153 changes.file = file; 154 this.from = from; 155 this.to = to; 156 this.renameSubmodules = renameSubmodules; 157 } 158 159 alias visit = ASTVisitor.visit; 160 161 override void visit(const ModuleDeclaration decl) 162 { 163 auto mod = decl.moduleName.identifiers.map!(a => a.text).array; 164 auto orig = mod; 165 if (mod.startsWith(from) && renameSubmodules) 166 mod = to ~ mod[from.length .. $]; 167 else if (mod == from) 168 mod = to; 169 if (mod != orig) 170 { 171 foundModule = true; 172 changes.replacements ~= CodeReplacement([decl.moduleName.identifiers[0].index, 173 decl.moduleName.identifiers[$ - 1].index + decl.moduleName.identifiers[$ - 1].text.length], 174 mod.join('.')); 175 } 176 } 177 178 override void visit(const SingleImport imp) 179 { 180 auto mod = imp.identifierChain.identifiers.map!(a => a.text).array; 181 auto orig = mod; 182 if (mod.startsWith(from) && renameSubmodules) 183 mod = to ~ mod[from.length .. $]; 184 else if (mod == from) 185 mod = to; 186 if (mod != orig) 187 { 188 changes.replacements ~= CodeReplacement([imp.identifierChain.identifiers[0].index, 189 imp.identifierChain.identifiers[$ - 1].index 190 + imp.identifierChain.identifiers[$ - 1].text.length], mod.join('.')); 191 } 192 } 193 194 override void visit(const ImportDeclaration decl) 195 { 196 if (decl) 197 { 198 return decl.accept(this); 199 } 200 } 201 202 override void visit(const BlockStatement content) 203 { 204 if (content) 205 { 206 return content.accept(this); 207 } 208 } 209 210 string[] from, to; 211 FileChanges changes; 212 bool renameSubmodules, foundModule; 213 } 214 215 void doNothing(string, size_t, size_t, string, bool) 216 { 217 } 218 219 /*unittest 220 { 221 auto workspace = makeTemporaryTestingWorkspace; 222 workspace.createDir("source/newmod"); 223 workspace.createDir("unregistered/source"); 224 workspace.writeFile("source/newmod/color.d", "module oldmod.color;void foo(){}"); 225 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(){}"); 226 workspace.writeFile("source/newmod/display.d", "module newmod.displaf;"); 227 workspace.writeFile("source/newmod/input.d", ""); 228 workspace.writeFile("source/newmod/package.d", ""); 229 workspace.writeFile("unregistered/source/package.d", ""); 230 workspace.writeFile("unregistered/source/app.d", ""); 231 232 importPathProvider = () => ["source"]; 233 234 start(workspace.directory); 235 236 FileChanges[] changes = rename("oldmod", "newmod").sort!"a.file < b.file".array; 237 238 assert(changes.length == 2); 239 assert(changes[0].file.endsWith("color.d")); 240 assert(changes[1].file.endsWith("render.d")); 241 242 assert(changes[0].replacements == [CodeReplacement([7, 19], "newmod.color")]); 243 assert(changes[1].replacements == [CodeReplacement([7, 20], "newmod.render"), 244 CodeReplacement([38, 50], "newmod.color"), CodeReplacement([58, 77], 245 "newmod.color.oldmod"), CodeReplacement([94, 102], "newmod.a")]); 246 247 foreach (change; changes) 248 { 249 string code = readText(change.file); 250 foreach_reverse (op; change.replacements) 251 code = op.apply(code); 252 std.file.write(change.file, code); 253 } 254 255 auto nrm = normalizeModules(workspace.getPath("source/newmod/input.d"), ""); 256 assert(nrm == [CodeReplacement([0, 0], "module newmod.input;")]); 257 258 nrm = normalizeModules(workspace.getPath("source/newmod/package.d"), ""); 259 assert(nrm == [CodeReplacement([0, 0], "module newmod;")]); 260 261 nrm = normalizeModules(workspace.getPath("source/newmod/display.d"), "module oldmod.displaf;"); 262 assert(nrm == [CodeReplacement([0, 22], "module newmod.display;")]); 263 264 nrm = normalizeModules(workspace.getPath("unregistered/source/app.d"), ""); 265 assert(nrm == [CodeReplacement([0, 0], "module app;")]); 266 267 nrm = normalizeModules(workspace.getPath("unregistered/source/package.d"), ""); 268 assert(nrm == []); 269 270 stop(); 271 }*/