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 }