From 4632a8bba4d2117a51d4eb82fc2c6b8f4e2d73e4 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 29 Jun 2026 14:55:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(compiler):=20=E6=B7=BB=E5=8A=A0=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=92=8C=E7=B1=BB=E5=9E=8B=E6=A3=80=E6=9F=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了 project.yml 配置文件解析支持 include-paths、defines、lto 等选项 - 添加了命令行参数与配置文件选项的合并逻辑 - 增强了类型检查错误消息中的可调用名称显示 - 添加了多个测试用例验证配置文件解析和类型检查行为 - 实现了 PHP 代码中父类方法重写和联合类型的功能测试 --- .../code/class-method-override-namespace.php | 32 +++++++++ .../inheritance_error_visibility_narrow.php | 12 ++++ .../inheritance_optional_param_allowed.php | 12 ++++ .../intersection_type_parent_not_allowed.php | 16 +++++ .../intersection_type_self_not_allowed.php | 12 ++++ .../intersection_type_static_not_allowed.php | 15 ++++ phpunit/code/union_type_parent_allowed.php | 12 ++++ phpunit/code/union_type_self_allowed.php | 8 +++ phpunit/src/CompilerBaseApiTest.php | 71 +++++++++++++++++++ phpunit/src/TypeCheckGeneratorTest.php | 70 ++++++++++++++++++ src/Php/CompilerBase.php | 2 +- src/Php/Translator.php | 63 ++++++++++++++-- 12 files changed, 320 insertions(+), 5 deletions(-) create mode 100644 phpunit/code/class-method-override-namespace.php create mode 100644 phpunit/code/inheritance_error_visibility_narrow.php create mode 100644 phpunit/code/inheritance_optional_param_allowed.php create mode 100644 phpunit/code/intersection_type_parent_not_allowed.php create mode 100644 phpunit/code/intersection_type_self_not_allowed.php create mode 100644 phpunit/code/intersection_type_static_not_allowed.php create mode 100644 phpunit/code/union_type_parent_allowed.php create mode 100644 phpunit/code/union_type_self_allowed.php create mode 100644 phpunit/src/TypeCheckGeneratorTest.php diff --git a/phpunit/code/class-method-override-namespace.php b/phpunit/code/class-method-override-namespace.php new file mode 100644 index 00000000..4b70736e --- /dev/null +++ b/phpunit/code/class-method-override-namespace.php @@ -0,0 +1,32 @@ +bar(); + } + + public function bar(): void + { + echo "Parent\n"; + } + } +} + +namespace { + function main(): void + { + $o = new Demo\Dispatch\ChildOverrideNs(); + $o->run(); + } +} diff --git a/phpunit/code/inheritance_error_visibility_narrow.php b/phpunit/code/inheritance_error_visibility_narrow.php new file mode 100644 index 00000000..449a1a71 --- /dev/null +++ b/phpunit/code/inheritance_error_visibility_narrow.php @@ -0,0 +1,12 @@ +originalArgv = $argv ?? []; $this->testDir = sys_get_temp_dir() . '/compiler_api_test_' . uniqid(); mkdir($this->testDir, 0777, true); $this->compiler = CompilerTest::create($this->testDir); @@ -24,6 +27,8 @@ class CompilerBaseApiTest extends TestCase protected function tearDown(): void { parent::tearDown(); + global $argv; + $argv = $this->originalArgv; // Recursively remove the test directory (compiler creates build/ subdir) $this->removeDirectory($this->testDir); } @@ -62,6 +67,17 @@ class CompilerBaseApiTest extends TestCase return $m->invoke($this->compiler, ...$args); } + private function createProjectFile(string $yaml): string + { + $sourceFile = $this->testDir . '/main.php'; + file_put_contents($sourceFile, "testDir . '/project.yml'; + file_put_contents($projectFile, $yaml); + + return $projectFile; + } + // ======================================================================== // getTypeFromZendType // ======================================================================== @@ -145,6 +161,61 @@ class CompilerBaseApiTest extends TestCase $this->assertEquals($buildDir . '/include', $includeDir); } + public function testParseProjectYamlLoadsDocumentedCompilerOptions(): void + { + $projectFile = $this->createProjectFile(<<<'YAML' +sources: + - main.php +include-paths: + - /opt/mylib/include + - ../shared/headers +defines: + - ENABLE_LOGGING=1 + - DEBUG_LEVEL=3 +lto: true +link-libs: + - curl + - ssl +link-paths: + - /usr/local/lib + - /opt/custom/lib +YAML); + + $this->invokeMethod('parseProjectYaml', $projectFile); + + $this->assertSame(['/opt/mylib/include', '../shared/headers'], $this->compiler->getUserIncludePaths()); + $this->assertSame(['ENABLE_LOGGING=1', 'DEBUG_LEVEL=3'], $this->compiler->getUserDefines()); + $this->assertTrue($this->compiler->isLtoEnabled()); + $this->assertSame(['curl', 'ssl'], $this->compiler->getLinkLibs()); + $this->assertSame(['/usr/local/lib', '/opt/custom/lib'], $this->compiler->getLinkPaths()); + } + + public function testApplyCommandLineArgumentsDoesNotClearYamlRepeatableOptionsWhenCliAbsent(): void + { + $projectFile = $this->createProjectFile(<<<'YAML' +sources: + - main.php +include-paths: + - /yaml/include +defines: + - YAML_DEFINE=1 +lto: true +link-libs: + - yamlssl +link-paths: + - /yaml/lib +YAML); + + $this->invokeMethod('parseProjectYaml', $projectFile); + $this->invokeMethod('applyCommandLineArguments'); + + $this->assertSame(['/yaml/include'], $this->compiler->getUserIncludePaths()); + $this->assertSame(['YAML_DEFINE=1'], $this->compiler->getUserDefines()); + $this->assertTrue($this->compiler->isLtoEnabled()); + $this->assertSame(['yamlssl'], $this->compiler->getLinkLibs()); + $this->assertSame(['/yaml/lib'], $this->compiler->getLinkPaths()); + } + // ======================================================================== // isWindows / isLinux / isMacos // ======================================================================== diff --git a/phpunit/src/TypeCheckGeneratorTest.php b/phpunit/src/TypeCheckGeneratorTest.php new file mode 100644 index 00000000..6934bf57 --- /dev/null +++ b/phpunit/src/TypeCheckGeneratorTest.php @@ -0,0 +1,70 @@ +setAccessible(true); + $ref->setValue($object, $value); + } + + private function invokeMethod(object $object, string $method, array $args = []): mixed + { + $ref = new ReflectionMethod($object, $method); + $ref->setAccessible(true); + return $ref->invokeArgs($object, $args); + } + + public function testMethodTypeCheckErrorUsesClassQualifiedCallableName(): void + { + $compiler = CompilerTest::create(ROOT_PATH); + $classDef = new ClassDef('Demo', 0, 'Foo\\Bar'); + $functionDef = new FunctionDef('run', 'php::Var', 'Foo\\Bar'); + $argInfo = new ArgInfo(); + $argInfo->name = 'value'; + $argInfo->typeStr = 'int|string'; + + $functionDef->returnTypeCheck = [['kind' => 'isInt'], ['kind' => 'isString']]; + $functionDef->returnTypeStr = 'int|string'; + + $this->setProtectedProperty($compiler, 'classDef', $classDef); + $this->setProtectedProperty($compiler, 'functionDef', $functionDef); + + $callableName = $this->invokeMethod($compiler, 'getTypeCheckCallableName'); + $paramExpr = $this->invokeMethod($compiler, 'genUnionParamTypeErrorExpr', [$argInfo, 'value', '1']); + $returnCode = $this->invokeMethod($compiler, 'genUnionReturnCheck', ['retval']); + + $this->assertSame('Foo\\Bar\\Demo::run', $callableName); + $this->assertStringContainsString('Foo\\\\Bar\\\\Demo::run(): Argument #', $paramExpr); + $this->assertStringContainsString('Foo\\\\Bar\\\\Demo::run', $returnCode); + } + + public function testFunctionTypeCheckErrorUsesFunctionQualifiedCallableName(): void + { + $compiler = CompilerTest::create(ROOT_PATH); + $functionDef = new FunctionDef('run', 'php::Var', 'Foo\\Bar'); + $argInfo = new ArgInfo(); + $argInfo->name = 'value'; + $argInfo->typeStr = 'int|string'; + + $functionDef->returnTypeCheck = [['kind' => 'isInt'], ['kind' => 'isString']]; + $functionDef->returnTypeStr = 'int|string'; + + $this->setProtectedProperty($compiler, 'classDef', null); + $this->setProtectedProperty($compiler, 'functionDef', $functionDef); + + $callableName = $this->invokeMethod($compiler, 'getTypeCheckCallableName'); + $paramExpr = $this->invokeMethod($compiler, 'genUnionParamTypeErrorExpr', [$argInfo, 'value', '1']); + $returnCode = $this->invokeMethod($compiler, 'genUnionReturnCheck', ['retval']); + + $this->assertSame('Foo\\Bar\\run', $callableName); + $this->assertStringContainsString('Foo\\\\Bar\\\\run(): Argument #', $paramExpr); + $this->assertStringContainsString('Foo\\\\Bar\\\\run', $returnCode); + } +} diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 86494cc6..36ca49de 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -3647,7 +3647,7 @@ class CompilerBase extends \PhpAot\Core\Translator return '-' . $code; } - protected function parseUnaryPlus(Expr\UnaryPlus $expr) + protected function parseUnaryPlus(Expr\UnaryPlus $expr): string { return $this->parseExpr($expr->expr); } diff --git a/src/Php/Translator.php b/src/Php/Translator.php index 12e701f6..7f3dae2b 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -339,9 +339,13 @@ class Translator extends Preprocessor } // 用户自定义 C++ include 路径(直接从 argv 解析以支持多值) - $this->userIncludePaths = $this->parseRepeatableArgv(['-I', '--include-path']); + if ($this->hasRepeatableArgvFlag(['-I', '--include-path'])) { + $this->userIncludePaths = $this->parseRepeatableArgv(['-I', '--include-path']); + } // 用户自定义预处理器宏(直接从 argv 解析以支持多值) - $this->userDefines = $this->parseRepeatableArgv(['-D', '--define']); + if ($this->hasRepeatableArgvFlag(['-D', '--define'])) { + $this->userDefines = $this->parseRepeatableArgv(['-D', '--define']); + } // 链接时优化 if ($this->climate->arguments->defined('lto')) { @@ -359,9 +363,13 @@ class Translator extends Preprocessor } // 用户自定义链接库(直接从 argv 解析以支持多值) - $this->linkLibs = $this->parseRepeatableArgv(['-l', '--link-lib']); + if ($this->hasRepeatableArgvFlag(['-l', '--link-lib'])) { + $this->linkLibs = $this->parseRepeatableArgv(['-l', '--link-lib']); + } // 用户自定义库搜索路径(直接从 argv 解析以支持多值) - $this->linkPaths = $this->parseRepeatableArgv(['-L', '--link-path']); + if ($this->hasRepeatableArgvFlag(['-L', '--link-path'])) { + $this->linkPaths = $this->parseRepeatableArgv(['-L', '--link-path']); + } } /** @@ -397,6 +405,32 @@ class Translator extends Preprocessor return $values; } + protected function hasRepeatableArgvFlag(array $flags): bool + { + global $argv; + + for ($i = 1; $i < count($argv); $i++) { + $arg = $argv[$i]; + if (in_array($arg, $flags, true)) { + return true; + } + + foreach ($flags as $flag) { + if (str_starts_with($arg, $flag . '=')) { + return true; + } + if (strlen($flag) === 2 && $flag[0] === '-') { + $short = substr($flag, 1); + if (preg_match('/^-' . preg_quote($short, '/') . '(.+)$/', $arg)) { + return true; + } + } + } + } + + return false; + } + /** * 处理 --flag=value 格式的长标志 */ @@ -1967,6 +2001,27 @@ CODE; } } + // 读取 include-paths + $includePaths = $cfg['include-paths'] ?? null; + if (!empty($includePaths) && is_array($includePaths)) { + foreach ($includePaths as $includePath) { + $this->userIncludePaths[] = (string) $includePath; + } + } + + // 读取 defines + $defines = $cfg['defines'] ?? null; + if (!empty($defines) && is_array($defines)) { + foreach ($defines as $define) { + $this->userDefines[] = (string) $define; + } + } + + // 读取 lto + if (!empty($cfg['lto'])) { + $this->enableLto = true; + } + // 读取 link-libs $linkLibs = $cfg['link-libs'] ?? null; if (!empty($linkLibs) && is_array($linkLibs)) {