1 /**
2  * This file is part of DCD, a development tool for the D programming language.
3  * Copyright (C) 2014 Brian Schott
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 module dsymbol.modulecache;
20 
21 import containers.dynamicarray;
22 import containers.hashset;
23 import containers.ttree;
24 import containers.unrolledlist;
25 import dsymbol.conversion;
26 import dsymbol.conversion.first;
27 import dsymbol.conversion.second;
28 import dsymbol.cache_entry;
29 import dsymbol.scope_;
30 import dsymbol.semantic;
31 import dsymbol.symbol;
32 import dsymbol.string_interning;
33 import dsymbol.deferred;
34 import std.algorithm;
35 import stdx.allocator;
36 import stdx.allocator.building_blocks.allocator_list;
37 import stdx.allocator.building_blocks.region;
38 import stdx.allocator.building_blocks.null_allocator;
39 import stdx.allocator.mallocator : Mallocator;
40 import stdx.allocator.gc_allocator : GCAllocator;
41 import std.conv;
42 import dparse.ast;
43 import std.datetime;
44 import dparse.lexer;
45 import dparse.parser;
46 import std.experimental.logger;
47 import std.file;
48 import std.experimental.lexer;
49 import std.path;
50 
51 alias ASTAllocator = CAllocatorImpl!(AllocatorList!(
52 	n => Region!Mallocator(1024 * 128), Mallocator));
53 
54 /**
55  * Returns: true if a file exists at the given path.
56  */
57 bool existanceCheck(A)(A path)
58 {
59 	if (path.exists())
60 		return true;
61 	warning("Cannot cache modules in ", path, " because it does not exist");
62 	return false;
63 }
64 
65 alias DeferredSymbolsAllocator = GCAllocator; // NOTE using `Mallocator` here fails when analysing Phobos as `free(): invalid pointer`
66 
67 /**
68  * Caches pre-parsed module information.
69  */
70 struct ModuleCache
71 {
72 	/// No copying.
73 	@disable this(this);
74 
75 	@disable this();
76 
77 	this(IAllocator symbolAllocator)
78 	{
79 		this.symbolAllocator = symbolAllocator;
80 	}
81 
82 	~this()
83 	{
84 		clear();
85 	}
86 
87 	/**
88 	 * Adds the given paths to the list of directories checked for imports.
89 	 * Performs duplicate checking, so multiple instances of the same path will
90 	 * not be present.
91 	 */
92 	void addImportPaths(const string[] paths)
93 	{
94 		import std.path : baseName;
95 		import std.array : array;
96 
97 		auto newPaths = paths
98 			.map!(a => absolutePath(expandTilde(a)))
99 			.filter!(a => existanceCheck(a) && !importPaths[].canFind!(b => b.path == a))
100 			.map!(a => ImportPath(istring(a)))
101 			.array;
102 		importPaths.insert(newPaths);
103 	}
104 
105 	/**
106 	 * Removes the given paths from the list of directories checked for
107 	 * imports. Corresponding cache entries are removed.
108 	 */
109 	void removeImportPaths(const string[] paths)
110 	{
111 		foreach (path; paths[])
112 		{
113 			if (!importPaths[].canFind!(a => a.path == path))
114 			{
115 				warning("Cannot remove ", path, " because it is not imported");
116 				continue;
117 			}
118 
119 			foreach (ref importPath; importPaths[].filter!(a => a.path == path))
120 				importPaths.remove(importPath);
121 
122 			foreach (cacheEntry; cache[])
123 			{
124 				if (cacheEntry.path.data.startsWith(path))
125 				{
126 					foreach (deferredSymbol; deferredSymbols[].find!(d => d.symbol.symbolFile.data.startsWith(cacheEntry.path.data)))
127 					{
128 						deferredSymbols.remove(deferredSymbol);
129 						DeferredSymbolsAllocator.instance.dispose(deferredSymbol);
130 					}
131 
132 					cache.remove(cacheEntry);
133 					CacheAllocator.instance.dispose(cacheEntry);
134 				}
135 			}
136 		}
137 	}
138 
139 	/**
140 	 * Clears the cache from all import paths
141 	 */
142 	void clear()
143 	{
144 		foreach (entry; cache[])
145 			CacheAllocator.instance.dispose(entry);
146 		foreach (symbol; deferredSymbols[])
147 			DeferredSymbolsAllocator.instance.dispose(symbol);
148 
149 		// TODO: This call to deallocateAll is a workaround for issues of
150 		// CAllocatorImpl and GCAllocator not interacting well.
151 		symbolAllocator.deallocateAll();
152 		cache.clear();
153 		deferredSymbols.clear();
154 		importPaths.clear();
155 	}
156 
157 	/**
158 	 * Caches the module at the given location
159 	 */
160 	DSymbol* cacheModule(string location)
161 	{
162 		import std.stdio : File;
163 
164 		assert (location !is null);
165 
166 		const cachedLocation = istring(location);
167 
168 		if (recursionGuard.contains(&cachedLocation.data[0]))
169 			return null;
170 
171 		if (!needsReparsing(cachedLocation))
172 			return getEntryFor(cachedLocation).symbol;
173 
174 		recursionGuard.insert(&cachedLocation.data[0]);
175 
176 		File f = File(cachedLocation);
177 		immutable fileSize = cast(size_t) f.size;
178 		if (fileSize == 0)
179 			return null;
180 
181 		const(Token)[] tokens;
182 		auto parseStringCache = StringCache(fileSize.optimalBucketCount);
183 		{
184 			ubyte[] source = cast(ubyte[]) Mallocator.instance.allocate(fileSize);
185 			scope (exit) Mallocator.instance.deallocate(source);
186 			f.rawRead(source);
187 			LexerConfig config;
188 			config.fileName = cachedLocation;
189 
190 			// The first three bytes are sliced off here if the file starts with a
191 			// Unicode byte order mark. The lexer/parser don't handle them.
192 			tokens = getTokensForParser(
193 				(source.length >= 3 && source[0 .. 3] == "\xef\xbb\xbf"c)
194 				? source[3 .. $] : source,
195 				config, &parseStringCache);
196 		}
197 
198 		CacheEntry* newEntry = CacheAllocator.instance.make!CacheEntry();
199 
200 		scope semanticAllocator = new ASTAllocator();
201 		import dparse.rollback_allocator:RollbackAllocator;
202 		RollbackAllocator parseAllocator;
203 		Module m = parseModuleSimple(tokens[], cachedLocation, &parseAllocator);
204 
205 		assert (symbolAllocator);
206 		scope first = new FirstPass(m, cachedLocation, symbolAllocator,
207 			semanticAllocator, false, &this, newEntry);
208 		first.run();
209 
210 		secondPass(first.rootSymbol, first.moduleScope, this);
211 
212 		typeid(Scope).destroy(first.moduleScope);
213 		symbolsAllocated += first.symbolsAllocated;
214 
215 		SysTime access;
216 		SysTime modification;
217 		getTimes(cachedLocation.data, access, modification);
218 
219 		newEntry.symbol = first.rootSymbol.acSymbol;
220 		newEntry.modificationTime = modification;
221 		newEntry.path = cachedLocation;
222 
223 		CacheEntry* oldEntry = getEntryFor(cachedLocation);
224 		if (oldEntry !is null)
225 		{
226 			// Generate update mapping from the old symbol to the new one
227 			UpdatePairCollection updatePairs;
228 			generateUpdatePairs(oldEntry.symbol, newEntry.symbol, updatePairs);
229 
230 			// Apply updates to all symbols in modules that depend on this one
231 			cache[].filter!(a => a.dependencies.contains(cachedLocation)).each!(
232 				upstream => upstream.symbol.updateTypes(updatePairs));
233 
234 			// Remove the old symbol.
235 			cache.remove(oldEntry, entry => CacheAllocator.instance.dispose(entry));
236 		}
237 
238 		cache.insert(newEntry);
239 		recursionGuard.remove(&cachedLocation.data[0]);
240 
241 		resolveDeferredTypes(cachedLocation);
242 
243 		typeid(SemanticSymbol).destroy(first.rootSymbol);
244 
245 		return newEntry.symbol;
246 	}
247 
248 	/**
249 	 * Resolves types for deferred symbols
250 	 */
251 	void resolveDeferredTypes(istring location)
252 	{
253 		DeferredSymbols temp;
254 		temp.insert(deferredSymbols[]);
255 		deferredSymbols.clear();
256 		foreach (deferred; temp[])
257 		{
258 			if (!deferred.imports.empty && !deferred.dependsOn(location))
259 			{
260 				deferredSymbols.insert(deferred);
261 				continue;
262 			}
263 			assert(deferred.symbol.type is null);
264 			if (deferred.symbol.kind == CompletionKind.importSymbol)
265 			{
266 				resolveImport(deferred.symbol, deferred.typeLookups, this);
267 			}
268 			else if (!deferred.typeLookups.empty)
269 			{
270 				// TODO: Is .front the right thing to do here?
271 				resolveTypeFromType(deferred.symbol, deferred.typeLookups.front, null,
272 					this, &deferred.imports);
273 			}
274 			DeferredSymbolsAllocator.instance.dispose(deferred);
275 		}
276 	}
277 
278 	/**
279 	 * Params:
280 	 *     moduleName = the name of the module in "a/b/c" form
281 	 * Returns:
282 	 *     The symbols defined in the given module, or null if the module is
283 	 *     not cached yet.
284 	 */
285 	DSymbol* getModuleSymbol(istring location)
286 	{
287 		auto existing = getEntryFor(location);
288 		return existing ? existing.symbol : cacheModule(location);
289 	}
290 
291 	/**
292 	 * Params:
293 	 *     moduleName = the name of the module being imported, in "a/b/c" style
294 	 * Returns:
295 	 *     The absolute path to the file that contains the module, or null if
296 	 *     not found.
297 	 */
298 	istring resolveImportLocation(string moduleName)
299 	{
300 		assert(moduleName !is null, "module name is null");
301 		if (isRooted(moduleName))
302 			return istring(moduleName);
303 		string alternative;
304 		foreach (importPath; importPaths[])
305 		{
306 			auto path = importPath.path;
307 			// import path is a filename
308 			// first check string if this is a feasable path (no filesystem usage)
309 			if (path.stripExtension.endsWith(moduleName)
310 				&& path.existsAnd!isFile)
311 			{
312 				// prefer exact import names above .di/package.d files
313 				return istring(path);
314 			}
315 			// no exact matches and no .di/package.d matches either
316 			else if (!alternative.length)
317 			{
318 				string dotDi = buildPath(path, moduleName) ~ ".di";
319 				string dotD = dotDi[0 .. $ - 1];
320 				string withoutSuffix = dotDi[0 .. $ - 3];
321 				if (existsAnd!isFile(dotD))
322 					return istring(dotD); // return early for exactly matching .d files
323 				else if (existsAnd!isFile(dotDi))
324 					alternative = dotDi;
325 				else if (existsAnd!isDir(withoutSuffix))
326 				{
327 					string packagePath = buildPath(withoutSuffix, "package.di");
328 					if (existsAnd!isFile(packagePath[0 .. $ - 1]))
329 						alternative = packagePath[0 .. $ - 1];
330 					else if (existsAnd!isFile(packagePath))
331 						alternative = packagePath;
332 				}
333 			}
334 			// we have a potential .di/package.d file but continue searching for
335 			// exact .d file matches to use instead
336 			else
337 			{
338 				string dotD = buildPath(path, moduleName) ~ ".d";
339 				if (existsAnd!isFile(dotD))
340 					return istring(dotD); // return early for exactly matching .d files
341 			}
342 		}
343 		return alternative.length > 0 ? istring(alternative) : istring(null);
344 	}
345 
346 	auto getImportPaths() const
347 	{
348 		return importPaths[].map!(a => a.path);
349 	}
350 
351 	auto getAllSymbols()
352 	{
353 		scanAll();
354 		return cache[];
355 	}
356 
357 	IAllocator symbolAllocator;
358 
359 	alias DeferredSymbols = UnrolledList!(DeferredSymbol*, DeferredSymbolsAllocator);
360 	DeferredSymbols deferredSymbols;
361 
362 	/// Count of autocomplete symbols that have been allocated
363 	uint symbolsAllocated;
364 
365 private:
366 
367 	CacheEntry* getEntryFor(istring cachedLocation)
368 	{
369 		CacheEntry dummy;
370 		dummy.path = cachedLocation;
371 		auto r = cache.equalRange(&dummy);
372 		return r.empty ? null : r.front;
373 	}
374 
375 	/**
376 	 * Params:
377 	 *     mod = the path to the module
378 	 * Returns:
379 	 *     true  if the module needs to be reparsed, false otherwise
380 	 */
381 	bool needsReparsing(istring mod)
382 	{
383 		if (!exists(mod.data))
384 			return true;
385 		CacheEntry e;
386 		e.path = mod;
387 		auto r = cache.equalRange(&e);
388 		if (r.empty)
389 			return true;
390 		SysTime access;
391 		SysTime modification;
392 		getTimes(mod.data, access, modification);
393 		return r.front.modificationTime != modification;
394 	}
395 
396 	void scanAll()
397 	{
398 		foreach (ref importPath; importPaths)
399 		{
400 			if (importPath.scanned)
401 				continue;
402 			scope(success) importPath.scanned = true;
403 
404 			if (importPath.path.existsAnd!isFile)
405 			{
406 				if (importPath.path.baseName.startsWith(".#"))
407 					continue;
408 				cacheModule(importPath.path);
409 			}
410 			else
411 			{
412 				void scanFrom(const string root)
413 				{
414 					if (exists(buildPath(root, ".no-dcd")))
415 						return;
416 
417 					try foreach (f; dirEntries(root, SpanMode.shallow))
418 					{
419 						if (f.name.existsAnd!isFile)
420 						{
421 							if (!f.name.extension.among(".d", ".di") || f.name.baseName.startsWith(".#"))
422 								continue;
423 							cacheModule(f.name);
424 						}
425 						else scanFrom(f.name);
426 					}
427 					catch(FileException) {}
428 				}
429 				scanFrom(importPath.path);
430 			}
431 		}
432 	}
433 
434 	// Mapping of file paths to their cached symbols.
435 	alias CacheAllocator = GCAllocator; // NOTE using `Mallocator` here fails when analysing Phobos as `Segmentation fault (core dumped)`
436 	alias Cache = TTree!(CacheEntry*, CacheAllocator);
437 	Cache cache;
438 
439 	HashSet!(immutable(char)*) recursionGuard;
440 
441 	struct ImportPath
442 	{
443 		string path;
444 		bool scanned;
445 	}
446 
447 	// Listing of paths to check for imports
448 	UnrolledList!ImportPath importPaths;
449 }
450 
451 /// Wrapper to check some attribute of a path, ignoring errors
452 /// (such as on a broken symlink).
453 private static bool existsAnd(alias fun)(string file)
454 {
455 	try
456 		return fun(file);
457 	catch (FileException e)
458 		return false;
459 }
460 
461 /// same as getAttributes without throwing
462 /// Returns: true if exists, false otherwise
463 private static bool getFileAttributesFast(R)(R name, uint* attributes)
464 {
465 	version (Windows)
466 	{
467 		import std.internal.cstring : tempCStringW;
468 		import core.sys.windows.winnt : INVALID_FILE_ATTRIBUTES;
469 		import core.sys.windows.winbase : GetFileAttributesW;
470 
471 		auto namez = tempCStringW(name);
472 		static auto trustedGetFileAttributesW(const(wchar)* namez) @trusted
473 		{
474 			return GetFileAttributesW(namez);
475 		}
476 		*attributes = trustedGetFileAttributesW(namez);
477 		return *attributes != INVALID_FILE_ATTRIBUTES;
478 	}
479 	else version (Posix)
480 	{
481 		import core.sys.posix.sys.stat : stat, stat_t;
482 		import std.internal.cstring : tempCString;
483 
484 		auto namez = tempCString(name);
485 		static auto trustedStat(const(char)* namez, out stat_t statbuf) @trusted
486 		{
487 			return stat(namez, &statbuf);
488 		}
489 
490 		stat_t statbuf;
491 		const ret = trustedStat(namez, statbuf) == 0;
492 		*attributes = statbuf.st_mode;
493 		return ret;
494 	}
495 	else
496 	{
497 		static assert(false, "Unimplemented getAttributes check");
498 	}
499 }
500 
501 private static bool existsAnd(alias fun : isFile)(string file)
502 {
503 	uint attributes;
504 	if (!getFileAttributesFast(file, &attributes))
505 		return false;
506 	return attrIsFile(attributes);
507 }
508 
509 private static bool existsAnd(alias fun : isDir)(string file)
510 {
511 	uint attributes;
512 	if (!getFileAttributesFast(file, &attributes))
513 		return false;
514 	return attrIsDir(attributes);
515 }
516 
517 version (Windows)
518 {
519 	unittest
520 	{
521 		assert(existsAnd!isFile(`C:\Windows\regedit.exe`));
522 		assert(existsAnd!isDir(`C:\Windows`));
523 		assert(!existsAnd!isDir(`C:\Windows\regedit.exe`));
524 		assert(!existsAnd!isDir(`C:\SomewhereNonExistant\nonexistant.exe`));
525 		assert(!existsAnd!isFile(`C:\SomewhereNonExistant\nonexistant.exe`));
526 		assert(!existsAnd!isFile(`C:\Windows`));
527 	}
528 }
529 else version (Posix)
530 {
531 	unittest
532 	{
533 		assert(existsAnd!isFile(`/bin/true`));
534 		assert(existsAnd!isDir(`/bin`));
535 		assert(!existsAnd!isDir(`/bin/true`));
536 		assert(!existsAnd!isDir(`/nonexistant_dir/__nonexistant`));
537 		assert(!existsAnd!isFile(`/nonexistant_dir/__nonexistant`));
538 		assert(!existsAnd!isFile(`/bin`));
539 	}
540 }