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 		FileChanges[] changes;
38 		bool foundModule = false;
39 		auto from = mod.split('.');
40 		auto to = rename.split('.');
41 		foreach (file; dirEntries(instance.cwd, SpanMode.depth))
42 		{
43 			if (file.extension != ".d")
44 				continue;
45 			string code = readText(file);
46 			auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache);
47 			auto parsed = parseModule(tokens, file, &rba);
48 			auto reader = new ModuleChangerVisitor(file, from, to, renameSubmodules);
49 			reader.visit(parsed);
50 			if (reader.changes.replacements.length)
51 				changes ~= reader.changes;
52 			if (reader.foundModule)
53 				foundModule = true;
54 		}
55 		if (!foundModule)
56 			return [];
57 		return changes;
58 	}
59 
60 	/// Renames/adds/removes a module from a file to match the majority of files in the folder.
61 	/// Params:
62 	/// 	file: File path to the file to normalize
63 	/// 	code: Current code inside the text buffer
64 	CodeReplacement[] normalizeModules(scope const(char)[] file, scope const(char)[] code)
65 	{
66 		if (!refInstance)
67 			throw new Exception("moduleman.normalizeModules requires to be instanced");
68 
69 		int[string] modulePrefixes;
70 		modulePrefixes[""] = 0;
71 		auto modName = file.replace("\\", "/").stripExtension;
72 		if (modName.baseName == "package")
73 			modName = modName.dirName;
74 		if (modName.startsWith(instance.cwd.replace("\\", "/")))
75 			modName = modName[instance.cwd.length .. $];
76 		modName = modName.stripLeft('/');
77 		auto longest = modName;
78 		foreach (imp; importPaths)
79 		{
80 			imp = imp.replace("\\", "/");
81 			if (imp.startsWith(instance.cwd.replace("\\", "/")))
82 				imp = imp[instance.cwd.length .. $];
83 			imp = imp.stripLeft('/');
84 			if (longest.startsWith(imp))
85 			{
86 				auto shortened = longest[imp.length .. $];
87 				if (shortened.length < modName.length)
88 					modName = shortened;
89 			}
90 		}
91 		auto sourcePos = (modName ~ '/').indexOf("/source/");
92 		if (sourcePos != -1)
93 			modName = modName[sourcePos + "/source".length .. $];
94 		modName = modName.stripLeft('/').replace("/", ".");
95 		if (!modName.length)
96 			return [];
97 		auto existing = describeModule(code);
98 		if (modName == existing.moduleName)
99 		{
100 			return [];
101 		}
102 		else
103 		{
104 			if (modName == "")
105 				return [CodeReplacement([existing.outerFrom, existing.outerTo], "")];
106 			else
107 				return [
108 					CodeReplacement([existing.outerFrom, existing.outerTo], text("module ",
109 							modName, (existing.outerTo == existing.outerFrom ? ";\n\n" : ";")))
110 				];
111 		}
112 	}
113 
114 	/// Returns the module name parts of a D code
115 	const(string)[] getModule(scope const(char)[] code)
116 	{
117 		return describeModule(code).raw;
118 	}
119 
120 	/// Returns the normalized module name as string of a D code
121 	string moduleName(scope const(char)[] code)
122 	{
123 		return describeModule(code).moduleName;
124 	}
125 
126 	///
127 	FileModuleInfo describeModule(scope const(char)[] code)
128 	{
129 		auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache);
130 		ptrdiff_t start = -1;
131 		size_t from, to;
132 		size_t outerFrom, outerTo;
133 
134 		foreach (i, Token t; tokens)
135 		{
136 			if (t.type == tok!"module")
137 			{
138 				start = i;
139 				outerFrom = t.index;
140 				break;
141 			}
142 		}
143 
144 		if (start == -1)
145 			return FileModuleInfo.init;
146 
147 		const(string)[] raw;
148 		string moduleName;
149 		foreach (t; tokens[start + 1 .. $])
150 		{
151 			if (t.type == tok!";")
152 			{
153 				outerTo = t.index + 1;
154 				break;
155 			}
156 			if (t.type == tok!"identifier")
157 			{
158 				if (from == 0)
159 					from = t.index;
160 				moduleName ~= t.text;
161 				to = t.index + t.text.length;
162 				raw ~= t.text;
163 			}
164 			if (t.type == tok!".")
165 			{
166 				moduleName ~= ".";
167 			}
168 		}
169 		return FileModuleInfo(raw, moduleName, from, to, outerFrom, outerTo);
170 	}
171 
172 private:
173 	RollbackAllocator rba;
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 }