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