diff --git a/phpunit/src/NativePropertyTest.php b/phpunit/src/NativePropertyTest.php index 9febc6f4..10b0fda6 100644 --- a/phpunit/src/NativePropertyTest.php +++ b/phpunit/src/NativePropertyTest.php @@ -38,8 +38,10 @@ class NativePropertyTest extends \BaseTest } $code = file_get_contents($outputFile); - $this->assertStringContainsString('php::getStaticProperty(php_get_called_class(this_), "count")', $code); - $this->assertStringContainsString('php::getStaticProperty(php_get_called_class(this_), "count") = php::toInt(value)', $code); + $this->assertStringContainsString('tmp_var_0 = php_get_called_class(this_);', $code); + $this->assertStringContainsString('php::getStaticProperty((tmp_var_0.isObject()', $code); + $this->assertStringContainsString('php::getStaticProperty((tmp_var_0.isObject() ? php::fn::get_class(tmp_var_0)', $code); + $this->assertStringContainsString('= php::toInt(value);', $code); } public function testNativeIntPropertyAssignOpUsesNativeReference(): void diff --git a/phpunit/src/PreprocessorTest.php b/phpunit/src/PreprocessorTest.php index 68b5c8fe..cf25e4c7 100644 --- a/phpunit/src/PreprocessorTest.php +++ b/phpunit/src/PreprocessorTest.php @@ -56,6 +56,17 @@ class PreprocessorTest extends TestCase private function setProperty(string $name, mixed $value): void { + if (in_array($name, ['classes', 'interfaces', 'functions', 'classExtends'], true)) { + $symbols = $this->getProperty('symbols'); + $method = match ($name) { + 'classes' => 'replaceClasses', + 'interfaces' => 'replaceInterfaces', + 'functions' => 'replaceFunctions', + 'classExtends' => 'replaceParents', + }; + $symbols->{$method}($value); + return; + } $prop = $this->ref->getProperty($name); $prop->setAccessible(true); $prop->setValue($this->compiler, $value); @@ -63,6 +74,10 @@ class PreprocessorTest extends TestCase private function getProperty(string $name): mixed { + if (in_array($name, ['classes', 'interfaces', 'functions'], true)) { + $symbols = $this->getProperty('symbols'); + return $symbols->{$name}(); + } $prop = $this->ref->getProperty($name); $prop->setAccessible(true); return $prop->getValue($this->compiler); diff --git a/phpunit/src/SsaAnalysisTest.php b/phpunit/src/SsaAnalysisTest.php index 2c731b39..8e87baa8 100644 --- a/phpunit/src/SsaAnalysisTest.php +++ b/phpunit/src/SsaAnalysisTest.php @@ -69,6 +69,12 @@ class SsaAnalysisTest extends TestCase private function setProperty(string $name, mixed $value): void { + if ($name === 'classes') { + $prop = $this->ref->getProperty('symbols'); + $prop->setAccessible(true); + $prop->getValue($this->compiler)->replaceClasses($value); + return; + } $prop = $this->ref->getProperty($name); $prop->setAccessible(true); $prop->setValue($this->compiler, $value); diff --git a/src/Build/CommandOptions.php b/src/Build/CommandOptions.php new file mode 100644 index 00000000..9b3ce472 --- /dev/null +++ b/src/Build/CommandOptions.php @@ -0,0 +1,20 @@ + */ +abstract class CommandOptions implements ArrayAccess +{ + final public function __construct(protected readonly array $values) + { + } + + public function toArray(): array { return $this->values; } + public function offsetExists(mixed $offset): bool { return isset($this->values[$offset]); } + public function offsetGet(mixed $offset): mixed { return $this->values[$offset] ?? null; } + public function offsetSet(mixed $offset, mixed $value): never { throw new LogicException('Command options are immutable'); } + public function offsetUnset(mixed $offset): never { throw new LogicException('Command options are immutable'); } +} diff --git a/src/Build/CompileOptions.php b/src/Build/CompileOptions.php new file mode 100644 index 00000000..f45ce0a0 --- /dev/null +++ b/src/Build/CompileOptions.php @@ -0,0 +1,13 @@ +values; + $values[$name] = $value; + return new self($values); + } +} diff --git a/src/Build/LinkOptions.php b/src/Build/LinkOptions.php new file mode 100644 index 00000000..10167e5a --- /dev/null +++ b/src/Build/LinkOptions.php @@ -0,0 +1,7 @@ +backend->buildCompileCommand($source, $object, $options->toArray()); + } + if ($language === 'c') { + return $this->backend->buildCCompileCommand($source, $object, $options->toArray()); + } + return $this->backend->buildNativeCompileCommand($source, $object, $options->toArray(), $language); + } + + public function linkCommand(array $objects, string $target, LinkOptions $options): string + { + return $this->backend->buildLinkCommand($objects, $target, $options->toArray()); + } + + /** @return array{command: string, output: list, status: int} */ + public function compile(string $source, string $object, CompileOptions $options, ?string $language, bool $quiet): array + { + $command = $this->compileCommand($source, $object, $options, $language); + $output = []; + if ($quiet) { + exec($command . ' 2>&1', $output, $status); + } else { + passthru($command, $status); + } + return ['command' => $command, 'output' => $output, 'status' => $status]; + } + + /** @return array{command: string, output: list, status: int, generated: bool} */ + public function link(array $objects, string $target, LinkOptions $options): array + { + $command = $this->linkCommand($objects, $target, $options); + try { + exec($command . ' 2>&1', $output, $status); + return [ + 'command' => $command, + 'output' => $output, + 'status' => $status, + 'generated' => file_exists($target), + ]; + } finally { + $this->cleanup(); + } + } + + /** + * @param Closure(string): string $objectFile + * @param Closure(string, string): void $worker + * @param Closure(): int $fork + * @param Closure(): array{int, int} $wait + * @param Closure(int): bool $succeeded + * @return array{objects: list, failures: list} + */ + public function dispatchParallel( + array $sources, + int $jobs, + Closure $objectFile, + Closure $worker, + Closure $fork, + Closure $wait, + Closure $succeeded, + ): array { + $queue = array_values($sources); + $running = []; + $objects = []; + $failures = []; + + while ($queue !== [] || $running !== []) { + while (count($running) < $jobs && $queue !== []) { + $source = array_shift($queue); + $object = $objectFile($source); + $pid = $fork(); + if ($pid === -1) { + $failures[] = $source; + array_push($failures, ...$queue); + $queue = []; + break; + } + if ($pid === 0) { + try { + $worker($source, $object); + exit(is_file($object) ? 0 : 1); + } catch (\Throwable) { + exit(1); + } + } + $running[$pid] = ['source' => $source, 'object' => $object]; + } + if ($running === []) { + break; + } + [$pid, $status] = $wait(); + $task = $running[$pid] ?? null; + unset($running[$pid]); + if ($task === null) { + continue; + } + if ($succeeded($status)) { + $objects[] = $task['object']; + } else { + $failures[] = $task['source']; + } + } + return ['objects' => $objects, 'failures' => $failures]; + } + + public function cleanup(): void + { + $this->backend->cleanupResponseFile(); + } +} diff --git a/src/Build/NativeCommandOptionsTrait.php b/src/Build/NativeCommandOptionsTrait.php index 7cea8bc3..a65baf81 100644 --- a/src/Build/NativeCommandOptionsTrait.php +++ b/src/Build/NativeCommandOptionsTrait.php @@ -12,7 +12,7 @@ use TypePhp\Metadata\Constants; trait NativeCommandOptionsTrait { - protected function getCommonCompileCommandOptions(): array + protected function getCommonCompileCommandOptions(): CompileOptions { $includePaths = $this->getIncludePaths(); if (!empty($this->userIncludePaths)) { @@ -24,7 +24,7 @@ trait NativeCommandOptionsTrait $userDefines[] = 'TYPEPHP_NO_MAIN=1'; } - return [ + return new CompileOptions([ 'include_paths' => $includePaths, 'optimize' => $this->optimizeLevel, 'debug' => $this->debug, @@ -37,39 +37,37 @@ trait NativeCommandOptionsTrait 'prof_output' => $this->targetName . '.prof', 'user_defines' => $userDefines, 'lto' => $this->enableLto, - ]; + ]); } - protected function getCompileCommandOptions(): array + protected function getCompileCommandOptions(): CompileOptions { $options = $this->getCommonCompileCommandOptions(); - $options['cpp_std'] = $this->cxxStd; - $options['cxxflags'] = $this->cxxFlags; - $options['suppressed_warnings'] = Constants::MSVC_SUPPRESSED_WARNINGS ?? []; - return $options; + return $options + ->with('cpp_std', $this->cxxStd) + ->with('cxxflags', $this->cxxFlags) + ->with('suppressed_warnings', Constants::MSVC_SUPPRESSED_WARNINGS ?? []); } - protected function getCCompileCommandOptions(): array + protected function getCCompileCommandOptions(): CompileOptions { $options = $this->getCommonCompileCommandOptions(); - $options['suppressed_warnings'] = ['4244', '4146']; - return $options; + return $options->with('suppressed_warnings', ['4244', '4146']); } - protected function getNativeCompileCommandOptions(string $language = ''): array + protected function getNativeCompileCommandOptions(string $language = ''): CompileOptions { $options = $this->getCommonCompileCommandOptions(); - $options['suppressed_warnings'] = Constants::MSVC_SUPPRESSED_WARNINGS ?? []; + $options = $options->with('suppressed_warnings', Constants::MSVC_SUPPRESSED_WARNINGS ?? []); if ($language === 'objective-c++') { - $options['cpp_std'] = $this->cxxStd; - $options['cxxflags'] = $this->cxxFlags; + $options = $options->with('cpp_std', $this->cxxStd)->with('cxxflags', $this->cxxFlags); } return $options; } - protected function getLinkCommandOptions(): array + protected function getLinkCommandOptions(): LinkOptions { $libraryPaths = array_merge($this->getLibraryPaths(), $this->linkPaths); $libraries = $this->getLibraries(); @@ -95,6 +93,6 @@ trait NativeCommandOptionsTrait $options['rpath'] = $rpaths; } - return $options; + return new LinkOptions($options); } } diff --git a/src/CompilerBase.php b/src/CompilerBase.php index bd647ee2..04677ab2 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -16,6 +16,9 @@ use TypePhp\Entity\ArgInfo; use TypePhp\Context\FunctionContext; use TypePhp\Context\CompilationStateTrait; use TypePhp\Diagnostics\CompilerDiagnosticTrait; +use TypePhp\Diagnostics\CliDiagnosticReporter; +use TypePhp\Diagnostics\DiagnosticReporter; +use TypePhp\Diagnostics\ThrowingDiagnosticReporter; use TypePhp\Entity\ClassDef; use TypePhp\Entity\ConstantDef; use TypePhp\Entity\FunctionDef; @@ -77,6 +80,7 @@ use TypePhp\Resolver\PropertyWriteTarget; use TypePhp\Resolver\Reflection; use TypePhp\Resolver\StaticPropertyFetchResolution; use TypePhp\Resolver\StaticPropertyFetchTarget; +use TypePhp\Symbol\SymbolRepository; use TypePhp\TypeSystem\CompositeTypeCheckerTrait; use TypePhp\TypeSystem\NativeTypeCompatibilityTrait; use PhpParser\Modifiers; @@ -360,19 +364,16 @@ class CompilerBase implements PropertyAccessContext /** * @var array */ - protected array $interfaces = []; /** * 存储所有函数、类方法的定义,key 是 native name,命名空间需要转为 `_`,并且必须为小写 * @var array */ - protected array $functions = []; /** * key 类名,包含命名空间 * @var array */ - protected array $classes = []; /** * @var array @@ -404,6 +405,7 @@ class CompilerBase implements PropertyAccessContext protected ?MethodDef $methodDef = null; protected ?InterfaceDef $interfaceDef = null; protected bool $inGeneratorBody = false; + private ?DiagnosticReporter $diagnosticReporter = null; protected FunctionContext $context; protected array $superGlobalVars = [ '_GET' => self::TYPE_ARRAY, @@ -454,7 +456,7 @@ class CompilerBase implements PropertyAccessContext * 存储所有类继承关系,类名必须全部为小写 * @var array */ - protected array $classExtends = []; + protected SymbolRepository $symbols; /** * Reverse class hierarchy: parent class (lowercase) => list of child classes (lowercase) @@ -472,6 +474,7 @@ class CompilerBase implements PropertyAccessContext $this->error('PHP 8.6.0 or later is not supported'); } $this->rootPath = $rootPath; + $this->symbols = new SymbolRepository(); $this->setPhpVersion(self::DEFAULT_PHP_VERSION); $this->printer = new PrettyPrinter\Standard(); $this->setBuildDir($rootPath . '/build'); @@ -479,6 +482,21 @@ class CompilerBase implements PropertyAccessContext $this->climate = $climate; } + public function setDiagnosticReporter(DiagnosticReporter $reporter): void + { + $this->diagnosticReporter = $reporter; + } + + protected function getDiagnosticReporter(): DiagnosticReporter + { + if ($this->diagnosticReporter !== null) { + return $this->diagnosticReporter; + } + return $this->forTest + ? new ThrowingDiagnosticReporter() + : new CliDiagnosticReporter($this->climate, $this->printBacktraceOnError); + } + public function setMode($mode): void { $this->mode = $mode; diff --git a/src/Config/ProjectYamlLoader.php b/src/Config/ProjectYamlLoader.php new file mode 100644 index 00000000..69563d57 --- /dev/null +++ b/src/Config/ProjectYamlLoader.php @@ -0,0 +1,140 @@ +phpVersion = $phpVersion; + } + + public function load(string $path): array + { + $config = Yaml::parseFile($path); + if (!is_array($config)) { + ($this->error)('Project YAML root must be a map'); + } + return $config; + } + + /** @return array{0: string, 1: string|null} */ + public function parseSourceEntry(mixed $entry): array + { + if (is_string($entry)) { + return [$entry, null]; + } + if (!is_array($entry)) { + ($this->error)('Each `sources` entry must be a string or map'); + } + $path = $entry['path'] ?? $entry['source'] ?? $entry['file'] ?? null; + if (!is_string($path) || trim($path) === '') { + ($this->error)('Conditional `sources` entries must include a non-empty `path`'); + } + $condition = $entry['if'] ?? $entry['when'] ?? null; + if ($condition !== null && !is_string($condition)) { + ($this->error)('Source condition must be a string'); + } + return [$path, $condition]; + } + + public function evaluateCondition(string $condition): bool + { + $condition = trim($condition); + if ($condition === '') { + ($this->error)('Source condition must not be empty'); + } + $expr = $this->replacePhpVersionComparisons($condition); + $expr = $this->replaceOsFamilyComparisons($expr, $condition); + if (preg_match('/[A-Za-z_]/', $expr) + || !preg_match('/^[0-9\s<>=!&|().+-]+$/', $expr) + || preg_match('/(?error)('Unsupported source condition: `' . $condition . '`'); + } + try { + return (bool) eval('return (' . $expr . ');'); + } catch (\Throwable) { + ($this->error)('Invalid source condition: `' . $condition . '`'); + } + } + + private function replacePhpVersionComparisons(string $condition): string + { + $literal = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\''; + $operator = '(>=|<=|==|!=|<>|=|>|<|lt|le|gt|ge|eq|ne)'; + $patterns = [ + '/\bPHP_VERSION_ID\b\s*' . $operator . '\s*([0-9]+)/i' => fn(array $m): bool => version_compare($this->phpVersion, $this->versionIdToString((int) $m[2]), strtolower($m[1])), + '/([0-9]+)\s*' . $operator . '\s*\bPHP_VERSION_ID\b/i' => fn(array $m): bool => version_compare($this->versionIdToString((int) $m[1]), $this->phpVersion, strtolower($m[2])), + '/\bPHP_VERSION\b\s*' . $operator . '\s*(' . $literal . ')/i' => function (array $m): bool { + $version = stripcslashes(($m[3] ?? '') !== '' ? $m[3] : $m[4]); + $this->assertVersion($version); + return version_compare($this->phpVersion, $version, strtolower($m[1])); + }, + '/(' . $literal . ')\s*' . $operator . '\s*\bPHP_VERSION\b/i' => function (array $m): bool { + $version = stripcslashes($m[2] !== '' ? $m[2] : $m[3]); + $this->assertVersion($version); + return version_compare($version, $this->phpVersion, strtolower($m[4])); + }, + ]; + $expr = $condition; + foreach ($patterns as $pattern => $compare) { + $expr = preg_replace_callback($pattern, fn(array $m): string => $compare($m) ? '1' : '0', $expr); + if ($expr === null) { + ($this->error)('Invalid source condition: `' . $condition . '`'); + } + } + if (preg_match('/\bPHP_VERSION(?:_ID)?\b/', $expr)) { + ($this->error)('Unsupported source condition: `' . $condition . '`'); + } + return $expr; + } + + private function replaceOsFamilyComparisons(string $expr, string $condition): string + { + $literal = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\''; + $expr = preg_replace_callback('/\bPHP_OS_FAMILY\b\s*(==|!=)\s*(' . $literal . ')/i', function (array $m): string { + $expected = stripcslashes(($m[3] ?? '') !== '' ? $m[3] : $m[4]); + $this->assertOsFamily($expected); + return (($this->osFamily === $expected) xor ($m[1] === '!=')) ? '1' : '0'; + }, $expr); + $expr = preg_replace_callback('/(' . $literal . ')\s*(==|!=)\s*\bPHP_OS_FAMILY\b/i', function (array $m): string { + $expected = stripcslashes($m[2] !== '' ? $m[2] : $m[3]); + $this->assertOsFamily($expected); + return (($expected === $this->osFamily) xor ($m[4] === '!=')) ? '1' : '0'; + }, $expr ?? ''); + if ($expr === null || preg_match('/\bPHP_OS_FAMILY\b/', $expr)) { + ($this->error)('Unsupported source condition: `' . $condition . '`'); + } + return $expr; + } + + private function assertVersion(string $version): void + { + if ($version === '' || !preg_match('/^[0-9A-Za-z_.+\-]+$/', $version)) { + ($this->error)('Invalid PHP_VERSION literal: `' . $version . '`'); + } + } + + private function assertOsFamily(string $osFamily): void + { + if (!in_array($osFamily, ['Windows', 'BSD', 'Darwin', 'Solaris', 'Linux', 'Unknown'], true)) { + ($this->error)('Invalid PHP_OS_FAMILY literal: `' . $osFamily . '`'); + } + } + + private function versionIdToString(int $versionId): string + { + return intdiv($versionId, 10000) . '.' . intdiv($versionId % 10000, 100) . '.' . ($versionId % 100); + } +} diff --git a/src/Context/CompilationStateTrait.php b/src/Context/CompilationStateTrait.php index 24b1b1b2..f6156e80 100644 --- a/src/Context/CompilationStateTrait.php +++ b/src/Context/CompilationStateTrait.php @@ -106,7 +106,7 @@ trait CompilationStateTrait protected function addFunction(string $name, FunctionDef $functionDef): void { - $this->functions[$this->escapeFunction($name)] = $functionDef; + $this->symbols->putFunction($this->escapeFunction($name), $functionDef); } /** @@ -114,47 +114,47 @@ trait CompilationStateTrait */ protected function hasFunction(string $name): bool { - return array_key_exists($this->escapeFunction($name), $this->functions); + return $this->symbols->hasFunction($this->escapeFunction($name)); } protected function getFunction(string $name): FunctionDef { - return $this->functions[$this->escapeFunction($name)]; + return $this->symbols->function($this->escapeFunction($name)); } protected function addClass(string $name, ClassDef $classDef): void { - $this->classes[$this->escapeClass($name)] = $classDef; + $this->symbols->putClass($this->escapeClass($name), $classDef); } protected function getClass(string $name): ClassDef { - return $this->classes[$this->escapeClass($name)]; + return $this->symbols->class($this->escapeClass($name)); } public function getClassDef(string $name): ?ClassDef { - return $this->classes[$this->escapeClass($name)] ?? null; + return $this->symbols->findClass($this->escapeClass($name)); } public function getParentClass(string $class): string { - return $this->classExtends[strtolower(ltrim($class, '\\'))] ?? ''; + return $this->symbols->parent(strtolower(ltrim($class, '\\'))); } protected function hasClass(string $name): bool { - return array_key_exists($this->escapeClass($name), $this->classes); + return $this->symbols->hasClass($this->escapeClass($name)); } protected function hasInterface(string $name): bool { - return array_key_exists($this->escapeClass($name), $this->interfaces); + return $this->symbols->hasInterface($this->escapeClass($name)); } protected function getInterface(string $name): InterfaceDef { - return $this->interfaces[$this->escapeClass($name)]; + return $this->symbols->interface($this->escapeClass($name)); } protected function checkFunction(string $name): void diff --git a/src/Diagnostics/CliDiagnosticReporter.php b/src/Diagnostics/CliDiagnosticReporter.php new file mode 100644 index 00000000..b50dc996 --- /dev/null +++ b/src/Diagnostics/CliDiagnosticReporter.php @@ -0,0 +1,29 @@ +climate->red("Fatal error: {$message}"); + if ($this->printBacktrace) { + debug_print_backtrace(); + } + exit(255); + } + + public function warning(Node $node, string $file, string $message): void + { + $this->climate->magenta("{$message} in {$file}:{$node->getStartLine()}"); + } +} diff --git a/src/Diagnostics/CompilerDiagnosticTrait.php b/src/Diagnostics/CompilerDiagnosticTrait.php index fcab5bb2..bc58dfc0 100644 --- a/src/Diagnostics/CompilerDiagnosticTrait.php +++ b/src/Diagnostics/CompilerDiagnosticTrait.php @@ -11,7 +11,6 @@ namespace TypePhp\Diagnostics; use PhpParser\Node; use PhpParser\Node\Expr\Variable; use PhpParser\NodeAbstract; -use TypePhp\Exception\TestError; trait CompilerDiagnosticTrait { @@ -20,15 +19,7 @@ trait CompilerDiagnosticTrait */ public function error(string $msg): never { - if ($this->forTest) { - throw new TestError($msg); - } - - $this->climate->red("Fatal error: {$msg}"); - if ($this->printBacktraceOnError) { - debug_print_backtrace(); - } - exit(255); + $this->getDiagnosticReporter()->fatal($msg); } public function fatalError(NodeAbstract $node, string $msg): never @@ -38,7 +29,7 @@ trait CompilerDiagnosticTrait protected function warning(Node $node, string $msg): void { - $this->climate->magenta("{$msg} in {$this->file}:{$node->getStartLine()}"); + $this->getDiagnosticReporter()->warning($node, $this->file, $msg); } protected function errorUndefinedVariable(Variable $node): never diff --git a/src/Diagnostics/DiagnosticReporter.php b/src/Diagnostics/DiagnosticReporter.php new file mode 100644 index 00000000..2fffd732 --- /dev/null +++ b/src/Diagnostics/DiagnosticReporter.php @@ -0,0 +1,12 @@ +functions as $func) { + foreach ($this->symbols->functions() as $func) { foreach ($func->argInfoList as $argInfo) { $plan = $argInfo->arrayInitPlan; if (!$plan || !$plan->requiresRuntimeInit()) { @@ -69,4 +69,3 @@ trait DefaultArgumentGenerator return $code ? $code . PHP_EOL : ''; } } - diff --git a/src/Optimizer/SsaPropOptimizer.php b/src/Optimizer/SsaPropOptimizer.php index b6a4c58e..c2cde4c4 100644 --- a/src/Optimizer/SsaPropOptimizer.php +++ b/src/Optimizer/SsaPropOptimizer.php @@ -336,7 +336,7 @@ trait SsaPropOptimizer */ protected function isClassSafeForPropHoisting(string $className): bool { - $classDef = $this->classes[$this->escapeClass($className)] ?? null; + $classDef = $this->symbols->findClass($this->escapeClass($className)); if (!$classDef) { return false; } diff --git a/src/Preprocessor.php b/src/Preprocessor.php index e1cd06a2..03dee1b4 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -532,7 +532,7 @@ class Preprocessor extends CompilerBase if ($parentClassLower === $fullClassNameLower) { $this->fatalError($class, "Class {$fullClassName} cannot extend itself"); } - $this->classExtends[$fullClassNameLower] = $parentClassLower; + $this->symbols->setParent($fullClassNameLower, $parentClassLower); $this->classSubClasses[$parentClassLower][] = $fullClassNameLower; if (!$this->isInternalClass($parentClassLower)) { $this->symbolCallInFile[$this->file][] = $parentClassLower; @@ -792,8 +792,7 @@ class Preprocessor extends CompilerBase $this->classMethodOverride[$fullMethodNameLower] = $isOverridden; // 查找父类是否有同名方法,递归向上标记父类方法已被覆盖 - while (isset($this->classExtends[$fullClassNameLower])) { - $parentClass = $this->classExtends[$fullClassNameLower]; + while (($parentClass = $this->symbols->parent($fullClassNameLower)) !== '') { $parentMethodLower = strtolower($parentClass . '::' . $this->method); if (isset($this->classMethodOverride[$parentMethodLower])) { $this->classMethodOverride[$parentMethodLower] = true; @@ -855,7 +854,7 @@ class Preprocessor extends CompilerBase } $this->symbolDeclInFile[$interfaceNameLower] = $this->file; - $this->interfaces[$this->escapeClass($interfaceName)] = $this->interfaceDef; + $this->symbols->putInterface($this->escapeClass($interfaceName), $this->interfaceDef); $this->interfacesDefineInFile[$interfaceName] = $this->interfaceDef; foreach ($v->stmts as $stmt) { diff --git a/src/Symbol/SymbolRepository.php b/src/Symbol/SymbolRepository.php new file mode 100644 index 00000000..56af9386 --- /dev/null +++ b/src/Symbol/SymbolRepository.php @@ -0,0 +1,50 @@ + */ + private array $functions = []; + /** @var array */ + private array $classes = []; + /** @var array */ + private array $interfaces = []; + /** @var array */ + private array $parents = []; + + public function putFunction(string $key, FunctionDef $definition): void { $this->functions[$key] = $definition; } + public function hasFunction(string $key): bool { return array_key_exists($key, $this->functions); } + public function function(string $key): FunctionDef { return $this->functions[$key]; } + /** @return array */ + public function functions(): array { return $this->functions; } + + public function putClass(string $key, ClassDef $definition): void { $this->classes[$key] = $definition; } + public function hasClass(string $key): bool { return array_key_exists($key, $this->classes); } + public function class(string $key): ClassDef { return $this->classes[$key]; } + public function findClass(string $key): ?ClassDef { return $this->classes[$key] ?? null; } + /** @return array */ + public function classes(): array { return $this->classes; } + + public function putInterface(string $key, InterfaceDef $definition): void { $this->interfaces[$key] = $definition; } + public function hasInterface(string $key): bool { return array_key_exists($key, $this->interfaces); } + public function interface(string $key): InterfaceDef { return $this->interfaces[$key]; } + /** @return array */ + public function interfaces(): array { return $this->interfaces; } + + public function setParent(string $class, string $parent): void { $this->parents[$class] = $parent; } + public function parent(string $class): string { return $this->parents[$class] ?? ''; } + + /** @param array $functions */ + public function replaceFunctions(array $functions): void { $this->functions = $functions; } + /** @param array $classes */ + public function replaceClasses(array $classes): void { $this->classes = $classes; } + /** @param array $interfaces */ + public function replaceInterfaces(array $interfaces): void { $this->interfaces = $interfaces; } + /** @param array $parents */ + public function replaceParents(array $parents): void { $this->parents = $parents; } +} diff --git a/src/Translator.php b/src/Translator.php index 7e99e41b..2ae78ab8 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -13,7 +13,9 @@ use TypePhp\Analysis\SsaBuilder; use TypePhp\Backend\CompilerFactory; use TypePhp\Build\FileScanner; use TypePhp\Build\NativeCommandOptionsTrait; +use TypePhp\Build\NativeBuilder; use TypePhp\Build\SourcePipelineTrait; +use TypePhp\Config\ProjectYamlLoader; use TypePhp\Build\ResourceCompilationTrait; use TypePhp\Entity\ArgInfo; use TypePhp\Entity\ClassDef; @@ -41,9 +43,6 @@ use PhpParser\Node\ArrayItem; use PhpParser\Node\Stmt\Foreach_; use PhpParser\NodeAbstract; use PhpParser\NodeTraverser; -use Ajaxray\AnsiKit\AnsiTerminal; -use Ajaxray\AnsiKit\Components\Progressbar; -use Symfony\Component\Yaml\Yaml; class Translator extends Preprocessor { @@ -59,6 +58,8 @@ class Translator extends Preprocessor protected bool $hasExplicitOutput = false; protected ?string $explicitOutputExtension = null; protected array $sourceDirs = []; + private ?ProjectYamlLoader $projectYamlLoader = null; + private ?NativeBuilder $nativeBuilder = null; protected bool $verbose = false; protected array $phpSrcFiles = []; protected array $ignorePaths = []; @@ -801,7 +802,7 @@ CODE; $code .= $this->getIndent() . "PHP_FE(cli_get_process_title, arginfo_cli_get_process_title)\n"; } - foreach ($this->functions as $functionDef) { + foreach ($this->symbols->functions() as $functionDef) { if ($this->isBuildModeExt() and $functionDef->name === self::ENTRY_FUNCTION) { continue; } @@ -852,7 +853,7 @@ CODE; } $code .= '// static property ' . PHP_EOL; - foreach ($this->classes as $classDef) { + foreach ($this->symbols->classes() as $classDef) { foreach ($classDef->properties as $property) { if (!$property->isStatic() || !$property->arrayInitPlan || !$property->default) { continue; @@ -900,7 +901,7 @@ CODE; } // Clean up inherited array constants from child classes - foreach ($this->classes as $className => $classDef) { + foreach ($this->symbols->classes() as $className => $classDef) { $ownConstNames = []; foreach ($classDef->constants as $constant) { if ($constant->type === self::TYPE_ARRAY) { @@ -909,8 +910,8 @@ CODE; } $parentName = $this->escapeClass($classDef->extends); - while ($parentName && isset($this->classes[$parentName])) { - $parentDef = $this->classes[$parentName]; + while ($parentName && $this->symbols->hasClass($parentName)) { + $parentDef = $this->symbols->class($parentName); foreach ($parentDef->constants as $constant) { if ($constant->type === self::TYPE_ARRAY && !isset($ownConstNames[$constant->name])) { $ownConstNames[$constant->name] = true; @@ -955,7 +956,7 @@ CODE; $code .= 'php_app_init();' . PHP_EOL; if ($this->isBuildModeBin()) { - $entryFunction = $this->functions[self::ENTRY_FUNCTION]; + $entryFunction = $this->symbols->function(self::ENTRY_FUNCTION); $entryFile = $entryFunction->sourceFile; $entryFileArg = $this->genCharPtr($entryFile, true); $entryPrefix = str_repeat("\n", max(0, $entryFunction->startLine - 1)); @@ -1155,19 +1156,19 @@ CODE; $this->invalidateMiscObjectCache($objectFile); } - $cmd = $this->buildCompileFileCommand($cppFile, $objectFile); + $language = $this->getLanguageFromExtension($cppFile); + $options = match ($language) { + null => $this->getCompileCommandOptions(), + 'c' => $this->getCCompileCommandOptions(), + default => $this->getNativeCompileCommandOptions($language), + }; + $result = $this->getNativeBuilder()->compile($cppFile, $objectFile, $options, $language, $parallel); if (!$parallel) { - $this->climate->comment($cmd); - } - // 在并行模式下,抑制 passthru 的输出 - if ($parallel) { - exec($cmd . ' 2>&1', $output, $ret); - } else { - passthru($cmd, $ret); + $this->climate->comment($result['command']); } - if ($ret !== 0) { - if ($parallel && !empty($output)) { - foreach ($output as $line) { + if ($result['status'] !== 0) { + if ($parallel && !empty($result['output'])) { + foreach ($result['output'] as $line) { $this->climate->red($line); } } @@ -1182,27 +1183,12 @@ CODE; protected function buildCompileFileCommand(string $sourceFile, string $objectFile): string { $language = $this->getLanguageFromExtension($sourceFile); - if ($language === null) { - return $this->getCompilerBackend()->buildCompileCommand( - $sourceFile, - $objectFile, - $this->getCompileCommandOptions() - ); - } - if ($language === 'c') { - return $this->getCompilerBackend()->buildCCompileCommand( - $sourceFile, - $objectFile, - $this->getCCompileCommandOptions() - ); - } - - return $this->getCompilerBackend()->buildNativeCompileCommand( - $sourceFile, - $objectFile, - $this->getNativeCompileCommandOptions($language), - $language - ); + $options = match ($language) { + null => $this->getCompileCommandOptions(), + 'c' => $this->getCCompileCommandOptions(), + default => $this->getNativeCompileCommandOptions($language), + }; + return $this->getNativeBuilder()->compileCommand($sourceFile, $objectFile, $options, $language); } public function compile(array $sourceFiles): array @@ -1316,137 +1302,32 @@ CODE; return pcntl_wifexited($status) && pcntl_wexitstatus($status) === 0; } - protected function getCompileChildFailureReason(int $status): string - { - if (pcntl_wifsignaled($status)) { - return 'terminated by signal ' . pcntl_wtermsig($status); - } - if (pcntl_wifexited($status)) { - return 'exited with status ' . pcntl_wexitstatus($status); - } - return 'terminated abnormally'; - } - protected function compileWithPcntl(array $sourceFiles, int $job): array { - // 检查 pcntl 扩展是否可用 if (!function_exists('pcntl_fork')) { $this->climate->warning('pcntl extension not available, using sequential compilation'); return $this->compileSourceFile($sourceFiles); } - $objectFiles = []; $totalFiles = count($sourceFiles); - $runningProcesses = 0; - $processPipes = []; - $fileQueue = $sourceFiles; - $compiledCount = 0; - $failedFiles = []; - $this->climate->lightBlue("Starting parallel compilation with {$job} jobs for {$totalFiles} files"); + $result = $this->getNativeBuilder()->dispatchParallel( + $sourceFiles, + $job, + fn(string $source): string => $this->getObjectFile($source), + function (string $source, string $object): void { + $this->compileFile($source, $object, true); + }, + fn(): int => $this->pcntlFork(), + fn(): array => $this->waitForCompileChild(), + fn(int $status): bool => $this->compileChildSucceeded($status), + ); - if (!$this->noProgress) { - $progress = new Progressbar(); - $progress->barStyle([AnsiTerminal::FG_GREEN]) - ->percentageStyle([AnsiTerminal::TEXT_BOLD]) - ->labelStyle([AnsiTerminal::FG_CYAN]); - $progress->renderInPlace(0, $totalFiles, 'Compiling'); - } - - while ($compiledCount < $totalFiles) { - // 启动新进程,直到达到最大并发数 - while ($runningProcesses < $job && !empty($fileQueue)) { - $cppFile = array_shift($fileQueue); - $objectFile = $this->getObjectFile($cppFile); - - $pid = $this->pcntlFork(); - if ($pid == -1) { - $failedFiles[] = $cppFile; - foreach ($fileQueue as $queuedFile) { - $failedFiles[] = $queuedFile; - } - $compiledCount += count($fileQueue) + 1; - $fileQueue = []; - $this->climate->red('Failed to fork compiler process; no additional files will be scheduled'); - break; - } - if ($pid === 0) { - // 子进程:执行编译 - try { - $this->compileFile($cppFile, $objectFile, true); - if (!is_file($objectFile)) { - exit(1); - } - exit(0); - } catch (\Throwable $e) { - // 在子进程中不抛出异常,直接退出 - exit(1); - } - } else { - // 父进程:记录子进程 - $processPipes[$pid] = ['file' => $cppFile, 'object' => $objectFile]; - $runningProcesses++; - } - } - - // 等待任意一个子进程完成 - if ($runningProcesses > 0) { - [$pid, $status] = $this->waitForCompileChild(); - $processInfo = $processPipes[$pid] ?? null; - unset($processPipes[$pid]); - $runningProcesses--; - - if (!$this->compileChildSucceeded($status)) { - $failedFile = $processInfo['file'] ?? 'unknown'; - $failedFiles[] = $failedFile; - echo PHP_EOL; - $reason = $this->getCompileChildFailureReason($status); - $this->climate->red("Compilation failed: {$failedFile} ({$reason})"); - echo PHP_EOL; - } elseif ($processInfo) { - $objectFiles[] = $processInfo['object']; - } - $compiledCount++; - if ($this->noProgress) { - $percent = intval($compiledCount / $totalFiles * 100); - $file = $processInfo['file'] ?? 'unknown'; - $fileShorted = $this->removeCommonPrefix($this->buildDir, $file); - $this->climate->white("[{$compiledCount}/{$totalFiles}] {$percent}% {$fileShorted}"); - } else { - $progress->renderInPlace($compiledCount, $totalFiles, 'Compiling'); - } - } - } - - // 确保所有子进程都已结束 - while ($runningProcesses > 0) { - [$pid, $status] = $this->waitForCompileChild(); - $processInfo = $processPipes[$pid] ?? null; - unset($processPipes[$pid]); - $runningProcesses--; - $compiledCount++; - if (!$this->compileChildSucceeded($status)) { - $failedFile = $processInfo['file'] ?? 'unknown'; - $failedFiles[] = $failedFile; - $reason = $this->getCompileChildFailureReason($status); - $this->climate->red("Compilation failed: {$failedFile} ({$reason})"); - } elseif ($processInfo) { - $objectFiles[] = $processInfo['object']; - } - if ($this->noProgress && $processInfo) { - $percent = intval($compiledCount / $totalFiles * 100); - $this->climate->darkGray("[{$compiledCount}/{$totalFiles}] {$percent}% {$processInfo['file']}"); - } - } - - echo PHP_EOL; - - if (!empty($failedFiles)) { - throw new \Exception('Compilation failed for: ' . implode(', ', $failedFiles)); + if ($result['failures'] !== []) { + throw new \Exception('Compilation failed for: ' . implode(', ', $result['failures'])); } - $this->climate->green("Successfully compiled {$totalFiles} files"); - return $objectFiles; + return $result['objects']; } public function output(string $message, string $style = 'out'): void @@ -1456,11 +1337,7 @@ CODE; protected function buildLinkCommand(array $objectFiles, string $targetFile): string { - return $this->getCompilerBackend()->buildLinkCommand( - $objectFiles, - $targetFile, - $this->getLinkCommandOptions() - ); + return $this->getNativeBuilder()->linkCommand($objectFiles, $targetFile, $this->getLinkCommandOptions()); } public function build(array $objectFiles): string @@ -1475,29 +1352,16 @@ CODE; } } - $backend = $this->getCompilerBackend(); $buildError = null; - try { - $linkCmd = $this->buildLinkCommand($objectFiles, $targetFile); - $this->climate->comment($linkCmd); - - // 执行链接并捕获输出 - exec($linkCmd . ' 2>&1', $output, $ret); - - // 显示输出(如果有) - if (!empty($output)) { - foreach ($output as $line) { - $this->climate->out($line); - } - } - - if ($ret !== 0) { - $buildError = 'link failed: ' . $targetFile; - } elseif (!file_exists($targetFile)) { - $buildError = 'target file not generated: ' . $targetFile; - } - } finally { - $backend->cleanupResponseFile(); + $result = $this->getNativeBuilder()->link($objectFiles, $targetFile, $this->getLinkCommandOptions()); + $this->climate->comment($result['command']); + foreach ($result['output'] as $line) { + $this->climate->out($line); + } + if ($result['status'] !== 0) { + $buildError = 'link failed: ' . $targetFile; + } elseif (!$result['generated']) { + $buildError = 'target file not generated: ' . $targetFile; } if ($buildError !== null) { @@ -1509,6 +1373,11 @@ CODE; return $targetFile; } + protected function getNativeBuilder(): NativeBuilder + { + return $this->nativeBuilder ??= new NativeBuilder($this->getCompilerBackend()); + } + public function isRunRequested(): bool { return $this->climate->arguments->defined('run'); @@ -1562,7 +1431,7 @@ CODE; } $code .= $this->genDefaultArgumentHelpers(); - foreach ($this->functions as $name => $func) { + foreach ($this->symbols->functions() as $name => $func) { $code .= 'extern ' . ($func->returnsByRef ? self::TYPE_REF : $func->returnType) . ' ' . self::PREFIX . $name . '('; $list = []; if ($func->method) { @@ -1715,7 +1584,7 @@ CODE; */ private function getClassLikesWithConstants(): array { - return array_merge($this->classes, $this->interfaces); + return array_merge($this->symbols->classes(), $this->symbols->interfaces()); } protected function getFilesFromDir(string $path): array @@ -1744,7 +1613,7 @@ CODE; } // Propagate array constants to child classes that don't override them - foreach ($this->classes as $className => $classDef) { + foreach ($this->symbols->classes() as $className => $classDef) { $ownConstNames = []; foreach ($classDef->constants as $constant) { if ($constant->type === self::TYPE_ARRAY) { @@ -1753,8 +1622,8 @@ CODE; } $parentName = $this->escapeClass($classDef->extends); - while ($parentName && isset($this->classes[$parentName])) { - $parentDef = $this->classes[$parentName]; + while ($parentName && $this->symbols->hasClass($parentName)) { + $parentDef = $this->symbols->class($parentName); foreach ($parentDef->constants as $constant) { if ($constant->type === self::TYPE_ARRAY && !isset($ownConstNames[$constant->name])) { $ownConstNames[$constant->name] = true; @@ -1819,7 +1688,7 @@ CODE; protected function parseProjectYaml(string $path): array { - $cfg = Yaml::parseFile($path); + $cfg = $this->getProjectYamlLoader()->load($path); $projectDir = dirname($path); if (array_key_exists('php-version', $cfg) && !$this->climate->arguments->defined('php-version')) { @@ -2067,187 +1936,19 @@ CODE; */ protected function parseProjectYamlSourceEntry(mixed $entry): array { - if (is_string($entry)) { - return [$entry, null]; - } - if (!is_array($entry)) { - $this->error('Each `sources` entry must be a string or map'); - } - - $path = $entry['path'] ?? $entry['source'] ?? $entry['file'] ?? null; - if (!is_string($path) || trim($path) === '') { - $this->error('Conditional `sources` entries must include a non-empty `path`'); - } - - $condition = $entry['if'] ?? $entry['when'] ?? null; - if ($condition !== null && !is_string($condition)) { - $this->error('Source condition must be a string'); - } - - return [$path, $condition]; + return $this->getProjectYamlLoader()->parseSourceEntry($entry); } protected function evaluateProjectYamlCondition(string $condition): bool { - $condition = trim($condition); - if ($condition === '') { - $this->error('Source condition must not be empty'); - } - - $expr = $this->replaceProjectYamlPhpVersionComparisons($condition); - $expr = $this->replaceProjectYamlPhpOsFamilyComparisons($expr, $condition); - if (preg_match('/[A-Za-z_]/', $expr)) { - $this->error('Unsupported source condition: `' . $condition . '`'); - } - if (!preg_match('/^[0-9\s<>=!&|().+-]+$/', $expr)) { - $this->error('Unsupported source condition: `' . $condition . '`'); - } - if (preg_match('/(?error('Unsupported source condition: `' . $condition . '`'); - } - - try { - /** @phpstan-ignore-next-line */ - return (bool) eval('return (' . $expr . ');'); - } catch (\ParseError|\Throwable) { - $this->error('Invalid source condition: `' . $condition . '`'); - } - } - - protected function replaceProjectYamlPhpVersionComparisons(string $condition): string - { - $versionLiteral = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\''; - $operator = '(>=|<=|==|!=|<>|=|>|<|lt|le|gt|ge|eq|ne)'; - - $expr = preg_replace_callback( - '/\bPHP_VERSION_ID\b\s*' . $operator . '\s*([0-9]+)/i', - function (array $matches): string { - return version_compare($this->phpVersion, $this->phpVersionIdToString((int) $matches[2]), $this->normalizeProjectYamlVersionOperator($matches[1])) ? '1' : '0'; - }, - $condition - ); - if ($expr === null) { - $this->error('Invalid source condition: `' . $condition . '`'); - } - - $expr = preg_replace_callback( - '/([0-9]+)\s*' . $operator . '\s*\bPHP_VERSION_ID\b/i', - function (array $matches): string { - return version_compare($this->phpVersionIdToString((int) $matches[1]), $this->phpVersion, $this->normalizeProjectYamlVersionOperator($matches[2])) ? '1' : '0'; - }, - $expr - ); - if ($expr === null) { - $this->error('Invalid source condition: `' . $condition . '`'); - } - - $expr = preg_replace_callback( - '/\bPHP_VERSION\b\s*' . $operator . '\s*(' . $versionLiteral . ')/i', - function (array $matches): string { - $version = stripcslashes(($matches[3] ?? '') !== '' ? $matches[3] : $matches[4]); - $this->assertProjectYamlVersionLiteral($version); - return version_compare($this->phpVersion, $version, $this->normalizeProjectYamlVersionOperator($matches[1])) ? '1' : '0'; - }, - $expr - ); - if ($expr === null) { - $this->error('Invalid source condition: `' . $condition . '`'); - } - - $expr = preg_replace_callback( - '/(' . $versionLiteral . ')\s*' . $operator . '\s*\bPHP_VERSION\b/i', - function (array $matches): string { - $version = stripcslashes($matches[2] !== '' ? $matches[2] : $matches[3]); - $this->assertProjectYamlVersionLiteral($version); - return version_compare($version, $this->phpVersion, $this->normalizeProjectYamlVersionOperator($matches[4])) ? '1' : '0'; - }, - $expr - ); - if ($expr === null) { - $this->error('Invalid source condition: `' . $condition . '`'); - } - - if (preg_match('/\bPHP_VERSION(?:_ID)?\b/', $expr)) { - $this->error('Unsupported source condition: `' . $condition . '`'); - } - - return $expr; - } - - protected function replaceProjectYamlPhpOsFamilyComparisons(string $expr, string $condition): string - { - $stringLiteral = '"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\''; - $operator = '(==|!=)'; - - $expr = preg_replace_callback( - '/\bPHP_OS_FAMILY\b\s*' . $operator . '\s*(' . $stringLiteral . ')/i', - function (array $matches): string { - $expected = stripcslashes(($matches[3] ?? '') !== '' ? $matches[3] : $matches[4]); - $this->assertProjectYamlOsFamilyLiteral($expected); - $result = PHP_OS_FAMILY === $expected; - if ($matches[1] === '!=') { - $result = !$result; - } - return $result ? '1' : '0'; - }, - $expr - ); - if ($expr === null) { - $this->error('Invalid source condition: `' . $condition . '`'); - } - - $expr = preg_replace_callback( - '/(' . $stringLiteral . ')\s*' . $operator . '\s*\bPHP_OS_FAMILY\b/i', - function (array $matches): string { - $expected = stripcslashes(($matches[2] ?? '') !== '' ? $matches[2] : $matches[3]); - $this->assertProjectYamlOsFamilyLiteral($expected); - $result = $expected === PHP_OS_FAMILY; - if ($matches[4] === '!=') { - $result = !$result; - } - return $result ? '1' : '0'; - }, - $expr - ); - if ($expr === null) { - $this->error('Invalid source condition: `' . $condition . '`'); - } - - if (preg_match('/\bPHP_OS_FAMILY\b/', $expr)) { - $this->error('Unsupported source condition: `' . $condition . '`'); - } - - return $expr; - } - - protected function normalizeProjectYamlVersionOperator(string $operator): string - { - return strtolower($operator); + return $this->getProjectYamlLoader()->evaluateCondition($condition); } - protected function assertProjectYamlVersionLiteral(string $version): void + protected function getProjectYamlLoader(): ProjectYamlLoader { - if ($version === '' || !preg_match('/^[0-9A-Za-z_.+\-]+$/', $version)) { - $this->error('Invalid PHP_VERSION literal: `' . $version . '`'); - } - } - - protected function assertProjectYamlOsFamilyLiteral(string $osFamily): void - { - if (!in_array($osFamily, ['Windows', 'BSD', 'Darwin', 'Solaris', 'Linux', 'Unknown'], true)) { - $this->error('Invalid PHP_OS_FAMILY literal: `' . $osFamily . '`'); - } - } - - protected function phpVersionIdToString(int $versionId): string - { - if ($versionId < 0) { - $this->error('Invalid PHP_VERSION_ID literal: `' . $versionId . '`'); - } - $major = intdiv($versionId, 10000); - $minor = intdiv($versionId % 10000, 100); - $patch = $versionId % 100; - return $major . '.' . $minor . '.' . $patch; + $this->projectYamlLoader ??= new ProjectYamlLoader($this->phpVersion, fn(string $message): never => $this->error($message)); + $this->projectYamlLoader->setPhpVersion($this->phpVersion); + return $this->projectYamlLoader; } protected function getInternalCeInfo(string $ce): array @@ -2343,13 +2044,13 @@ CODE; protected function genClassCeList(): void { - if (empty($this->interfaces) and empty($this->classes)) { + if (empty($this->symbols->interfaces()) and empty($this->symbols->classes())) { return; } $sorter = new StringSort(); - foreach ($this->interfaces as $interfaceDef) { + foreach ($this->symbols->interfaces() as $interfaceDef) { $ce = $this->getClassCe($interfaceDef); $deps = []; @@ -2371,14 +2072,14 @@ CODE; $sorter->add($ce, $deps); } - foreach ($this->classes as $classDef) { + foreach ($this->symbols->classes() as $classDef) { $ce = $this->getClassCe($classDef); $deps = []; $parent = $classDef->extends; if ($parent) { // 不存在的父类,说明可能是内置类 $tmpCe = $this->getParentClassCe($classDef); - if (!isset($this->classes[$parent])) { + if (!$this->symbols->hasClass($parent)) { $sorter->add($tmpCe); } $deps[] = $tmpCe; @@ -2388,7 +2089,7 @@ CODE; if ($implements) { foreach ($implements as $interface) { $tmpCe = self::PREFIX . 'class_entry_' . $this->escapeCeName($interface); - if (!isset($this->interfaces[$interface])) { + if (!$this->symbols->hasInterface($interface)) { $sorter->add($tmpCe); } $deps[] = $tmpCe;