1 /**
2 	...
3 
4 	Copyright: © 2012 Matthias Dondorff
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Matthias Dondorff
7 */
8 module dub.internal.utils;
9 
10 import dub.internal.vibecompat.core.file;
11 import dub.internal.vibecompat.core.log;
12 import dub.internal.vibecompat.data.json;
13 import dub.internal.vibecompat.inet.url;
14 import dub.compilers.buildsettings : BuildSettings;
15 import dub.version_;
16 
17 import core.time : Duration;
18 import std.algorithm : canFind, startsWith;
19 import std.array : appender, array;
20 import std.conv : to;
21 import std.exception : enforce;
22 import std.file;
23 import std..string : format;
24 import std.process;
25 import std.traits : isIntegral;
26 version(DubUseCurl)
27 {
28 	import std.net.curl;
29 	public import std.net.curl : HTTPStatusException;
30 }
31 
32 
33 private NativePath[] temporary_files;
34 
35 NativePath getTempDir()
36 {
37 	return NativePath(std.file.tempDir());
38 }
39 
40 NativePath getTempFile(string prefix, string extension = null)
41 {
42 	import std.uuid : randomUUID;
43 	import std.array: replace;
44 
45 	string fileName = prefix ~ "-" ~ randomUUID.toString() ~ extension;
46 
47 	if (extension !is null && extension == ".d")
48 		fileName = fileName.replace("-", "_");
49 
50 	auto path = getTempDir() ~ fileName;
51 	temporary_files ~= path;
52 	return path;
53 }
54 
55 /**
56    Obtain a lock for a file at the given path. If the file cannot be locked
57    within the given duration, an exception is thrown.  The file will be created
58    if it does not yet exist. Deleting the file is not safe as another process
59    could create a new file with the same name.
60    The returned lock will get unlocked upon destruction.
61 
62    Params:
63      path = path to file that gets locked
64      timeout = duration after which locking failed
65    Returns:
66      The locked file or an Exception on timeout.
67 */
68 auto lockFile(string path, Duration timeout)
69 {
70 	import core.thread : Thread;
71 	import std.datetime, std.stdio : File;
72 	import std.algorithm : move;
73 
74 	// Just a wrapper to hide (and destruct) the locked File.
75 	static struct LockFile
76 	{
77 		// The Lock can't be unlinked as someone could try to lock an already
78 		// opened fd while a new file with the same name gets created.
79 		// Exclusive filesystem locks (O_EXCL, mkdir) could be deleted but
80 		// aren't automatically freed when a process terminates, see #1149.
81 		private File f;
82 	}
83 
84 	auto file = File(path, "w");
85 	auto t0 = Clock.currTime();
86 	auto dur = 1.msecs;
87 	while (true)
88 	{
89 		if (file.tryLock())
90 			return LockFile(move(file));
91 		enforce(Clock.currTime() - t0 < timeout, "Failed to lock '"~path~"'.");
92 		if (dur < 1024.msecs) // exponentially increase sleep time
93 			dur *= 2;
94 		Thread.sleep(dur);
95 	}
96 }
97 
98 static ~this()
99 {
100 	foreach (path; temporary_files)
101 	{
102 		auto spath = path.toNativeString();
103 		if (spath.exists)
104 			std.file.remove(spath);
105 	}
106 }
107 
108 bool isWritableDir(NativePath p, bool create_if_missing = false)
109 {
110 	import std.random;
111 	auto fname = p ~ format("__dub_write_test_%08X", uniform(0, uint.max));
112 	if (create_if_missing && !exists(p.toNativeString())) mkdirRecurse(p.toNativeString());
113 	try openFile(fname, FileMode.createTrunc).close();
114 	catch (Exception) return false;
115 	remove(fname.toNativeString());
116 	return true;
117 }
118 
119 Json jsonFromFile(NativePath file, bool silent_fail = false) {
120 	if( silent_fail && !existsFile(file) ) return Json.emptyObject;
121 	auto f = openFile(file.toNativeString(), FileMode.read);
122 	scope(exit) f.close();
123 	auto text = stripUTF8Bom(cast(string)f.readAll());
124 	return parseJsonString(text, file.toNativeString());
125 }
126 
127 /**
128 	Read package info file content from archive.
129 	File needs to be in root folder or in first
130 	sub folder.
131 
132 	Params:
133 		zip = path to archive file
134 		fileName = Package file name
135 	Returns:
136 		package file content.
137 */
138 string packageInfoFileFromZip(NativePath zip, out string fileName) {
139 	import std.zip : ZipArchive, ArchiveMember;
140 	import dub.package_ : packageInfoFiles;
141 
142 	auto f = openFile(zip, FileMode.read);
143 	ubyte[] b = new ubyte[cast(size_t)f.size];
144 	f.rawRead(b);
145 	f.close();
146 	auto archive = new ZipArchive(b);
147 	alias PSegment = typeof (NativePath.init.head);
148 	foreach (ArchiveMember am; archive.directory) {
149 		auto path = NativePath(am.name).bySegment.array;
150 		foreach (fil; packageInfoFiles) {
151 			if ((path.length == 1 && path[0] == fil.filename) || (path.length == 2 && path[$-1].name == fil.filename)) {
152 				fileName = fil.filename;
153 				return stripUTF8Bom(cast(string) archive.expand(archive.directory[am.name]));
154 			}
155 		}
156 	}
157 	throw new Exception("No package descriptor found");
158 }
159 
160 void writeJsonFile(NativePath path, Json json)
161 {
162 	auto f = openFile(path, FileMode.createTrunc);
163 	scope(exit) f.close();
164 	f.writePrettyJsonString(json);
165 }
166 
167 /// Performs a write->delete->rename sequence to atomically "overwrite" the destination file
168 void atomicWriteJsonFile(NativePath path, Json json)
169 {
170 	import std.random : uniform;
171 	auto tmppath = path.parentPath ~ format("%s.%s.tmp", path.head, uniform(0, int.max));
172 	auto f = openFile(tmppath, FileMode.createTrunc);
173 	scope (failure) {
174 		f.close();
175 		removeFile(tmppath);
176 	}
177 	f.writePrettyJsonString(json);
178 	f.close();
179 	if (existsFile(path)) removeFile(path);
180 	moveFile(tmppath, path);
181 }
182 
183 bool existsDirectory(NativePath path) {
184 	if( !existsFile(path) ) return false;
185 	auto fi = getFileInfo(path);
186 	return fi.isDirectory;
187 }
188 
189 void runCommand(string command, string[string] env = null, string workDir = null)
190 {
191 	runCommands((&command)[0 .. 1], env, workDir);
192 }
193 
194 void runCommands(in string[] commands, string[string] env = null, string workDir = null)
195 {
196 	import std.stdio : stdin, stdout, stderr, File;
197 
198 	version(Windows) enum nullFile = "NUL";
199 	else version(Posix) enum nullFile = "/dev/null";
200 	else static assert(0);
201 
202 	auto childStdout = stdout;
203 	auto childStderr = stderr;
204 	auto config = Config.retainStdout | Config.retainStderr;
205 
206 	// Disable child's stdout/stderr depending on LogLevel
207 	auto logLevel = getLogLevel();
208 	if(logLevel >= LogLevel.warn)
209 		childStdout = File(nullFile, "w");
210 	if(logLevel >= LogLevel.none)
211 		childStderr = File(nullFile, "w");
212 
213 	foreach(cmd; commands){
214 		logDiagnostic("Running %s", cmd);
215 		Pid pid;
216 		pid = spawnShell(cmd, stdin, childStdout, childStderr, env, config, workDir);
217 		auto exitcode = pid.wait();
218 		enforce(exitcode == 0, "Command failed with exit code "
219 			~ to!string(exitcode) ~ ": " ~ cmd);
220 	}
221 }
222 
223 version (Have_vibe_d_http)
224 	public import vibe.http.common : HTTPStatusException;
225 
226 /**
227 	Downloads a file from the specified URL.
228 
229 	Any redirects will be followed until the actual file resource is reached or if the redirection
230 	limit of 10 is reached. Note that only HTTP(S) is currently supported.
231 
232 	The download times out if a connection cannot be established within
233 	`timeout` ms, or if the average transfer rate drops below 10 bytes / s for
234 	more than `timeout` seconds.  Pass `0` as `timeout` to disable both timeout
235 	mechanisms.
236 
237 	Note: Timeouts are only implemented when curl is used (DubUseCurl).
238 */
239 void download(string url, string filename, uint timeout = 8)
240 {
241 	version(DubUseCurl) {
242 		auto conn = HTTP();
243 		setupHTTPClient(conn, timeout);
244 		logDebug("Storing %s...", url);
245 		std.net.curl.download(url, filename, conn);
246 		// workaround https://issues.dlang.org/show_bug.cgi?id=18318
247 		auto sl = conn.statusLine;
248 		logDebug("Download %s %s", url, sl);
249 		if (sl.code / 100 != 2)
250 			throw new HTTPStatusException(sl.code,
251 				"Downloading %s failed with %d (%s).".format(url, sl.code, sl.reason));
252 	} else version (Have_vibe_d_http) {
253 		import vibe.inet.urltransfer;
254 		vibe.inet.urltransfer.download(url, filename);
255 	} else assert(false);
256 }
257 /// ditto
258 void download(URL url, NativePath filename, uint timeout = 8)
259 {
260 	download(url.toString(), filename.toNativeString(), timeout);
261 }
262 /// ditto
263 ubyte[] download(string url, uint timeout = 8)
264 {
265 	version(DubUseCurl) {
266 		auto conn = HTTP();
267 		setupHTTPClient(conn, timeout);
268 		logDebug("Getting %s...", url);
269 		return cast(ubyte[])get(url, conn);
270 	} else version (Have_vibe_d_http) {
271 		import vibe.inet.urltransfer;
272 		import vibe.stream.operations;
273 		ubyte[] ret;
274 		vibe.inet.urltransfer.download(url, (scope input) { ret = input.readAll(); });
275 		return ret;
276 	} else assert(false);
277 }
278 /// ditto
279 ubyte[] download(URL url, uint timeout = 8)
280 {
281 	return download(url.toString(), timeout);
282 }
283 
284 /**
285 	Downloads a file from the specified URL with retry logic.
286 
287 	Downloads a file from the specified URL with up to n tries on failure
288 	Throws: `Exception` if the download failed or `HTTPStatusException` after the nth retry or
289 	on "unrecoverable failures" such as 404 not found
290 	Otherwise might throw anything else that `download` throws.
291 	See_Also: download
292 
293 	The download times out if a connection cannot be established within
294 	`timeout` ms, or if the average transfer rate drops below 10 bytes / s for
295 	more than `timeout` seconds.  Pass `0` as `timeout` to disable both timeout
296 	mechanisms.
297 
298 	Note: Timeouts are only implemented when curl is used (DubUseCurl).
299 **/
300 void retryDownload(URL url, NativePath filename, size_t retryCount = 3, uint timeout = 8)
301 {
302 	foreach(i; 0..retryCount) {
303 		version(DubUseCurl) {
304 			try {
305 				download(url, filename, timeout);
306 				return;
307 			}
308 			catch(HTTPStatusException e) {
309 				if (e.status == 404) throw e;
310 				else {
311 					logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
312 					if (i == retryCount - 1) throw e;
313 					else continue;
314 				}
315 			}
316 			catch(CurlException e) {
317 				logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
318 				continue;
319 			}
320 		}
321 		else
322 		{
323 			try {
324 				download(url, filename);
325 				return;
326 			}
327 			catch(HTTPStatusException e) {
328 				if (e.status == 404) throw e;
329 				else {
330 					logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
331 					if (i == retryCount - 1) throw e;
332 					else continue;
333 				}
334 			}
335 		}
336 	}
337 	throw new Exception("Failed to download %s".format(url));
338 }
339 
340 ///ditto
341 ubyte[] retryDownload(URL url, size_t retryCount = 3, uint timeout = 8)
342 {
343 	foreach(i; 0..retryCount) {
344 		version(DubUseCurl) {
345 			try {
346 				return download(url, timeout);
347 			}
348 			catch(HTTPStatusException e) {
349 				if (e.status == 404) throw e;
350 				else {
351 					logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
352 					if (i == retryCount - 1) throw e;
353 					else continue;
354 				}
355 			}
356 			catch(CurlException e) {
357 				logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
358 				continue;
359 			}
360 		}
361 		else
362 		{
363 			try {
364 				return download(url);
365 			}
366 			catch(HTTPStatusException e) {
367 				if (e.status == 404) throw e;
368 				else {
369 					logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
370 					if (i == retryCount - 1) throw e;
371 					else continue;
372 				}
373 			}
374 		}
375 	}
376 	throw new Exception("Failed to download %s".format(url));
377 }
378 
379 /// Returns the current DUB version in semantic version format
380 string getDUBVersion()
381 {
382 	import dub.version_;
383 	import std.array : split, join;
384 	// convert version string to valid SemVer format
385 	auto verstr = dubVersion;
386 	if (verstr.startsWith("v")) verstr = verstr[1 .. $];
387 	auto parts = verstr.split("-");
388 	if (parts.length >= 3) {
389 		// detect GIT commit suffix
390 		if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber())
391 			verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-");
392 	}
393 	return verstr;
394 }
395 
396 
397 /**
398 	Get current executable's path if running as DUB executable,
399 	or find a DUB executable if DUB is used as a library.
400 	For the latter, the following locations are checked in order:
401 	$(UL
402 		$(LI current working directory)
403 		$(LI same directory as `compilerBinary` (if supplied))
404 		$(LI all components of the `$PATH` variable)
405 	)
406 	Params:
407 		compilerBinary = optional path to a D compiler executable, used to locate DUB executable
408 	Returns:
409 		The path to a valid DUB executable
410 	Throws:
411 		an Exception if no valid DUB executable is found
412 */
413 public string getDUBExePath(in string compilerBinary=null)
414 {
415 	version(DubApplication) {
416 		import std.file : thisExePath;
417 		return thisExePath();
418 	}
419 	else {
420 		// this must be dub as a library
421 		import std.algorithm : filter, map, splitter;
422 		import std.array : array;
423 		import std.file : exists, getcwd;
424 		import std.path : chainPath, dirName;
425 		import std.range : chain, only, take;
426 		import std.process : environment;
427 
428 		version(Windows) {
429 			enum exeName = "dub.exe";
430 			enum pathSep = ';';
431 		}
432 		else {
433 			enum exeName = "dub";
434 			enum pathSep = ':';
435 		}
436 
437 		auto dubLocs = only(
438 			getcwd().chainPath(exeName),
439 			compilerBinary.dirName.chainPath(exeName),
440 		)
441 		.take(compilerBinary.length ? 2 : 1)
442 		.chain(
443 			environment.get("PATH", "")
444 				.splitter(pathSep)
445 				.map!(p => p.chainPath(exeName))
446 		)
447 		.filter!exists;
448 
449 		enforce(!dubLocs.empty, "Could not find DUB executable");
450 		return dubLocs.front.array;
451 	}
452 }
453 
454 
455 version(DubUseCurl) {
456 	void setupHTTPClient(ref HTTP conn, uint timeout)
457 	{
458 		static if( is(typeof(&conn.verifyPeer)) )
459 			conn.verifyPeer = false;
460 
461 		auto proxy = environment.get("http_proxy", null);
462 		if (proxy.length) conn.proxy = proxy;
463 
464 		auto noProxy = environment.get("no_proxy", null);
465 		if (noProxy.length) conn.handle.set(CurlOption.noproxy, noProxy);
466 
467 		conn.handle.set(CurlOption.encoding, "");
468 		if (timeout) {
469 			// connection (TLS+TCP) times out after 8s
470 			conn.handle.set(CurlOption.connecttimeout, timeout);
471 			// transfers time out after 8s below 10 byte/s
472 			conn.handle.set(CurlOption.low_speed_limit, 10);
473 			conn.handle.set(CurlOption.low_speed_time, timeout);
474 		}
475 
476 		conn.addRequestHeader("User-Agent", "dub/"~getDUBVersion()~" (std.net.curl; +https://github.com/rejectedsoftware/dub)");
477 
478 		enum CURL_NETRC_OPTIONAL = 1;
479 		conn.handle.set(CurlOption.netrc, CURL_NETRC_OPTIONAL);
480 	}
481 }
482 
483 string stripUTF8Bom(string str)
484 {
485 	if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] )
486 		return str[3 ..$];
487 	return str;
488 }
489 
490 private bool isNumber(string str) {
491 	foreach (ch; str)
492 		switch (ch) {
493 			case '0': .. case '9': break;
494 			default: return false;
495 		}
496 	return true;
497 }
498 
499 private bool isHexNumber(string str) {
500 	foreach (ch; str)
501 		switch (ch) {
502 			case '0': .. case '9': break;
503 			case 'a': .. case 'f': break;
504 			case 'A': .. case 'F': break;
505 			default: return false;
506 		}
507 	return true;
508 }
509 
510 /**
511 	Get the closest match of $(D input) in the $(D array), where $(D distance)
512 	is the maximum levenshtein distance allowed between the compared strings.
513 	Returns $(D null) if no closest match is found.
514 */
515 string getClosestMatch(string[] array, string input, size_t distance)
516 {
517 	import std.algorithm : countUntil, map, levenshteinDistance;
518 	import std.uni : toUpper;
519 
520 	auto distMap = array.map!(elem =>
521 		levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input));
522 	auto idx = distMap.countUntil!(a => a <= distance);
523 	return (idx == -1) ? null : array[idx];
524 }
525 
526 /**
527 	Searches for close matches to input in range. R must be a range of strings
528 	Note: Sorts the strings range. Use std.range.indexed to avoid this...
529   */
530 auto fuzzySearch(R)(R strings, string input){
531 	import std.algorithm : levenshteinDistance, schwartzSort, partition3;
532 	import std.traits : isSomeString;
533 	import std.range : ElementType;
534 
535 	static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang");
536 	immutable threshold = input.length / 4;
537 	return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1]
538 			.schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper));
539 }
540 
541 /**
542 	If T is a bitfield-style enum, this function returns a string range
543 	listing the names of all members included in the given value.
544 
545 	Example:
546 	---------
547 	enum Bits {
548 		none = 0,
549 		a = 1<<0,
550 		b = 1<<1,
551 		c = 1<<2,
552 		a_c = a | c,
553 	}
554 
555 	assert( bitFieldNames(Bits.none).equals(["none"]) );
556 	assert( bitFieldNames(Bits.a).equals(["a"]) );
557 	assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) );
558 	---------
559   */
560 auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T)
561 {
562 	import std.algorithm : filter, map;
563 	import std.conv : to;
564 	import std.traits : EnumMembers;
565 
566 	return [ EnumMembers!(T) ]
567 		.filter!(member => member==0? value==0 : (value & member) == member)
568 		.map!(member => to!string(member));
569 }
570 
571 
572 bool isIdentChar(dchar ch)
573 {
574 	import std.ascii : isAlphaNum;
575 	return isAlphaNum(ch) || ch == '_';
576 }
577 
578 string stripDlangSpecialChars(string s)
579 {
580 	import std.array : appender;
581 	auto ret = appender!string();
582 	foreach(ch; s)
583 		ret.put(isIdentChar(ch) ? ch : '_');
584 	return ret.data;
585 }
586 
587 string determineModuleName(BuildSettings settings, NativePath file, NativePath base_path)
588 {
589 	import std.algorithm : map;
590 	import std.array : array;
591 	import std.range : walkLength;
592 
593 	assert(base_path.absolute);
594 	if (!file.absolute) file = base_path ~ file;
595 
596 	size_t path_skip = 0;
597 	foreach (ipath; settings.importPaths.map!(p => NativePath(p))) {
598 		if (!ipath.absolute) ipath = base_path ~ ipath;
599 		assert(!ipath.empty);
600 		if (file.startsWith(ipath) && ipath.bySegment.walkLength > path_skip)
601 			path_skip = ipath.bySegment.walkLength;
602 	}
603 
604 	enforce(path_skip > 0,
605 		format("Source file '%s' not found in any import path.", file.toNativeString()));
606 
607 	auto mpath = file.bySegment.array[path_skip .. $];
608 	auto ret = appender!string;
609 
610 	//search for module keyword in file
611 	string moduleName = getModuleNameFromFile(file.to!string);
612 
613 	if(moduleName.length) return moduleName;
614 
615 	//create module name from path
616 	foreach (i; 0 .. mpath.length) {
617 		import std.path;
618 		auto p = mpath[i].name;
619 		if (p == "package.d") break;
620 		if (i > 0) ret ~= ".";
621 		if (i+1 < mpath.length) ret ~= p;
622 		else ret ~= p.baseName(".d");
623 	}
624 
625 	return ret.data;
626 }
627 
628 /**
629  * Search for module keyword in D Code
630  */
631 string getModuleNameFromContent(string content) {
632 	import std.regex;
633 	import std..string;
634 
635 	content = content.strip;
636 	if (!content.length) return null;
637 
638 	static bool regex_initialized = false;
639 	static Regex!char comments_pattern, module_pattern;
640 
641 	if (!regex_initialized) {
642 		comments_pattern = regex(`//[^\r\n]*\r?\n?|/\*.*?\*/|/\+.*\+/`, "gs");
643 		module_pattern = regex(`module\s+([\w\.]+)\s*;`, "g");
644 		regex_initialized = true;
645 	}
646 
647 	content = replaceAll(content, comments_pattern, " ");
648 	auto result = matchFirst(content, module_pattern);
649 
650 	if (!result.empty) return result[1];
651 
652 	return null;
653 }
654 
655 unittest {
656 	assert(getModuleNameFromContent("") == "");
657 	assert(getModuleNameFromContent("module myPackage.myModule;") == "myPackage.myModule");
658 	assert(getModuleNameFromContent("module \t\n myPackage.myModule \t\r\n;") == "myPackage.myModule");
659 	assert(getModuleNameFromContent("// foo\nmodule bar;") == "bar");
660 	assert(getModuleNameFromContent("/*\nfoo\n*/\nmodule bar;") == "bar");
661 	assert(getModuleNameFromContent("/+\nfoo\n+/\nmodule bar;") == "bar");
662 	assert(getModuleNameFromContent("/***\nfoo\n***/\nmodule bar;") == "bar");
663 	assert(getModuleNameFromContent("/+++\nfoo\n+++/\nmodule bar;") == "bar");
664 	assert(getModuleNameFromContent("// module foo;\nmodule bar;") == "bar");
665 	assert(getModuleNameFromContent("/* module foo; */\nmodule bar;") == "bar");
666 	assert(getModuleNameFromContent("/+ module foo; +/\nmodule bar;") == "bar");
667 	assert(getModuleNameFromContent("/+ /+ module foo; +/ +/\nmodule bar;") == "bar");
668 	assert(getModuleNameFromContent("// module foo;\nmodule bar; // module foo;") == "bar");
669 	assert(getModuleNameFromContent("// module foo;\nmodule// module foo;\nbar//module foo;\n;// module foo;") == "bar");
670 	assert(getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;") == "bar", getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;"));
671 	assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar;") == "bar");
672 	//assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar/++/;") == "bar"); // nested comments require a context-free parser!
673 	assert(getModuleNameFromContent("/*\nmodule sometest;\n*/\n\nmodule fakemath;\n") == "fakemath");
674 }
675 
676 /**
677  * Search for module keyword in file
678  */
679 string getModuleNameFromFile(string filePath) {
680 	string fileContent = filePath.readText;
681 
682 	logDiagnostic("Get module name from path: " ~ filePath);
683 	return getModuleNameFromContent(fileContent);
684 }