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 }