From e1a23090d64c1ef21285484887ff328b4bfed25d Mon Sep 17 00:00:00 2001 From: whwyy Date: Tue, 7 Jul 2026 15:09:38 +0800 Subject: [PATCH] eee --- Scanner.php | 158 +++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 150 insertions(+), 8 deletions(-) diff --git a/Scanner.php b/Scanner.php index 8d47b24..068c249 100644 --- a/Scanner.php +++ b/Scanner.php @@ -41,9 +41,13 @@ class Scanner extends Component 'follow_links' => false, 'cache_enabled' => false, 'cache_ttl' => 3600, + 'opcache_compile' => false, + 'require_files_without_classes' => false, 'debug' => false, ]; + private static ?string $currentFile = null; + public function __construct() { $this->basePath = $this->normalizePath($_SERVER['PWD'] ?? APP_PATH ?? getcwd()); @@ -79,6 +83,9 @@ class Scanner extends Component } $this->syncLegacyState(); + if ($this->config['cache_enabled'] && $cacheFile !== null) { + $this->saveToCache($cacheFile); + } return $this->changeSet; } @@ -113,6 +120,9 @@ class Scanner extends Component } $this->syncLegacyState(); + if ($this->config['cache_enabled'] && $scopePath !== null && $cacheFile !== null) { + $this->saveToCache($cacheFile); + } return $this->changeSet; } @@ -236,22 +246,45 @@ class Scanner extends Component { $this->optimizeWithOpcache($path); - $before = get_declared_classes(); - require_once $path; - $after = get_declared_classes(); + self::$currentFile = $path; + $classes = $this->parseDeclaredClasses($path); + $loadFile = $classes === [] ? (bool)$this->config['require_files_without_classes'] : false; - $classes = array_values(array_diff($after, $before)); foreach ($classes as $class) { - if (class_exists($class)) { - $this->analyzeClass($class); + if (!$this->isDeclaredSymbol($class)) { + $loadFile = true; + break; } } - return $classes; - } + try { + if ($loadFile) { + $before = $this->getDeclaredSymbols(); + require_once $path; + $after = $this->getDeclaredSymbols(); + $classes = array_values(array_unique(array_merge( + $classes, + array_diff($after, $before) + ))); + } + foreach ($classes as $class) { + if (class_exists($class)) { + $this->analyzeClass($class); + } + } + + return $classes; + } finally { + self::$currentFile = null; + } + } private function optimizeWithOpcache(string $path): void { + if (!$this->config['opcache_compile']) { + return; + } + if ($this->hasOpcache === null) { $this->hasOpcache = function_exists('opcache_invalidate') && function_exists('opcache_compile_file'); } @@ -273,6 +306,110 @@ class Scanner extends Component } } + private function parseDeclaredClasses(string $path): array + { + $tokens = token_get_all(file_get_contents($path) ?: ''); + $namespace = ''; + $classes = []; + $count = count($tokens); + + for ($i = 0; $i < $count; $i++) { + $token = $tokens[$i]; + if (!is_array($token)) { + continue; + } + + if ($token[0] === T_NAMESPACE) { + $namespace = $this->parseNamespace($tokens, $i + 1); + continue; + } + + if (!$this->isClassLikeToken($token[0])) { + continue; + } + + if ($token[0] === T_CLASS && $this->isAnonymousClass($tokens, $i)) { + continue; + } + + $name = $this->parseClassLikeName($tokens, $i + 1); + if ($name === null) { + continue; + } + + $classes[] = ltrim($namespace . '\\' . $name, '\\'); + } + + return array_values(array_unique($classes)); + } + + private function parseNamespace(array $tokens, int $offset): string + { + $namespace = ''; + $count = count($tokens); + for ($i = $offset; $i < $count; $i++) { + $token = $tokens[$i]; + if ($token === ';' || $token === '{') { + break; + } + if (is_array($token) && in_array($token[0], [T_STRING, T_NAME_QUALIFIED, T_NS_SEPARATOR], true)) { + $namespace .= $token[1]; + } + } + + return trim($namespace, '\\'); + } + + private function parseClassLikeName(array $tokens, int $offset): ?string + { + $count = count($tokens); + for ($i = $offset; $i < $count; $i++) { + $token = $tokens[$i]; + if (!is_array($token)) { + continue; + } + if ($token[0] === T_STRING) { + return $token[1]; + } + } + + return null; + } + + private function isClassLikeToken(int $token): bool + { + return $token === T_CLASS + || $token === T_INTERFACE + || $token === T_TRAIT + || (defined('T_ENUM') && $token === T_ENUM); + } + + private function isAnonymousClass(array $tokens, int $offset): bool + { + for ($i = $offset - 1; $i >= 0; $i--) { + $token = $tokens[$i]; + if (is_array($token) && in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) { + continue; + } + + return is_array($token) && $token[0] === T_NEW; + } + + return false; + } + + private function getDeclaredSymbols(): array + { + return array_merge(get_declared_classes(), get_declared_interfaces(), get_declared_traits()); + } + + private function isDeclaredSymbol(string $class): bool + { + return class_exists($class, false) + || interface_exists($class, false) + || trait_exists($class, false) + || (function_exists('enum_exists') && enum_exists($class, false)); + } private function analyzeClass(string $class): void { try { @@ -493,6 +630,11 @@ class Scanner extends Component $this->visitedFiles = []; } + public static function getCurrentFile(): ?string + { + return self::$currentFile; + } + private function detectRemovedFiles(string $path): void { $prefix = rtrim($path, '/\\') . '/';