Chris@17: Chris@17: * @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600) Chris@17: * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence Chris@17: */ Chris@17: Chris@17: namespace PHP_CodeSniffer\Util; Chris@17: Chris@17: use PHP_CodeSniffer\Autoload; Chris@17: use PHP_CodeSniffer\Config; Chris@17: use PHP_CodeSniffer\Ruleset; Chris@17: Chris@17: class Cache Chris@17: { Chris@17: Chris@17: /** Chris@17: * The filesystem location of the cache file. Chris@17: * Chris@17: * @var void Chris@17: */ Chris@17: private static $path = ''; Chris@17: Chris@17: /** Chris@17: * The cached data. Chris@17: * Chris@17: * @var array Chris@17: */ Chris@17: private static $cache = []; Chris@17: Chris@17: Chris@17: /** Chris@17: * Loads existing cache data for the run, if any. Chris@17: * Chris@17: * @param \PHP_CodeSniffer\Ruleset $ruleset The ruleset used for the run. Chris@17: * @param \PHP_CodeSniffer\Config $config The config data for the run. Chris@17: * Chris@17: * @return void Chris@17: */ Chris@17: public static function load(Ruleset $ruleset, Config $config) Chris@17: { Chris@17: // Look at every loaded sniff class so far and use their file contents Chris@17: // to generate a hash for the code used during the run. Chris@17: // At this point, the loaded class list contains the core PHPCS code Chris@17: // and all sniffs that have been loaded as part of the run. Chris@17: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@17: echo PHP_EOL."\tGenerating loaded file list for code hash".PHP_EOL; Chris@17: } Chris@17: Chris@17: $codeHashFiles = []; Chris@17: Chris@17: $classes = array_keys(Autoload::getLoadedClasses()); Chris@17: sort($classes); Chris@17: Chris@17: $installDir = dirname(__DIR__); Chris@17: $installDirLen = strlen($installDir); Chris@17: $standardDir = $installDir.DIRECTORY_SEPARATOR.'Standards'; Chris@17: $standardDirLen = strlen($standardDir); Chris@17: foreach ($classes as $file) { Chris@17: if (substr($file, 0, $standardDirLen) !== $standardDir) { Chris@17: if (substr($file, 0, $installDirLen) === $installDir) { Chris@17: // We are only interested in sniffs here. Chris@17: continue; Chris@17: } Chris@17: Chris@17: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@17: echo "\t\t=> external file: $file".PHP_EOL; Chris@17: } Chris@17: } else if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@17: echo "\t\t=> internal sniff: $file".PHP_EOL; Chris@17: } Chris@17: Chris@17: $codeHashFiles[] = $file; Chris@17: } Chris@17: Chris@17: // Add the content of the used rulesets to the hash so that sniff setting Chris@17: // changes in the ruleset invalidate the cache. Chris@17: $rulesets = $ruleset->paths; Chris@17: sort($rulesets); Chris@17: foreach ($rulesets as $file) { Chris@17: if (substr($file, 0, $standardDirLen) !== $standardDir) { Chris@17: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@17: echo "\t\t=> external ruleset: $file".PHP_EOL; Chris@17: } Chris@17: } else if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@17: echo "\t\t=> internal ruleset: $file".PHP_EOL; Chris@17: } Chris@17: Chris@17: $codeHashFiles[] = $file; Chris@17: } Chris@17: Chris@17: // Go through the core PHPCS code and add those files to the file Chris@17: // hash. This ensures that core PHPCS changes will also invalidate the cache. Chris@17: // Note that we ignore sniffs here, and any files that don't affect Chris@17: // the outcome of the run. Chris@17: $di = new \RecursiveDirectoryIterator($installDir); Chris@17: $filter = new \RecursiveCallbackFilterIterator( Chris@17: $di, Chris@17: function ($file, $key, $iterator) { Chris@17: // Skip hidden files. Chris@17: $filename = $file->getFilename(); Chris@17: if (substr($filename, 0, 1) === '.') { Chris@17: return false; Chris@17: } Chris@17: Chris@17: $filePath = Common::realpath($file->getPathname()); Chris@17: if ($filePath === false) { Chris@17: return false; Chris@17: } Chris@17: Chris@17: if (is_dir($filePath) === true Chris@17: && ($filename === 'Standards' Chris@17: || $filename === 'Exceptions' Chris@17: || $filename === 'Reports' Chris@17: || $filename === 'Generators') Chris@17: ) { Chris@17: return false; Chris@17: } Chris@17: Chris@17: return true; Chris@17: } Chris@17: ); Chris@17: Chris@17: $iterator = new \RecursiveIteratorIterator($filter); Chris@17: foreach ($iterator as $file) { Chris@17: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@17: echo "\t\t=> core file: $file".PHP_EOL; Chris@17: } Chris@17: Chris@17: $codeHashFiles[] = $file->getPathname(); Chris@17: } Chris@17: Chris@17: $codeHash = ''; Chris@17: sort($codeHashFiles); Chris@17: foreach ($codeHashFiles as $file) { Chris@17: $codeHash .= md5_file($file); Chris@17: } Chris@17: Chris@17: $codeHash = md5($codeHash); Chris@17: Chris@17: // Along with the code hash, use various settings that can affect Chris@17: // the results of a run to create a new hash. This hash will be used Chris@17: // in the cache file name. Chris@17: $rulesetHash = md5(var_export($ruleset->ignorePatterns, true).var_export($ruleset->includePatterns, true)); Chris@17: $configData = [ Chris@17: 'phpVersion' => PHP_VERSION_ID, Chris@17: 'tabWidth' => $config->tabWidth, Chris@17: 'encoding' => $config->encoding, Chris@17: 'recordErrors' => $config->recordErrors, Chris@17: 'annotations' => $config->annotations, Chris@17: 'configData' => Config::getAllConfigData(), Chris@17: 'codeHash' => $codeHash, Chris@17: 'rulesetHash' => $rulesetHash, Chris@17: ]; Chris@17: Chris@17: $configString = var_export($configData, true); Chris@17: $cacheHash = substr(sha1($configString), 0, 12); Chris@17: Chris@17: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@17: echo "\tGenerating cache key data".PHP_EOL; Chris@17: foreach ($configData as $key => $value) { Chris@17: if (is_array($value) === true) { Chris@17: echo "\t\t=> $key:".PHP_EOL; Chris@17: foreach ($value as $subKey => $subValue) { Chris@17: echo "\t\t\t=> $subKey: $subValue".PHP_EOL; Chris@17: } Chris@17: Chris@17: continue; Chris@17: } Chris@17: Chris@17: if ($value === true || $value === false) { Chris@17: $value = (int) $value; Chris@17: } Chris@17: Chris@17: echo "\t\t=> $key: $value".PHP_EOL; Chris@17: } Chris@17: Chris@17: echo "\t\t=> cacheHash: $cacheHash".PHP_EOL; Chris@17: }//end if Chris@17: Chris@17: if ($config->cacheFile !== null) { Chris@17: $cacheFile = $config->cacheFile; Chris@17: } else { Chris@17: // Determine the common paths for all files being checked. Chris@17: // We can use this to locate an existing cache file, or to Chris@17: // determine where to create a new one. Chris@17: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@17: echo "\tChecking possible cache file paths".PHP_EOL; Chris@17: } Chris@17: Chris@17: $paths = []; Chris@17: foreach ($config->files as $file) { Chris@17: $file = Common::realpath($file); Chris@17: while ($file !== DIRECTORY_SEPARATOR) { Chris@17: if (isset($paths[$file]) === false) { Chris@17: $paths[$file] = 1; Chris@17: } else { Chris@17: $paths[$file]++; Chris@17: } Chris@17: Chris@17: $lastFile = $file; Chris@17: $file = dirname($file); Chris@17: if ($file === $lastFile) { Chris@17: // Just in case something went wrong, Chris@17: // we don't want to end up in an infinite loop. Chris@17: break; Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: ksort($paths); Chris@17: $paths = array_reverse($paths); Chris@17: Chris@17: $numFiles = count($config->files); Chris@17: Chris@17: $cacheFile = null; Chris@17: $cacheDir = getenv('XDG_CACHE_HOME'); Chris@17: if ($cacheDir === false || is_dir($cacheDir) === false) { Chris@17: $cacheDir = sys_get_temp_dir(); Chris@17: } Chris@17: Chris@17: foreach ($paths as $file => $count) { Chris@17: if ($count !== $numFiles) { Chris@17: unset($paths[$file]); Chris@17: continue; Chris@17: } Chris@17: Chris@17: $fileHash = substr(sha1($file), 0, 12); Chris@17: $testFile = $cacheDir.DIRECTORY_SEPARATOR."phpcs.$fileHash.$cacheHash.cache"; Chris@17: if ($cacheFile === null) { Chris@17: // This will be our default location if we can't find Chris@17: // an existing file. Chris@17: $cacheFile = $testFile; Chris@17: } Chris@17: Chris@17: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@17: echo "\t\t=> $testFile".PHP_EOL; Chris@17: echo "\t\t\t * based on shared location: $file *".PHP_EOL; Chris@17: } Chris@17: Chris@17: if (file_exists($testFile) === true) { Chris@17: $cacheFile = $testFile; Chris@17: break; Chris@17: } Chris@17: }//end foreach Chris@17: Chris@17: if ($cacheFile === null) { Chris@17: // Unlikely, but just in case $paths is empty for some reason. Chris@17: $cacheFile = $cacheDir.DIRECTORY_SEPARATOR."phpcs.$cacheHash.cache"; Chris@17: } Chris@17: }//end if Chris@17: Chris@17: self::$path = $cacheFile; Chris@17: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@17: echo "\t=> Using cache file: ".self::$path.PHP_EOL; Chris@17: } Chris@17: Chris@17: if (file_exists(self::$path) === true) { Chris@17: self::$cache = json_decode(file_get_contents(self::$path), true); Chris@17: Chris@17: // Verify the contents of the cache file. Chris@17: if (self::$cache['config'] !== $configData) { Chris@17: self::$cache = []; Chris@17: if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@17: echo "\t* cache was invalid and has been cleared *".PHP_EOL; Chris@17: } Chris@17: } Chris@17: } else if (PHP_CODESNIFFER_VERBOSITY > 1) { Chris@17: echo "\t* cache file does not exist *".PHP_EOL; Chris@17: } Chris@17: Chris@17: self::$cache['config'] = $configData; Chris@17: Chris@17: }//end load() Chris@17: Chris@17: Chris@17: /** Chris@17: * Saves the current cache to the filesystem. Chris@17: * Chris@17: * @return void Chris@17: */ Chris@17: public static function save() Chris@17: { Chris@17: file_put_contents(self::$path, json_encode(self::$cache)); Chris@17: Chris@17: }//end save() Chris@17: Chris@17: Chris@17: /** Chris@17: * Retrieves a single entry from the cache. Chris@17: * Chris@17: * @param string $key The key of the data to get. If NULL, Chris@17: * everything in the cache is returned. Chris@17: * Chris@17: * @return mixed Chris@17: */ Chris@17: public static function get($key=null) Chris@17: { Chris@17: if ($key === null) { Chris@17: return self::$cache; Chris@17: } Chris@17: Chris@17: if (isset(self::$cache[$key]) === true) { Chris@17: return self::$cache[$key]; Chris@17: } Chris@17: Chris@17: return false; Chris@17: Chris@17: }//end get() Chris@17: Chris@17: Chris@17: /** Chris@17: * Retrieves a single entry from the cache. Chris@17: * Chris@17: * @param string $key The key of the data to set. If NULL, Chris@17: * sets the entire cache. Chris@17: * @param mixed $value The value to set. Chris@17: * Chris@17: * @return void Chris@17: */ Chris@17: public static function set($key, $value) Chris@17: { Chris@17: if ($key === null) { Chris@17: self::$cache = $value; Chris@17: } else { Chris@17: self::$cache[$key] = $value; Chris@17: } Chris@17: Chris@17: }//end set() Chris@17: Chris@17: Chris@17: /** Chris@17: * Retrieves the number of cache entries. Chris@17: * Chris@17: * @return int Chris@17: */ Chris@17: public static function getSize() Chris@17: { Chris@17: return (count(self::$cache) - 1); Chris@17: Chris@17: }//end getSize() Chris@17: Chris@17: Chris@17: }//end class