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 = fetchModule(file, 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 of a D code
106 	const(string)[] getModule(string code)
107 	{
108 		return fetchModule("", code).raw;
109 	}
110 
111 private:
112 	RollbackAllocator rba;
113 	LexerConfig config;
114 
115 	ModuleFetchVisitor fetchModule(string file, string code)
116 	{
117 		auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache);
118 		auto parsed = parseModule(tokens, file, &rba, (&doNothing).toDelegate);
119 		auto reader = new ModuleFetchVisitor();
120 		reader.visit(parsed);
121 		return reader;
122 	}
123 }
124 
125 private:
126 
127 class ModuleFetchVisitor : ASTVisitor
128 {
129 	alias visit = ASTVisitor.visit;
130 
131 	override void visit(const ModuleDeclaration decl)
132 	{
133 		outerFrom = decl.startLocation;
134 		outerTo = decl.endLocation + 1; // + semicolon
135 
136 		raw = decl.moduleName.identifiers.map!(a => a.text).array;
137 		moduleName = raw.join(".");
138 		from = decl.moduleName.identifiers[0].index;
139 		to = decl.moduleName.identifiers[$ - 1].index + decl.moduleName.identifiers[$ - 1].text.length;
140 	}
141 
142 	const(string)[] raw;
143 	string moduleName = "";
144 	Token fileName;
145 	size_t from, to;
146 	size_t outerFrom, outerTo;
147 }
148 
149 class ModuleChangerVisitor : ASTVisitor
150 {
151 	this(string file, string[] from, string[] to, bool renameSubmodules)
152 	{
153 		changes.file = file;
154 		this.from = from;
155 		this.to = to;
156 		this.renameSubmodules = renameSubmodules;
157 	}
158 
159 	alias visit = ASTVisitor.visit;
160 
161 	override void visit(const ModuleDeclaration decl)
162 	{
163 		auto mod = decl.moduleName.identifiers.map!(a => a.text).array;
164 		auto orig = mod;
165 		if (mod.startsWith(from) && renameSubmodules)
166 			mod = to ~ mod[from.length .. $];
167 		else if (mod == from)
168 			mod = to;
169 		if (mod != orig)
170 		{
171 			foundModule = true;
172 			changes.replacements ~= CodeReplacement([decl.moduleName.identifiers[0].index,
173 					decl.moduleName.identifiers[$ - 1].index + decl.moduleName.identifiers[$ - 1].text.length],
174 					mod.join('.'));
175 		}
176 	}
177 
178 	override void visit(const SingleImport imp)
179 	{
180 		auto mod = imp.identifierChain.identifiers.map!(a => a.text).array;
181 		auto orig = mod;
182 		if (mod.startsWith(from) && renameSubmodules)
183 			mod = to ~ mod[from.length .. $];
184 		else if (mod == from)
185 			mod = to;
186 		if (mod != orig)
187 		{
188 			changes.replacements ~= CodeReplacement([imp.identifierChain.identifiers[0].index,
189 					imp.identifierChain.identifiers[$ - 1].index
190 					+ imp.identifierChain.identifiers[$ - 1].text.length], mod.join('.'));
191 		}
192 	}
193 
194 	override void visit(const ImportDeclaration decl)
195 	{
196 		if (decl)
197 		{
198 			return decl.accept(this);
199 		}
200 	}
201 
202 	override void visit(const BlockStatement content)
203 	{
204 		if (content)
205 		{
206 			return content.accept(this);
207 		}
208 	}
209 
210 	string[] from, to;
211 	FileChanges changes;
212 	bool renameSubmodules, foundModule;
213 }
214 
215 void doNothing(string, size_t, size_t, string, bool)
216 {
217 }
218 
219 /*unittest
220 {
221 	auto workspace = makeTemporaryTestingWorkspace;
222 	workspace.createDir("source/newmod");
223 	workspace.createDir("unregistered/source");
224 	workspace.writeFile("source/newmod/color.d", "module oldmod.color;void foo(){}");
225 	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(){}");
226 	workspace.writeFile("source/newmod/display.d", "module newmod.displaf;");
227 	workspace.writeFile("source/newmod/input.d", "");
228 	workspace.writeFile("source/newmod/package.d", "");
229 	workspace.writeFile("unregistered/source/package.d", "");
230 	workspace.writeFile("unregistered/source/app.d", "");
231 
232 	importPathProvider = () => ["source"];
233 
234 	start(workspace.directory);
235 
236 	FileChanges[] changes = rename("oldmod", "newmod").sort!"a.file < b.file".array;
237 
238 	assert(changes.length == 2);
239 	assert(changes[0].file.endsWith("color.d"));
240 	assert(changes[1].file.endsWith("render.d"));
241 
242 	assert(changes[0].replacements == [CodeReplacement([7, 19], "newmod.color")]);
243 	assert(changes[1].replacements == [CodeReplacement([7, 20], "newmod.render"),
244 			CodeReplacement([38, 50], "newmod.color"), CodeReplacement([58, 77],
245 				"newmod.color.oldmod"), CodeReplacement([94, 102], "newmod.a")]);
246 
247 	foreach (change; changes)
248 	{
249 		string code = readText(change.file);
250 		foreach_reverse (op; change.replacements)
251 			code = op.apply(code);
252 		std.file.write(change.file, code);
253 	}
254 
255 	auto nrm = normalizeModules(workspace.getPath("source/newmod/input.d"), "");
256 	assert(nrm == [CodeReplacement([0, 0], "module newmod.input;")]);
257 
258 	nrm = normalizeModules(workspace.getPath("source/newmod/package.d"), "");
259 	assert(nrm == [CodeReplacement([0, 0], "module newmod;")]);
260 
261 	nrm = normalizeModules(workspace.getPath("source/newmod/display.d"), "module oldmod.displaf;");
262 	assert(nrm == [CodeReplacement([0, 22], "module newmod.display;")]);
263 
264 	nrm = normalizeModules(workspace.getPath("unregistered/source/app.d"), "");
265 	assert(nrm == [CodeReplacement([0, 0], "module app;")]);
266 
267 	nrm = normalizeModules(workspace.getPath("unregistered/source/package.d"), "");
268 	assert(nrm == []);
269 
270 	stop();
271 }*/