feat(compiler): 添加项目配置文件解析和类型检查功能

- 实现了 project.yml 配置文件解析支持 include-paths、defines、lto 等选项
- 添加了命令行参数与配置文件选项的合并逻辑
- 增强了类型检查错误消息中的可调用名称显示
- 添加了多个测试用例验证配置文件解析和类型检查行为
- 实现了 PHP 代码中父类方法重写和联合类型的功能测试
pull/5/head
韩天峰 2 months ago
parent 409c0959f4
commit 4632a8bba4
  1. 32
      phpunit/code/class-method-override-namespace.php
  2. 12
      phpunit/code/inheritance_error_visibility_narrow.php
  3. 12
      phpunit/code/inheritance_optional_param_allowed.php
  4. 16
      phpunit/code/intersection_type_parent_not_allowed.php
  5. 12
      phpunit/code/intersection_type_self_not_allowed.php
  6. 15
      phpunit/code/intersection_type_static_not_allowed.php
  7. 12
      phpunit/code/union_type_parent_allowed.php
  8. 8
      phpunit/code/union_type_self_allowed.php
  9. 71
      phpunit/src/CompilerBaseApiTest.php
  10. 70
      phpunit/src/TypeCheckGeneratorTest.php
  11. 2
      src/Php/CompilerBase.php
  12. 63
      src/Php/Translator.php

@ -0,0 +1,32 @@
<?php
namespace Demo\Dispatch {
class ChildOverrideNs extends ParentBaseNs
{
public function bar(): void
{
echo "Child\n";
}
}
class ParentBaseNs
{
public function run(): void
{
$this->bar();
}
public function bar(): void
{
echo "Parent\n";
}
}
}
namespace {
function main(): void
{
$o = new Demo\Dispatch\ChildOverrideNs();
$o->run();
}
}

@ -0,0 +1,12 @@
<?php
class A
{
public function f() {}
}
class B extends A
{
protected function f() {}
}
function main() {}

@ -0,0 +1,12 @@
<?php
class A
{
public function f($x) {}
}
class B extends A
{
public function f($x, $y = 1) {}
}
function main() {}

@ -0,0 +1,16 @@
<?php
interface ITypeParent
{
}
class BaseIntersectionParent implements ITypeParent
{
}
class DemoParentIntersection extends BaseIntersectionParent
{
public function run(parent&ITypeParent $value): void {}
}
function main() {}

@ -0,0 +1,12 @@
<?php
interface ITypeSelf
{
}
class DemoSelfIntersection implements ITypeSelf
{
public function run(self&ITypeSelf $value): void {}
}
function main() {}

@ -0,0 +1,15 @@
<?php
interface ITypeStatic
{
}
class DemoStaticIntersection implements ITypeStatic
{
public function run(): static&ITypeStatic
{
return $this;
}
}
function main() {}

@ -0,0 +1,12 @@
<?php
class BaseParentUnion
{
}
class DemoParentUnion extends BaseParentUnion
{
public function run(parent|string $value): void {}
}
function main() {}

@ -0,0 +1,8 @@
<?php
class DemoSelfUnion
{
public function run(self|string $value): void {}
}
function main() {}

@ -11,10 +11,13 @@ class CompilerBaseApiTest extends TestCase
private string $testDir;
private CompilerTest $compiler;
private \ReflectionClass $ref;
private array $originalArgv;
protected function setUp(): void
{
parent::setUp();
global $argv;
$this->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, "<?php\nfunction main() {}\n");
$projectFile = $this->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
// ========================================================================

@ -0,0 +1,70 @@
<?php
use PhpAot\Php\ArgInfo;
use PhpAot\Php\CompilerTest;
use PhpAot\Php\Entity\ClassDef;
use PhpAot\Php\Entity\FunctionDef;
class TypeCheckGeneratorTest extends \PHPUnit\Framework\TestCase
{
private function setProtectedProperty(object $object, string $property, mixed $value): void
{
$ref = new ReflectionProperty($object, $property);
$ref->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);
}
}

@ -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);
}

@ -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)) {

Loading…
Cancel
Save