> */ private array $unchanged_members = []; /** * @var array> */ private array $unchanged_signature_members = []; /** * @var array> */ private array $changed_members = []; /** * @var array */ private array $errors = []; /** * @var array> */ private array $diff_map = []; /** * @var array> */ private array $deletion_ranges = []; private static ?Emulative $lexer = null; private static ?Parser $parser = null; public function __construct( FileProvider $file_provider, ?ParserCacheProvider $parser_cache_provider = null, ?FileStorageCacheProvider $file_storage_cache_provider = null ) { $this->file_provider = $file_provider; $this->parser_cache_provider = $parser_cache_provider; $this->this_modified_time = filemtime(__FILE__); $this->file_storage_cache_provider = $file_storage_cache_provider; $this->statements_volatile_cache = StatementsVolatileCache::getInstance(); } /** * @return list */ public function getStatementsForFile( string $file_path, int $analysis_php_version_id, ?Progress $progress = null ): array { unset($this->errors[$file_path]); if ($progress === null) { $progress = new VoidProgress(); } $from_cache = false; $version = PHP_PARSER_VERSION . $this->this_modified_time; $file_contents = $this->file_provider->getContents($file_path); $modified_time = $this->file_provider->getModifiedTime($file_path); $config = Config::getInstance(); if (PHP_VERSION_ID >= 8_01_00) { $file_content_hash = hash('xxh128', $version . $file_contents); } else { $file_content_hash = hash('md4', $version . $file_contents); } if (!$this->parser_cache_provider || (!$config->isInProjectDirs($file_path) && strpos($file_path, 'vendor')) ) { $cache_key = "{$file_content_hash}:{$analysis_php_version_id}"; if ($this->statements_volatile_cache->has($cache_key)) { return $this->statements_volatile_cache->get($cache_key); } $progress->debug('Parsing ' . $file_path . "\n"); $has_errors = false; $stmts = self::parseStatements($file_contents, $analysis_php_version_id, $has_errors, $file_path); $this->statements_volatile_cache->set($cache_key, $stmts); return $stmts; } $stmts = $this->parser_cache_provider->loadStatementsFromCache( $file_path, $modified_time, $file_content_hash, ); if ($stmts === null) { $progress->debug('Parsing ' . $file_path . "\n"); $existing_statements = $this->parser_cache_provider->loadExistingStatementsFromCache($file_path); $existing_file_contents = $this->parser_cache_provider->loadExistingFileContentsFromCache($file_path); // this happens after editing temporary file if ($existing_file_contents === $file_contents && $existing_statements) { $this->diff_map[$file_path] = []; $this->parser_cache_provider->saveStatementsToCache( $file_path, $file_content_hash, $existing_statements, true, ); return $existing_statements; } $file_changes = null; $existing_statements_copy = null; if ($existing_statements && $existing_file_contents && abs(strlen($existing_file_contents) - strlen($file_contents)) < 5_000 ) { $file_changes = FileDiffer::getDiff($existing_file_contents, $file_contents); if (count($file_changes) < 10) { $traverser = new PhpParser\NodeTraverser; $traverser->addVisitor(new CloningVisitor); // performs a deep clone /** @var list */ $existing_statements_copy = $traverser->traverse($existing_statements); } else { $file_changes = null; } } $has_errors = false; $stmts = self::parseStatements( $file_contents, $analysis_php_version_id, $has_errors, $file_path, $existing_file_contents, $existing_statements_copy, $file_changes, ); if ($existing_file_contents && $existing_statements && (!$has_errors || $stmts)) { [$unchanged_members, $unchanged_signature_members, $changed_members, $diff_map, $deletion_ranges] = FileStatementsDiffer::diff( $existing_statements, $stmts, $existing_file_contents, $file_contents, ); $unchanged_members = array_fill_keys($unchanged_members, true); $unchanged_signature_members = array_fill_keys($unchanged_signature_members, true); // do NOT change this to hash, it will fail on Windows for whatever reason $file_path_hash = md5($file_path); $changed_members = array_map( static function (string $key) use ($file_path_hash): string { if (strpos($key, 'use:') === 0) { return $key . ':' . $file_path_hash; } return $key; }, $changed_members, ); $changed_members = array_fill_keys($changed_members, true); if (isset($this->unchanged_members[$file_path])) { $this->unchanged_members[$file_path] = array_intersect_key( $this->unchanged_members[$file_path], $unchanged_members, ); } else { $this->unchanged_members[$file_path] = $unchanged_members; } if (isset($this->unchanged_signature_members[$file_path])) { $this->unchanged_signature_members[$file_path] = array_intersect_key( $this->unchanged_signature_members[$file_path], $unchanged_signature_members, ); } else { $this->unchanged_signature_members[$file_path] = $unchanged_signature_members; } if (isset($this->changed_members[$file_path])) { $this->changed_members[$file_path] = array_merge( $this->changed_members[$file_path], $changed_members, ); } else { $this->changed_members[$file_path] = $changed_members; } $this->diff_map[$file_path] = $diff_map; $this->deletion_ranges[$file_path] = $deletion_ranges; } elseif ($has_errors && !$stmts) { $this->errors[$file_path] = true; } if ($this->file_storage_cache_provider) { $this->file_storage_cache_provider->removeCacheForFile($file_path); } $this->parser_cache_provider->cacheFileContents($file_path, $file_contents); } else { $from_cache = true; $this->diff_map[$file_path] = []; $this->deletion_ranges[$file_path] = []; } $this->parser_cache_provider->saveStatementsToCache($file_path, $file_content_hash, $stmts, $from_cache); if (!$stmts) { return []; } return $stmts; } /** * @return array> */ public function getChangedMembers(): array { return $this->changed_members; } /** * @param array> $more_changed_members */ public function addChangedMembers(array $more_changed_members): void { $this->changed_members = array_merge($more_changed_members, $this->changed_members); } /** * @return array> */ public function getUnchangedSignatureMembers(): array { return $this->unchanged_signature_members; } /** * @param array> $more_unchanged_members */ public function addUnchangedSignatureMembers(array $more_unchanged_members): void { $this->unchanged_signature_members = array_merge($more_unchanged_members, $this->unchanged_signature_members); } /** * @return array */ public function getErrors(): array { return $this->errors; } /** * @param array $errors */ public function addErrors(array $errors): void { $this->errors += $errors; } public function setUnchangedFile(string $file_path): void { if (!isset($this->diff_map[$file_path])) { $this->diff_map[$file_path] = []; } if (!isset($this->deletion_ranges[$file_path])) { $this->deletion_ranges[$file_path] = []; } } /** * @return array> */ public function getDiffMap(): array { return $this->diff_map; } /** * @return array> */ public function getDeletionRanges(): array { return $this->deletion_ranges; } /** * @param array> $diff_map */ public function addDiffMap(array $diff_map): void { $this->diff_map = array_merge($diff_map, $this->diff_map); } /** * @param array> $deletion_ranges */ public function addDeletionRanges(array $deletion_ranges): void { $this->deletion_ranges = array_merge($deletion_ranges, $this->deletion_ranges); } public function resetDiffs(): void { $this->changed_members = []; $this->unchanged_members = []; $this->unchanged_signature_members = []; $this->diff_map = []; $this->deletion_ranges = []; } /** * @param list $existing_statements * @param array $file_changes * @return list */ public static function parseStatements( string $file_contents, int $analysis_php_version_id, bool &$has_errors, ?string $file_path = null, ?string $existing_file_contents = null, ?array $existing_statements = null, ?array $file_changes = null ): array { $attributes = [ 'comments', 'startLine', 'startFilePos', 'endFilePos', ]; if (!self::$lexer) { $major_version = Codebase::transformPhpVersionId($analysis_php_version_id, 10_000); $minor_version = Codebase::transformPhpVersionId($analysis_php_version_id % 10_000, 100); self::$lexer = new Emulative([ 'usedAttributes' => $attributes, 'phpVersion' => $major_version . '.' . $minor_version, ]); } if (!self::$parser) { self::$parser = (new PhpParser\ParserFactory())->create(PhpParser\ParserFactory::ONLY_PHP7, self::$lexer); } $used_cached_statements = false; $error_handler = new Collecting(); if ($existing_statements && $file_changes && $existing_file_contents) { $clashing_traverser = new CustomTraverser; $offset_analyzer = new PartialParserVisitor( self::$parser, $error_handler, $file_changes, $existing_file_contents, $file_contents, ); $clashing_traverser->addVisitor($offset_analyzer); $clashing_traverser->traverse($existing_statements); if (!$offset_analyzer->mustRescan()) { $used_cached_statements = true; $stmts = $existing_statements; } else { try { /** @var list */ $stmts = self::$parser->parse($file_contents, $error_handler) ?: []; } catch (Throwable $t) { $stmts = []; // hope this got caught below } } } else { try { /** @var list */ $stmts = self::$parser->parse($file_contents, $error_handler) ?: []; } catch (Throwable $t) { $stmts = []; // hope this got caught below } } if ($error_handler->hasErrors() && $file_path) { $config = Config::getInstance(); $has_errors = true; foreach ($error_handler->getErrors() as $error) { if ($error->hasColumnInfo()) { IssueBuffer::maybeAdd( new ParseError( $error->getMessage(), new ParseErrorLocation( $error, $file_contents, $file_path, $config->shortenFileName($file_path), ), ), ); } } } $error_handler->clearErrors(); $resolving_traverser = new PhpParser\NodeTraverser; $name_resolver = new SimpleNameResolver( $error_handler, $used_cached_statements ? $file_changes : [], ); $resolving_traverser->addVisitor($name_resolver); $resolving_traverser->traverse($stmts); return $stmts; } public static function clearLexer(): void { self::$lexer = null; } public static function clearParser(): void { self::$parser = null; } }