eee
This commit is contained in:
+150
-8
@@ -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, '/\\') . '/';
|
||||
|
||||
Reference in New Issue
Block a user