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