This commit is contained in:
2026-07-07 17:00:04 +08:00
parent 8d340a8a45
commit 2753a42d82
+254 -9
View File
@@ -89,7 +89,7 @@ class Scanner extends Component
return $this->changeSet;
}
public function scanFiles(array $files, ?string $scopePath = null, ?string $cacheFile = null): ChangeSet
public function scanFiles(array $files, ?string $scopePath = null, ?string $cacheFile = null, bool $force = false): ChangeSet
{
$this->changeSet = new ChangeSet();
$this->visitedFiles = [];
@@ -108,7 +108,7 @@ class Scanner extends Component
foreach (array_values(array_unique($files)) as $file) {
$path = $this->normalizePath($file);
if (file_exists($path)) {
$this->processFile($path);
$this->processFile($path, $force);
continue;
}
@@ -127,6 +127,66 @@ class Scanner extends Component
return $this->changeSet;
}
public function expandDependentFiles(array $files, ?string $scopePath = null): array
{
$expanded = [];
foreach (array_values(array_unique($files)) as $file) {
$path = $this->normalizePath($file);
$expanded[$path] = true;
}
if ($scopePath === null || $expanded === []) {
return array_keys($expanded);
}
$scopePath = $this->normalizePath($scopePath);
$scopeFiles = $this->collectFiles($scopePath);
$symbols = [];
foreach (array_keys($expanded) as $file) {
if (!file_exists($file)) {
continue;
}
foreach ($this->parseDeclaredSymbols($file) as $class => $kind) {
$symbols[$class] = $kind;
}
}
if ($symbols === []) {
return array_keys($expanded);
}
$processedSymbols = [];
do {
$newSymbols = array_diff_key($symbols, $processedSymbols);
if ($newSymbols === []) {
break;
}
$processedSymbols += $newSymbols;
$changed = false;
foreach ($scopeFiles as $file) {
$file = $this->normalizePath($file);
if (isset($expanded[$file]) || !file_exists($file)) {
continue;
}
$dependencies = $this->parseFileDependencies($file);
if (array_intersect_key($dependencies, $symbols) === []) {
continue;
}
$expanded[$file] = true;
foreach ($this->parseDeclaredSymbols($file) as $class => $kind) {
if (!isset($symbols[$class])) {
$symbols[$class] = $kind;
$changed = true;
}
}
}
} while ($changed);
return array_keys($expanded);
}
private function scanDirectory(string $path, int $depth = 0): void
{
if ($depth > $this->config['max_depth']) {
@@ -191,7 +251,7 @@ class Scanner extends Component
return false;
}
private function processFile(string $path): void
private function processFile(string $path, bool $force = false): void
{
$path = $this->normalizePath($path);
if (!in_array(pathinfo($path, PATHINFO_EXTENSION), $this->config['extensions'], true)) {
@@ -199,7 +259,7 @@ class Scanner extends Component
}
$this->visitedFiles[$path] = true;
if (!$this->shouldProcessFile($path)) {
if (!$force && !$this->shouldProcessFile($path)) {
return;
}
@@ -247,7 +307,7 @@ class Scanner extends Component
$this->optimizeWithOpcache($path);
self::$currentFile = $path;
$classes = $this->parseDeclaredClasses($path);
$classes = array_keys($this->parseDeclaredSymbols($path));
$loadFile = $classes === [] ? (bool)$this->config['require_files_without_classes'] : false;
foreach ($classes as $class) {
@@ -305,7 +365,7 @@ class Scanner extends Component
}
}
private function parseDeclaredClasses(string $path): array
private function parseDeclaredSymbols(string $path): array
{
$tokens = token_get_all(file_get_contents($path) ?: '');
$namespace = '';
@@ -336,10 +396,10 @@ class Scanner extends Component
continue;
}
$classes[] = ltrim($namespace . '\\' . $name, '\\');
$classes[ltrim($namespace . '\\' . $name, '\\')] = $this->getClassLikeKind($tokens, $i);
}
return array_values(array_unique($classes));
return $classes;
}
private function parseNamespace(array $tokens, int $offset): string
@@ -410,6 +470,190 @@ class Scanner extends Component
return false;
}
private function parseFileDependencies(string $path): array
{
$tokens = token_get_all(file_get_contents($path) ?: '');
$namespace = '';
$imports = [];
$dependencies = [];
$count = count($tokens);
$braceDepth = 0;
for ($i = 0; $i < $count; $i++) {
$token = $tokens[$i];
if ($token === '{') {
$braceDepth++;
continue;
}
if ($token === '}') {
$braceDepth = max(0, $braceDepth - 1);
continue;
}
if (!is_array($token)) {
continue;
}
if ($token[0] === T_NAMESPACE) {
$namespace = $this->parseNamespace($tokens, $i + 1);
continue;
}
if ($token[0] === T_USE) {
if ($braceDepth === 0) {
foreach ($this->parseUseImports($tokens, $i + 1, $namespace) as $alias => $class) {
$imports[$alias] = $class;
}
continue;
}
foreach ($this->parseNameList($tokens, $i + 1, [';', '{']) as $name) {
$dependencies[$this->resolveClassName($name, $namespace, $imports)] = true;
}
continue;
}
if ($token[0] === T_EXTENDS || $token[0] === T_IMPLEMENTS) {
foreach ($this->parseNameList($tokens, $i + 1, ['{']) as $name) {
$dependencies[$this->resolveClassName($name, $namespace, $imports)] = true;
}
}
}
return $dependencies;
}
private function parseUseImports(array $tokens, int $offset, string $namespace): array
{
$imports = [];
$count = count($tokens);
for ($i = $offset; $i < $count; $i++) {
$token = $tokens[$i];
if ($token === ';') {
break;
}
if (is_array($token) && in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
continue;
}
if (is_array($token) && in_array($token[0], [T_FUNCTION, T_CONST], true)) {
return [];
}
$name = $this->collectQualifiedName($tokens, $i);
if ($name === null) {
continue;
}
$alias = null;
$j = $i + 1;
while ($j < $count) {
$next = $tokens[$j];
if ($next === ',' || $next === ';') {
break;
}
if (is_array($next) && $next[0] === T_AS) {
$aliasOffset = $j + 1;
$alias = $this->collectQualifiedName($tokens, $aliasOffset);
break;
}
$j++;
}
$class = trim($name, '\\');
$short = $alias ?: basename(str_replace('\\', '/', $class));
$imports[strtolower($short)] = $class;
$i = $j;
}
return $imports;
}
private function parseNameList(array $tokens, int $offset, array $stoppers): array
{
$names = [];
$count = count($tokens);
for ($i = $offset; $i < $count; $i++) {
$token = $tokens[$i];
if (is_string($token) && in_array($token, $stoppers, true)) {
break;
}
$name = $this->collectQualifiedName($tokens, $i);
if ($name !== null) {
$names[] = $name;
}
}
return $names;
}
private function collectQualifiedName(array $tokens, int &$offset): ?string
{
$count = count($tokens);
$name = '';
$started = false;
for ($i = $offset; $i < $count; $i++) {
$token = $tokens[$i];
if (!$started && is_array($token) && in_array($token[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
continue;
}
if (is_array($token) && in_array($token[0], [T_STRING, T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED, T_NS_SEPARATOR], true)) {
$name .= $token[1];
$started = true;
$offset = $i;
continue;
}
break;
}
return $name === '' ? null : $name;
}
private function resolveClassName(string $name, string $namespace, array $imports): string
{
$name = trim($name, '\\');
if ($name === '') {
return $name;
}
$parts = explode('\\', $name);
$first = strtolower($parts[0]);
if (isset($imports[$first])) {
array_shift($parts);
return $imports[$first] . ($parts === [] ? '' : '\\' . implode('\\', $parts));
}
return $namespace === '' ? $name : $namespace . '\\' . $name;
}
private function getClassLikeKind(array $tokens, int $offset): string
{
$token = $tokens[$offset];
if (!is_array($token)) {
return 'class';
}
if ($token[0] === T_TRAIT) {
return 'trait';
}
if ($token[0] === T_INTERFACE) {
return 'interface';
}
if (defined('T_ENUM') && $token[0] === T_ENUM) {
return 'enum';
}
for ($i = $offset - 1; $i >= 0; $i--) {
$previous = $tokens[$i];
if (is_array($previous) && in_array($previous[0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
continue;
}
return is_array($previous) && $previous[0] === T_ABSTRACT ? 'abstract_class' : 'class';
}
return 'class';
}
private function getDeclaredSymbols(): array
{
return array_merge(get_declared_classes(), get_declared_interfaces(), get_declared_traits());
@@ -454,7 +698,8 @@ class Scanner extends Component
private function analyzeClassMethods(ReflectionClass $reflect, string $class): void
{
foreach ($reflect->getMethods() as $method) {
if ($method->isStatic() || $method->getDeclaringClass()->getName() !== $class) {
$declaringClass = $method->getDeclaringClass();
if ($method->isStatic() || ($declaringClass->getName() !== $class && !$declaringClass->isTrait())) {
continue;
}