This commit is contained in:
2026-07-07 15:09:38 +08:00
parent b08fdb1801
commit e1a23090d6
+150 -8
View File
@@ -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, '/\\') . '/';