1 module workspaced.com.moduleman; 2 3 import dparse.parser; 4 import dparse.lexer; 5 import dparse.ast; 6 import dparse.rollback_allocator; 7 8 import std.algorithm; 9 import std.array; 10 import std.string; 11 import std.file; 12 import std.path; 13 14 import workspaced.api; 15 16 @component("moduleman") : 17 18 /// Initializes the module & import parser. Call with `{"cmd": "load", "components": ["moduleman"]}` 19 @load void start(string projectRoot) 20 { 21 config.stringBehavior = StringBehavior.source; 22 cache = new StringCache(StringCache.defaultBucketCount); 23 .projectRoot = projectRoot; 24 } 25 26 /// Has no purpose right now. 27 @unload void stop() 28 { 29 } 30 31 /// Renames a module to something else (only in the project root). 32 /// Params: 33 /// 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.*` 34 /// Returns: all changes that need to happen to rename the module. If no module statement could be found this will return an empty array. 35 /// Call_With: `{"subcmd": "rename"}` 36 @arguments("subcmd", "rename") 37 FileChanges[] rename(string mod, string rename, bool renameSubmodules = true) 38 { 39 FileChanges[] changes; 40 bool foundModule = false; 41 auto from = mod.split('.'); 42 auto to = rename.split('.'); 43 foreach (file; dirEntries(projectRoot, SpanMode.depth)) 44 { 45 if (file.extension != ".d") 46 continue; 47 string code = readText(file); 48 auto tokens = getTokensForParser(cast(ubyte[]) code, config, cache); 49 auto parsed = parseModule(tokens, file, &rba, &doNothing); 50 auto reader = new ModuleChangerVisitor(file, from, to, renameSubmodules); 51 reader.visit(parsed); 52 if (reader.changes.replacements.length) 53 changes ~= reader.changes; 54 if (reader.foundModule) 55 foundModule = true; 56 } 57 if (!foundModule) 58 return []; 59 return changes; 60 } 61 62 /// Renames/adds/removes a module from a file to match the majority of files in the folder. 63 /// Params: 64 /// file: File path to the file to normalize 65 /// code: Current code inside the text buffer 66 CodeReplacement[] normalizeModules(string file, string code) 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(projectRoot.replace("\\", "/"))) 74 modName = modName[projectRoot.length .. $]; 75 modName = modName.stripLeft('/'); 76 foreach (imp; importPathProvider()) 77 { 78 imp = imp.replace("\\", "/"); 79 if (imp.startsWith(projectRoot.replace("\\", "/"))) 80 imp = imp[projectRoot.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 = fetchModule(file, 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 of a D code 109 const(string)[] getModule(string code) 110 { 111 return fetchModule("", code).raw; 112 } 113 114 private __gshared: 115 RollbackAllocator rba; 116 LexerConfig config; 117 StringCache* cache; 118 string projectRoot; 119 120 ModuleFetchVisitor fetchModule(string file, string code) 121 { 122 auto tokens = getTokensForParser(cast(ubyte[]) code, config, cache); 123 auto parsed = parseModule(tokens, file, &rba, &doNothing); 124 auto reader = new ModuleFetchVisitor(); 125 reader.visit(parsed); 126 return reader; 127 } 128 129 class ModuleFetchVisitor : ASTVisitor 130 { 131 alias visit = ASTVisitor.visit; 132 133 override void visit(const ModuleDeclaration decl) 134 { 135 outerFrom = decl.startLocation; 136 outerTo = decl.endLocation + 1; // + semicolon 137 138 raw = decl.moduleName.identifiers.map!(a => a.text).array; 139 moduleName = raw.join("."); 140 from = decl.moduleName.identifiers[0].index; 141 to = decl.moduleName.identifiers[$ - 1].index + decl.moduleName.identifiers[$ - 1].text.length; 142 } 143 144 const(string)[] raw; 145 string moduleName = ""; 146 Token fileName; 147 size_t from, to; 148 size_t outerFrom, outerTo; 149 } 150 151 class ModuleChangerVisitor : ASTVisitor 152 { 153 this(string file, string[] from, string[] to, bool renameSubmodules) 154 { 155 changes.file = file; 156 this.from = from; 157 this.to = to; 158 this.renameSubmodules = renameSubmodules; 159 } 160 161 alias visit = ASTVisitor.visit; 162 163 override void visit(const ModuleDeclaration decl) 164 { 165 auto mod = decl.moduleName.identifiers.map!(a => a.text).array; 166 auto orig = mod; 167 if (mod.startsWith(from) && renameSubmodules) 168 mod = to ~ mod[from.length .. $]; 169 else if (mod == from) 170 mod = to; 171 if (mod != orig) 172 { 173 foundModule = true; 174 changes.replacements ~= CodeReplacement([decl.moduleName.identifiers[0].index, 175 decl.moduleName.identifiers[$ - 1].index + decl.moduleName.identifiers[$ - 1].text.length], 176 mod.join('.')); 177 } 178 } 179 180 override void visit(const SingleImport imp) 181 { 182 auto mod = imp.identifierChain.identifiers.map!(a => a.text).array; 183 auto orig = mod; 184 if (mod.startsWith(from) && renameSubmodules) 185 mod = to ~ mod[from.length .. $]; 186 else if (mod == from) 187 mod = to; 188 if (mod != orig) 189 { 190 changes.replacements ~= CodeReplacement([imp.identifierChain.identifiers[0].index, 191 imp.identifierChain.identifiers[$ - 1].index 192 + imp.identifierChain.identifiers[$ - 1].text.length], mod.join('.')); 193 } 194 } 195 196 override void visit(const ImportDeclaration decl) 197 { 198 if (decl) 199 { 200 return decl.accept(this); 201 } 202 } 203 204 override void visit(const BlockStatement content) 205 { 206 if (content) 207 { 208 return content.accept(this); 209 } 210 } 211 212 string[] from, to; 213 FileChanges changes; 214 bool renameSubmodules, foundModule; 215 } 216 217 void doNothing(string, size_t, size_t, string, bool) 218 { 219 } 220 221 unittest 222 { 223 auto workspace = makeTemporaryTestingWorkspace; 224 workspace.createDir("source/newmod"); 225 workspace.createDir("unregistered/source"); 226 workspace.writeFile("source/newmod/color.d", "module oldmod.color;void foo(){}"); 227 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(){}"); 228 workspace.writeFile("source/newmod/display.d", "module newmod.displaf;"); 229 workspace.writeFile("source/newmod/input.d", ""); 230 workspace.writeFile("source/newmod/package.d", ""); 231 workspace.writeFile("unregistered/source/package.d", ""); 232 workspace.writeFile("unregistered/source/app.d", ""); 233 234 importPathProvider = () => ["source"]; 235 236 start(workspace.directory); 237 238 FileChanges[] changes = rename("oldmod", "newmod").sort!"a.file < b.file".array; 239 240 assert(changes.length == 2); 241 assert(changes[0].file.endsWith("color.d")); 242 assert(changes[1].file.endsWith("render.d")); 243 244 assert(changes[0].replacements == [CodeReplacement([7, 19], "newmod.color")]); 245 assert(changes[1].replacements == [CodeReplacement([7, 20], "newmod.render"), 246 CodeReplacement([38, 50], "newmod.color"), CodeReplacement([58, 77], 247 "newmod.color.oldmod"), CodeReplacement([94, 102], "newmod.a")]); 248 249 foreach (change; changes) 250 { 251 string code = readText(change.file); 252 foreach_reverse (op; change.replacements) 253 code = op.apply(code); 254 std.file.write(change.file, code); 255 } 256 257 auto nrm = normalizeModules(workspace.getPath("source/newmod/input.d"), ""); 258 assert(nrm == [CodeReplacement([0, 0], "module newmod.input;")]); 259 260 nrm = normalizeModules(workspace.getPath("source/newmod/package.d"), ""); 261 assert(nrm == [CodeReplacement([0, 0], "module newmod;")]); 262 263 nrm = normalizeModules(workspace.getPath("source/newmod/display.d"), "module oldmod.displaf;"); 264 assert(nrm == [CodeReplacement([0, 22], "module newmod.display;")]); 265 266 nrm = normalizeModules(workspace.getPath("unregistered/source/app.d"), ""); 267 assert(nrm == [CodeReplacement([0, 0], "module app;")]); 268 269 nrm = normalizeModules(workspace.getPath("unregistered/source/package.d"), ""); 270 assert(nrm == []); 271 272 stop(); 273 }