TypePHP 编译器
https://swoole.com/aot/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
190 lines
5.6 KiB
190 lines
5.6 KiB
<?php
|
|
/**
|
|
* This file is part of TypePHP.
|
|
*
|
|
* @link https://www.swoole.com/
|
|
* @contact service@swoole.com
|
|
*/
|
|
|
|
namespace TypePhp\Build;
|
|
|
|
class FileScanner
|
|
{
|
|
public const array PHP_EXT = ['php'];
|
|
|
|
public const array CPP_EXT = ['cpp', 'cxx', 'cc'];
|
|
|
|
public const array C_EXT = ['c'];
|
|
|
|
public const array ASM_EXT = ['s', 'S'];
|
|
|
|
public const array OBJC_EXT = ['m'];
|
|
|
|
public const array OBJCXX_EXT = ['mm'];
|
|
|
|
public const array NATIVE_SRC_EXT = ['cpp', 'cxx', 'cc', 'c', 's', 'S', 'm', 'mm'];
|
|
|
|
private string $directory;
|
|
private array $excludePatterns;
|
|
|
|
public function __construct(string $directory)
|
|
{
|
|
if (!is_dir($directory)) {
|
|
throw new \InvalidArgumentException("Directory does not exist: {$directory}");
|
|
}
|
|
|
|
$this->directory = rtrim($directory, DIRECTORY_SEPARATOR);
|
|
$this->excludePatterns = [];
|
|
}
|
|
|
|
public static function getFileName(string $path): string
|
|
{
|
|
return pathinfo($path, PATHINFO_FILENAME);
|
|
}
|
|
|
|
public static function getFileExt(string $path): string
|
|
{
|
|
return pathinfo($path, PATHINFO_EXTENSION);
|
|
}
|
|
|
|
public static function isPhpFile(string $file): bool
|
|
{
|
|
return in_array(self::getFileExt($file), self::PHP_EXT);
|
|
}
|
|
|
|
public static function isCppFile(string $file): bool
|
|
{
|
|
return in_array(self::getFileExt($file), self::CPP_EXT);
|
|
}
|
|
|
|
public static function isNativeSourceFile(string $file): bool
|
|
{
|
|
return in_array(self::getFileExt($file), self::NATIVE_SRC_EXT);
|
|
}
|
|
|
|
public function addExcludePattern(string $pattern): self
|
|
{
|
|
$this->excludePatterns[] = $pattern;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function setExcludePatterns(array $patterns): self
|
|
{
|
|
$this->excludePatterns = $patterns;
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function getDirectory(): string
|
|
{
|
|
return $this->directory;
|
|
}
|
|
|
|
public function scan(): array
|
|
{
|
|
$files = [];
|
|
$iterator = new \RecursiveIteratorIterator($this->createDirectoryIterator());
|
|
|
|
foreach ($iterator as $file) {
|
|
if ($file->isFile()) {
|
|
$filePath = $file->getPathname();
|
|
if (!self::isPhpFile($filePath) && !self::isNativeSourceFile($filePath)) {
|
|
continue;
|
|
}
|
|
if (!$this->isExcluded($filePath)) {
|
|
$files[] = $filePath;
|
|
}
|
|
}
|
|
}
|
|
|
|
// RecursiveDirectoryIterator follows filesystem directory-entry order,
|
|
// which differs between a fresh checkout and a long-lived worktree.
|
|
// Preparation allocates symbol-cache IDs while visiting this list, so
|
|
// keep both generated code and cache classification deterministic.
|
|
sort($files, SORT_STRING);
|
|
|
|
// Keep aliases until the project-level ignore rules have run. If two
|
|
// links reach the same file and only one is ignored, removing aliases
|
|
// here would also remove the allowed path. The source pipeline performs
|
|
// real-path deduplication after filtering.
|
|
return $files;
|
|
}
|
|
|
|
/**
|
|
* Descend into symlinked directories.
|
|
*
|
|
* RecursiveDirectoryIterator does not follow them unless asked to:
|
|
* RecursiveIteratorIterator calls hasChildren(), whose $allowLinks argument
|
|
* defaults to false. Composer path repositories install a package as a
|
|
* symlink, so without FOLLOW_SYMLINKS a source directory that lives behind
|
|
* one contributes no files at all.
|
|
*/
|
|
private function createDirectoryIterator(): \RecursiveIterator
|
|
{
|
|
$iterator = new \RecursiveDirectoryIterator(
|
|
$this->directory,
|
|
\FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS,
|
|
);
|
|
|
|
return new \RecursiveCallbackFilterIterator(
|
|
$iterator,
|
|
fn(\SplFileInfo $entry): bool => !$this->closesLoop($entry),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Whether descending into an entry would repeat the branch that reached it,
|
|
* which is what a link pointing at one of its own ancestors does. Only a
|
|
* link can: every other directory resolves below the one holding it.
|
|
*
|
|
* The branch is the whole test, never a set of everything visited so far.
|
|
* Two links to one directory are two ordinary source paths, and either of
|
|
* them may be the one a project excludes, so which of the two is the same
|
|
* file is decided after exclusions instead, by real path.
|
|
*/
|
|
private function closesLoop(\SplFileInfo $entry): bool
|
|
{
|
|
if (!$entry->isLink() || !$entry->isDir()) {
|
|
return false;
|
|
}
|
|
|
|
$target = $entry->getRealPath();
|
|
if ($target === false) {
|
|
return false;
|
|
}
|
|
|
|
$ancestor = dirname($entry->getPathname());
|
|
while (true) {
|
|
if (realpath($ancestor) === $target) {
|
|
return true;
|
|
}
|
|
if ($ancestor === $this->directory) {
|
|
return false;
|
|
}
|
|
$parent = dirname($ancestor);
|
|
if ($parent === $ancestor) {
|
|
return false;
|
|
}
|
|
$ancestor = $parent;
|
|
}
|
|
}
|
|
|
|
private function isExcluded(string $filePath): bool
|
|
{
|
|
$excluded = false;
|
|
foreach ($this->excludePatterns as $pattern) {
|
|
if ($this->matchPattern($pattern, $filePath)) {
|
|
$excluded = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $excluded;
|
|
}
|
|
|
|
private function matchPattern(string $pattern, string $path): bool
|
|
{
|
|
return fnmatch($pattern, $path, FNM_PATHNAME);
|
|
}
|
|
}
|
|
|