1 module workspaced.com.moduleman;
2 
3 import dparse.parser;
4 import dparse.lexer;
5 import dparse.ast;
6 import dparse.rollback_allocator;
7 
8 import std.algorithm;
9 import std.array;
10 import std.string;
11 import std.file;
12 import std.path;
13 
14 import workspaced.api;
15 
16 @component("moduleman") :
17 
18 /// Initializes the module & import parser. Call with `{"cmd": "load", "components": ["moduleman"]}`
19 @load void start(string projectRoot)
20 {
21 	config.stringBehavior = StringBehavior.source;
22 	cache = new StringCache(StringCache.defaultBucketCount);
23 	.projectRoot = projectRoot;
24 }
25 
26 /// Has no purpose right now.
27 @unload void stop()
28 {
29 }
30 
31 /// Renames a module to something else (only in the project root).
32 /// Params:
33 /// 	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.*`
34 /// Returns: all changes that need to happen to rename the module. If no module statement could be found this will return an empty array.
35 /// Call_With: `{"subcmd": "rename"}`
36 @arguments("subcmd", "rename")
37 FileChanges[] rename(string mod, string rename, bool renameSubmodules = true)
38 {
39 	FileChanges[] changes;
40 	bool foundModule = false;
41 	auto from = mod.split('.');
42 	auto to = rename.split('.');
43 	foreach (file; dirEntries(projectRoot, SpanMode.depth))
44 	{
45 		if (file.extension != ".d")
46 			continue;
47 		string code = readText(file);
48 		auto tokens = getTokensForParser(cast(ubyte[]) code, config, cache);
49 		auto parsed = parseModule(tokens, file, &rba, &doNothing);
50 		auto reader = new ModuleChangerVisitor(file, from, to, renameSubmodules);
51 		reader.visit(parsed);
52 		if (reader.changes.replacements.length)
53 			changes ~= reader.changes;
54 		if (reader.foundModule)
55 			foundModule = true;
56 	}
57 	if (!foundModule)
58 		return [];
59 	return changes;
60 }
61 
62 /// Renames/adds/removes a module from a file to match the majority of files in the folder.
63 /// Params:
64 /// 	file: File path to the file to normalize
65 /// 	code: Current code inside the text buffer
66 CodeReplacement[] normalizeModules(string file, string code)
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(projectRoot.replace("\\", "/")))
74 		modName = modName[projectRoot.length .. $];
75 	modName = modName.stripLeft('/');
76 	foreach (imp; importPathProvider())
77 	{
78 		imp = imp.replace("\\", "/");
79 		if (imp.startsWith(projectRoot.replace("\\", "/")))
80 			imp = imp[projectRoot.length .. $];
81 		imp = imp.stripLeft('/');
82 		if (modName.startsWith(imp))
83 		{
84 			modName = modName[imp.length .. $];
85 			break;
86 		}
87 	}
88 	auto sourcePos = (modName ~ '/').indexOf("/source/");
89 	if (sourcePos != -1)
90 		modName = modName[sourcePos + "/source".length .. $];
91 	modName = modName.stripLeft('/').replace("/", ".");
92 	if (!modName.length)
93 		return [];
94 	auto existing = fetchModule(file, code);
95 	if (modName == existing.moduleName)
96 	{
97 		return [];
98 	}
99 	else
100 	{
101 		if (modName == "")
102 			return [CodeReplacement([existing.outerFrom, existing.outerTo], "")];
103 		else
104 			return [CodeReplacement([existing.outerFrom, existing.outerTo], "module " ~ modName ~ ";")];
105 	}
106 }
107 
108 /// Returns the module name of a D code
109 const(string)[] getModule(string code)
110 {
111 	return fetchModule("", code).raw;
112 }
113 
114 private __gshared:
115 RollbackAllocator rba;
116 LexerConfig config;
117 StringCache* cache;
118 string projectRoot;
119 
120 ModuleFetchVisitor fetchModule(string file, string code)
121 {
122 	auto tokens = getTokensForParser(cast(ubyte[]) code, config, cache);
123 	auto parsed = parseModule(tokens, file, &rba, &doNothing);
124 	auto reader = new ModuleFetchVisitor();
125 	reader.visit(parsed);
126 	return reader;
127 }
128 
129 class ModuleFetchVisitor : ASTVisitor
130 {
131 	alias visit = ASTVisitor.visit;
132 
133 	override void visit(const ModuleDeclaration decl)
134 	{
135 		outerFrom = decl.startLocation;
136 		outerTo = decl.endLocation + 1; // + semicolon
137 
138 		raw = decl.moduleName.identifiers.map!(a => a.text).array;
139 		moduleName = raw.join(".");
140 		from = decl.moduleName.identifiers[0].index;
141 		to = decl.moduleName.identifiers[$ - 1].index + decl.moduleName.identifiers[$ - 1].text.length;
142 	}
143 
144 	const(string)[] raw;
145 	string moduleName = "";
146 	Token fileName;
147 	size_t from, to;
148 	size_t outerFrom, outerTo;
149 }
150 
151 class ModuleChangerVisitor : ASTVisitor
152 {
153 	this(string file, string[] from, string[] to, bool renameSubmodules)
154 	{
155 		changes.file = file;
156 		this.from = from;
157 		this.to = to;
158 		this.renameSubmodules = renameSubmodules;
159 	}
160 
161 	alias visit = ASTVisitor.visit;
162 
163 	override void visit(const ModuleDeclaration decl)
164 	{
165 		auto mod = decl.moduleName.identifiers.map!(a => a.text).array;
166 		auto orig = mod;
167 		if (mod.startsWith(from) && renameSubmodules)
168 			mod = to ~ mod[from.length .. $];
169 		else if (mod == from)
170 			mod = to;
171 		if (mod != orig)
172 		{
173 			foundModule = true;
174 			changes.replacements ~= CodeReplacement([decl.moduleName.identifiers[0].index,
175 					decl.moduleName.identifiers[$ - 1].index + decl.moduleName.identifiers[$ - 1].text.length],
176 					mod.join('.'));
177 		}
178 	}
179 
180 	override void visit(const SingleImport imp)
181 	{
182 		auto mod = imp.identifierChain.identifiers.map!(a => a.text).array;
183 		auto orig = mod;
184 		if (mod.startsWith(from) && renameSubmodules)
185 			mod = to ~ mod[from.length .. $];
186 		else if (mod == from)
187 			mod = to;
188 		if (mod != orig)
189 		{
190 			changes.replacements ~= CodeReplacement([imp.identifierChain.identifiers[0].index,
191 					imp.identifierChain.identifiers[$ - 1].index
192 					+ imp.identifierChain.identifiers[$ - 1].text.length], mod.join('.'));
193 		}
194 	}
195 
196 	override void visit(const ImportDeclaration decl)
197 	{
198 		if (decl)
199 		{
200 			return decl.accept(this);
201 		}
202 	}
203 
204 	override void visit(const BlockStatement content)
205 	{
206 		if (content)
207 		{
208 			return content.accept(this);
209 		}
210 	}
211 
212 	string[] from, to;
213 	FileChanges changes;
214 	bool renameSubmodules, foundModule;
215 }
216 
217 void doNothing(string, size_t, size_t, string, bool)
218 {
219 }
220 
221 unittest
222 {
223 	auto workspace = makeTemporaryTestingWorkspace;
224 	workspace.createDir("source/newmod");
225 	workspace.createDir("unregistered/source");
226 	workspace.writeFile("source/newmod/color.d", "module oldmod.color;void foo(){}");
227 	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(){}");
228 	workspace.writeFile("source/newmod/display.d", "module newmod.displaf;");
229 	workspace.writeFile("source/newmod/input.d", "");
230 	workspace.writeFile("source/newmod/package.d", "");
231 	workspace.writeFile("unregistered/source/package.d", "");
232 	workspace.writeFile("unregistered/source/app.d", "");
233 
234 	importPathProvider = () => ["source"];
235 
236 	start(workspace.directory);
237 
238 	FileChanges[] changes = rename("oldmod", "newmod").sort!"a.file < b.file".array;
239 
240 	assert(changes.length == 2);
241 	assert(changes[0].file.endsWith("color.d"));
242 	assert(changes[1].file.endsWith("render.d"));
243 
244 	assert(changes[0].replacements == [CodeReplacement([7, 19], "newmod.color")]);
245 	assert(changes[1].replacements == [CodeReplacement([7, 20], "newmod.render"),
246 			CodeReplacement([38, 50], "newmod.color"), CodeReplacement([58, 77],
247 				"newmod.color.oldmod"), CodeReplacement([94, 102], "newmod.a")]);
248 
249 	foreach (change; changes)
250 	{
251 		string code = readText(change.file);
252 		foreach_reverse (op; change.replacements)
253 			code = op.apply(code);
254 		std.file.write(change.file, code);
255 	}
256 
257 	auto nrm = normalizeModules(workspace.getPath("source/newmod/input.d"), "");
258 	assert(nrm == [CodeReplacement([0, 0], "module newmod.input;")]);
259 
260 	nrm = normalizeModules(workspace.getPath("source/newmod/package.d"), "");
261 	assert(nrm == [CodeReplacement([0, 0], "module newmod;")]);
262 
263 	nrm = normalizeModules(workspace.getPath("source/newmod/display.d"), "module oldmod.displaf;");
264 	assert(nrm == [CodeReplacement([0, 22], "module newmod.display;")]);
265 
266 	nrm = normalizeModules(workspace.getPath("unregistered/source/app.d"), "");
267 	assert(nrm == [CodeReplacement([0, 0], "module app;")]);
268 
269 	nrm = normalizeModules(workspace.getPath("unregistered/source/package.d"), "");
270 	assert(nrm == []);
271 
272 	stop();
273 }