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