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