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 }