*/ private $classlike_files = []; /** * @var array */ private $files_to_scan = []; /** * @var array */ private $classes_to_scan = []; /** * @var array */ private $classes_to_deep_scan = []; /** * @var array */ private $files_to_deep_scan = []; /** * @var array */ private $scanned_files = []; /** * @var null|array */ private $composer_classmap; /** * @var array */ private $store_scan_failure = []; /** * @var array */ private $reflected_classlikes_lc = []; /** * @var Reflection */ private $reflection; /** * @var Config */ private $config; /** * @var bool */ private $debug_output; /** * @var FileStorageProvider */ private $file_storage_provider; /** * @var FileProvider */ private $file_provider; /** * @param bool $debug_output */ public function __construct( Codebase $codebase, Config $config, FileStorageProvider $file_storage_provider, FileProvider $file_provider, Reflection $reflection, $debug_output ) { $this->codebase = $codebase; $this->reflection = $reflection; $this->file_provider = $file_provider; $this->debug_output = $debug_output; $this->file_storage_provider = $file_storage_provider; $this->config = $config; } /** * @param array $files_to_scan * * @return void */ public function addFilesToDeepScan(array $files_to_scan) { $this->files_to_scan += $files_to_scan; $this->files_to_deep_scan += $files_to_scan; } /** * @param string $fq_classlike_name_lc * @param string $file_path * * @return void */ public function setClassLikeFilePath($fq_classlike_name_lc, $file_path) { $this->classlike_files[$fq_classlike_name_lc] = $file_path; } /** * @param string $fq_classlike_name_lc * * @return string */ public function getClassLikeFilePath($fq_classlike_name_lc) { if (!isset($this->classlike_files[$fq_classlike_name_lc])) { throw new \UnexpectedValueException('Could not find file for ' . $fq_classlike_name_lc); } return $this->classlike_files[$fq_classlike_name_lc]; } /** * @param string $fq_classlike_name * @param string|null $referencing_file_path * @param bool $analyze_too * @param bool $store_failure * * @return void */ public function queueClassLikeForScanning( $fq_classlike_name, $referencing_file_path = null, $analyze_too = false, $store_failure = true ) { $fq_classlike_name_lc = strtolower($fq_classlike_name); // avoid checking classes that we know will just end in failure if ($fq_classlike_name_lc === 'null' || substr($fq_classlike_name_lc, -5) === '\null') { return; } if (!isset($this->classlike_files[$fq_classlike_name_lc])) { if (!isset($this->classes_to_scan[$fq_classlike_name_lc]) || $store_failure) { $this->classes_to_scan[$fq_classlike_name_lc] = $fq_classlike_name; } if ($analyze_too) { $this->classes_to_deep_scan[$fq_classlike_name_lc] = true; } $this->store_scan_failure[$fq_classlike_name] = $store_failure; } if ($referencing_file_path) { FileReferenceProvider::addFileReferenceToClass($referencing_file_path, $fq_classlike_name_lc); } } /** * @return bool */ public function scanFiles(ClassLikes $classlikes) { $filetype_scanners = $this->config->getFiletypeScanners(); $has_changes = false; while ($this->files_to_scan || $this->classes_to_scan) { if ($this->files_to_scan) { $file_path = array_shift($this->files_to_scan); if (!isset($this->scanned_files[$file_path])) { $this->scanFile( $file_path, $filetype_scanners, isset($this->files_to_deep_scan[$file_path]) ); $has_changes = true; } } else { /** @var string */ $fq_classlike_name = array_shift($this->classes_to_scan); $fq_classlike_name_lc = strtolower($fq_classlike_name); if (isset($this->reflected_classlikes_lc[$fq_classlike_name_lc])) { continue; } if ($classlikes->isMissingClassLike($fq_classlike_name_lc)) { continue; } if (!isset($this->classlike_files[$fq_classlike_name_lc])) { if ($classlikes->doesClassLikeExist($fq_classlike_name_lc)) { if ($this->debug_output) { echo 'Using reflection to get metadata for ' . $fq_classlike_name . PHP_EOL; } $reflected_class = new \ReflectionClass($fq_classlike_name); $this->reflection->registerClass($reflected_class); $this->reflected_classlikes_lc[$fq_classlike_name_lc] = true; } elseif ($this->fileExistsForClassLike($classlikes, $fq_classlike_name)) { // even though we've checked this above, calling the method invalidates it if (isset($this->classlike_files[$fq_classlike_name_lc])) { /** @var string */ $file_path = $this->classlike_files[$fq_classlike_name_lc]; $this->files_to_scan[$file_path] = $file_path; if (isset($this->classes_to_deep_scan[$fq_classlike_name_lc])) { unset($this->classes_to_deep_scan[$fq_classlike_name_lc]); $this->files_to_deep_scan[$file_path] = $file_path; } } } elseif ($this->store_scan_failure[$fq_classlike_name]) { $classlikes->registerMissingClassLike($fq_classlike_name_lc); } } } } return $has_changes; } /** * @param string $file_path * @param array $filetype_scanners * @param bool $will_analyze * * @return FileScanner * * @psalm-suppress MixedOffset */ private function scanFile( $file_path, array $filetype_scanners, $will_analyze = false ) { $file_scanner = $this->getScannerForPath($file_path, $filetype_scanners, $will_analyze); if (isset($this->scanned_files[$file_path])) { throw new \UnexpectedValueException('Should not be rescanning ' . $file_path); } $file_contents = $this->file_provider->getContents($file_path); $from_cache = $this->file_storage_provider->has($file_path, $file_contents); if (!$from_cache) { $this->file_storage_provider->create($file_path); } $this->scanned_files[$file_path] = true; $file_storage = $this->file_storage_provider->get($file_path); $file_scanner->scan( $this->codebase, $file_storage, $from_cache, $this->debug_output ); $file_storage->deep_scan = $will_analyze; if (!$from_cache) { if (!$file_storage->has_trait) { $this->file_storage_provider->cache->writeToCache($file_storage, $file_contents); } } else { foreach ($file_storage->included_file_paths as $file_path) { $this->codebase->scanner->queueFileForScanning($file_path); } foreach ($file_storage->classlikes_in_file as $fq_classlike_name) { $this->codebase->exhumeClassLikeStorage($fq_classlike_name, $file_path); } foreach ($file_storage->required_classes as $fq_classlike_name) { $this->queueClassLikeForScanning($fq_classlike_name, $file_path, $will_analyze); } foreach ($file_storage->required_interfaces as $fq_classlike_name) { $this->queueClassLikeForScanning($fq_classlike_name, $file_path); } foreach ($file_storage->referenced_classlikes as $fq_classlike_name) { $this->queueClassLikeForScanning($fq_classlike_name, $file_path); } } return $file_scanner; } /** * @param string $file_path * @param array $filetype_scanners * @param bool $will_analyze * * @return FileScanner */ private function getScannerForPath( $file_path, array $filetype_scanners, $will_analyze = false ) { $path_parts = explode(DIRECTORY_SEPARATOR, $file_path); $file_name_parts = explode('.', array_pop($path_parts)); $extension = count($file_name_parts) > 1 ? array_pop($file_name_parts) : null; $file_name = $this->config->shortenFileName($file_path); if (isset($filetype_scanners[$extension])) { /** @var FileScanner */ return new $filetype_scanners[$extension]($file_path, $file_name, $will_analyze); } return new FileScanner($file_path, $file_name, $will_analyze); } /** * @param string $file_path * * @return void */ public function queueFileForScanning($file_path) { $this->files_to_scan[$file_path] = $file_path; } /** * @return array */ public function getScannedFiles() { return $this->scanned_files; } /** * Checks whether a class exists, and if it does then records what file it's in * for later checking * * @param string $fq_class_name * * @return bool */ private function fileExistsForClassLike(ClassLikes $classlikes, $fq_class_name) { $fq_class_name_lc = strtolower($fq_class_name); if (isset($this->classlike_files[$fq_class_name_lc])) { return true; } if (isset($this->existing_classlikes_lc[$fq_class_name_lc])) { throw new \InvalidArgumentException('Why are you asking about a builtin class?'); } if ($this->composer_classmap === null) { $this->composer_classmap = $this->config->getComposerClassMap(); } if (isset($this->composer_classmap[$fq_class_name_lc])) { if (file_exists($this->composer_classmap[$fq_class_name_lc])) { if ($this->debug_output) { echo 'Using generated composer classmap to locate file for ' . $fq_class_name . PHP_EOL; } $classlikes->addFullyQualifiedClassLikeName( $fq_class_name_lc, $this->composer_classmap[$fq_class_name_lc] ); return true; } } $old_level = error_reporting(); if (!$this->debug_output) { error_reporting(E_ERROR); } try { if ($this->debug_output) { echo 'Using reflection to locate file for ' . $fq_class_name . PHP_EOL; } $reflected_class = new \ReflectionClass($fq_class_name); } catch (\ReflectionException $e) { error_reporting($old_level); // do not cache any results here (as case-sensitive filenames can screw things up) return false; } error_reporting($old_level); /** @psalm-suppress MixedMethodCall due to Reflection class weirdness */ $file_path = (string)$reflected_class->getFileName(); // if the file was autoloaded but exists in evaled code only, return false if (!file_exists($file_path)) { return false; } $fq_class_name = $reflected_class->getName(); $classlikes->addFullyQualifiedClassLikeName($fq_class_name_lc); if ($reflected_class->isInterface()) { $classlikes->addFullyQualifiedInterfaceName($fq_class_name, $file_path); } elseif ($reflected_class->isTrait()) { $classlikes->addFullyQualifiedTraitName($fq_class_name, $file_path); } else { $classlikes->addFullyQualifiedClassName($fq_class_name, $file_path); } return true; } }