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 }