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.
290 lines
11 KiB
290 lines
11 KiB
<?php
|
|
/**
|
|
* This file is part of TypePHP(AOT).
|
|
*
|
|
* @link https://www.swoole.com/aot/
|
|
* @contact service@swoole.com
|
|
*/
|
|
|
|
use TypePhp\CompilerTest;
|
|
use TypePhp\Exception\TestError;
|
|
|
|
/**
|
|
* Compound declarations must be rejected by the TypePHP front end, rather
|
|
* than leaking into gen_stub.php or the C++ compiler.
|
|
* @internal
|
|
* @coversNothing
|
|
*/
|
|
final class CompoundTypeDeclarationValidationTest extends PHPUnit\Framework\TestCase
|
|
{
|
|
private string $testRoot;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->testRoot = sys_get_temp_dir() . '/typephp-compound-type-' . bin2hex(random_bytes(8));
|
|
mkdir($this->testRoot, 0777, true);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
if (!is_dir($this->testRoot)) {
|
|
return;
|
|
}
|
|
$iterator = new RecursiveIteratorIterator(
|
|
new RecursiveDirectoryIterator($this->testRoot, FilesystemIterator::SKIP_DOTS),
|
|
RecursiveIteratorIterator::CHILD_FIRST,
|
|
);
|
|
foreach ($iterator as $entry) {
|
|
$entry->isDir() ? rmdir($entry->getPathname()) : unlink($entry->getPathname());
|
|
}
|
|
rmdir($this->testRoot);
|
|
}
|
|
|
|
/** @dataProvider invalidNamedDeclarationProvider */
|
|
public function testInvalidNamedDeclarationFailsDuringPrepare(string $declaration, string $diagnostic): void
|
|
{
|
|
[$compiler, $file] = $this->compilerFor("<?php\n{$declaration}\nfunction main(): void {}\n");
|
|
|
|
$this->expectException(TestError::class);
|
|
$this->expectExceptionMessage($diagnostic);
|
|
$compiler->prepareFile($file);
|
|
}
|
|
|
|
public static function invalidNamedDeclarationProvider(): iterable
|
|
{
|
|
yield 'duplicate builtin union member' => [
|
|
'function broken(int|string|int $value): void {}',
|
|
'Duplicate type int is redundant',
|
|
];
|
|
yield 'duplicate resolved class union member' => [
|
|
'namespace App; use Vendor\Model as Item; function broken(Item|\Vendor\Model $value): void {}',
|
|
'Duplicate type Vendor\Model is redundant',
|
|
];
|
|
yield 'iterable includes array' => [
|
|
'function broken(iterable|array $value): void {}',
|
|
'Duplicate type array is redundant',
|
|
];
|
|
yield 'iterable includes Traversable' => [
|
|
'function broken(\Traversable|iterable $value): void {}',
|
|
'Duplicate type Traversable is redundant',
|
|
];
|
|
yield 'bool includes false' => [
|
|
'function broken(false|bool $value): void {}',
|
|
'Duplicate type false is redundant',
|
|
];
|
|
yield 'true and false must use bool' => [
|
|
'function broken(true|false $value): void {}',
|
|
'Type contains both true and false, bool must be used instead',
|
|
];
|
|
yield 'mixed in union' => [
|
|
'function broken(mixed|string $value): void {}',
|
|
'Type mixed can only be used as a standalone type',
|
|
];
|
|
yield 'void in union' => [
|
|
'function broken(): void|string {}',
|
|
'Type void can only be used as a standalone type',
|
|
];
|
|
yield 'never in union' => [
|
|
'function broken(): never|string {}',
|
|
'Type never can only be used as a standalone type',
|
|
];
|
|
yield 'nullable mixed' => [
|
|
'function broken(?mixed $value): void {}',
|
|
'Type mixed cannot be marked as nullable since mixed already includes null',
|
|
];
|
|
yield 'nullable null' => [
|
|
'function broken(?null $value): void {}',
|
|
'null cannot be marked as nullable',
|
|
];
|
|
yield 'nullable void' => [
|
|
'function broken(): ?void {}',
|
|
'Void can only be used as a standalone type',
|
|
];
|
|
yield 'nullable never' => [
|
|
'function broken(): ?never {}',
|
|
'never can only be used as a standalone type',
|
|
];
|
|
yield 'scalar intersection member' => [
|
|
'function broken(A&int $value): void {}',
|
|
'Type int cannot be part of an intersection type',
|
|
];
|
|
yield 'callable intersection member' => [
|
|
'function broken(A&callable $value): void {}',
|
|
'Type callable cannot be part of an intersection type',
|
|
];
|
|
yield 'duplicate resolved intersection member' => [
|
|
'namespace App; use Vendor\Contract as C; function broken(C&\Vendor\Contract $value): void {}',
|
|
'Duplicate type Vendor\Contract is redundant',
|
|
];
|
|
yield 'permuted duplicate DNF member' => [
|
|
'function broken((A&B)|(B&A) $value): void {}',
|
|
'Type B&A is redundant with type A&B',
|
|
];
|
|
yield 'DNF strict superset after subset' => [
|
|
'function broken((A&B)|(A&B&C) $value): void {}',
|
|
'Type A&B&C is redundant as it is more restrictive than type A&B',
|
|
];
|
|
yield 'DNF strict superset before subset' => [
|
|
'function broken((A&B&C)|(A&B) $value): void {}',
|
|
'Type A&B&C is redundant as it is more restrictive than type A&B',
|
|
];
|
|
yield 'plain class subsumes DNF member' => [
|
|
'function broken((A&B)|A $value): void {}',
|
|
'Type A&B is redundant as it is more restrictive than type A',
|
|
];
|
|
yield 'object subsumes class' => [
|
|
'function broken(object|A $value): void {}',
|
|
'contains both object and a class type, which is redundant',
|
|
];
|
|
yield 'object subsumes DNF member' => [
|
|
'function broken(object|(A&B) $value): void {}',
|
|
'contains both object and a class type, which is redundant',
|
|
];
|
|
yield 'self in global function' => [
|
|
'function broken(): self {}',
|
|
'Cannot use "self" when no class scope is active',
|
|
];
|
|
yield 'self in global function union' => [
|
|
'function broken(): self|A {}',
|
|
'Cannot use "self" when no class scope is active',
|
|
];
|
|
yield 'self in global function DNF parameter' => [
|
|
'function broken((self&A)|B $value): void {}',
|
|
'Cannot use "self" when no class scope is active',
|
|
];
|
|
yield 'static in global function' => [
|
|
'function broken(): static {}',
|
|
'Cannot use "static" when no class scope is active',
|
|
];
|
|
yield 'static in global function union' => [
|
|
'function broken(): static|A {}',
|
|
'Cannot use "static" when no class scope is active',
|
|
];
|
|
yield 'parent in global function' => [
|
|
'function broken(): parent {}',
|
|
'Cannot use "parent" when no class scope is active',
|
|
];
|
|
yield 'parent method without parent class' => [
|
|
'class A { public function broken(): parent {} }',
|
|
'Cannot use "parent" when current class scope has no parent',
|
|
];
|
|
yield 'parent property without parent class' => [
|
|
'class A { public parent $value; }',
|
|
'Cannot use "parent" when current class scope has no parent',
|
|
];
|
|
yield 'parent constant without parent class' => [
|
|
'class A { public const parent VALUE = null; }',
|
|
'Cannot use "parent" when current class scope has no parent',
|
|
];
|
|
yield 'self in intersection inside class' => [
|
|
'class A { public function broken(self&B $value): void {} }',
|
|
"Type 'self' cannot be part of an intersection type",
|
|
];
|
|
yield 'self in DNF promoted property' => [
|
|
'class A { public function __construct(public (self&B)|C $value) {} }',
|
|
"Type 'self' cannot be part of an intersection type",
|
|
];
|
|
yield 'static in intersection return type' => [
|
|
'class A { public function broken(): static&B {} }',
|
|
"Type 'static' cannot be part of an intersection type",
|
|
];
|
|
yield 'duplicate class implements' => [
|
|
'interface I {} class A implements I, I {}',
|
|
'Class A cannot implement previously implemented interface I',
|
|
];
|
|
yield 'duplicate enum implements through alias' => [
|
|
'namespace App; interface I {} use App\I as Contract; enum E implements I, Contract { case A; }',
|
|
'Enum App\E cannot implement previously implemented interface App\I',
|
|
];
|
|
}
|
|
|
|
/** @dataProvider invalidClosureDeclarationProvider */
|
|
public function testInvalidClosureDeclarationFailsDuringConvert(string $body, string $diagnostic): void
|
|
{
|
|
[$compiler, $file] = $this->compilerFor("<?php\nfunction main(): void {\n{$body}\n}\n");
|
|
$compiler->prepareFile($file);
|
|
|
|
$this->expectException(TestError::class);
|
|
$this->expectExceptionMessage($diagnostic);
|
|
$compiler->convertFile($file);
|
|
}
|
|
|
|
public static function invalidClosureDeclarationProvider(): iterable
|
|
{
|
|
yield 'closure duplicate union' => [
|
|
'$fn = function (int|string|int $value): void {};',
|
|
'Duplicate type int is redundant',
|
|
];
|
|
yield 'arrow function invalid intersection' => [
|
|
'$fn = fn (A&int $value): int => 1;',
|
|
'Type int cannot be part of an intersection type',
|
|
];
|
|
yield 'closure permuted DNF' => [
|
|
'$fn = function ((A&B)|(B&A) $value): void {};',
|
|
'Type B&A is redundant with type A&B',
|
|
];
|
|
yield 'global closure self intersection' => [
|
|
'$fn = function (self&A $value): void {};',
|
|
"Type 'self' cannot be part of an intersection type",
|
|
];
|
|
}
|
|
|
|
public function testValidBoundaryDeclarationsCompile(): void
|
|
{
|
|
[$compiler, $file] = $this->compilerFor(<<<'PHP'
|
|
<?php
|
|
namespace App;
|
|
|
|
interface A {}
|
|
interface B {}
|
|
interface C {}
|
|
interface Traversable {}
|
|
|
|
class Base {}
|
|
class Child extends Base
|
|
{
|
|
public function choose((A&B)|(A&C)|self|parent $value): (A&B)|(A&C)|self|parent|static
|
|
{
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
function localNameDoesNotOverlapBuiltin(iterable|Traversable $value): void {}
|
|
function main(): void {}
|
|
PHP);
|
|
$compiler->prepareFile($file);
|
|
$compiler->convertFile($file);
|
|
|
|
self::assertFileExists($compiler->getCppFile($file));
|
|
}
|
|
|
|
public function testGlobalClosuresKeepBindableSelfAndStaticTypes(): void
|
|
{
|
|
[$compiler, $file] = $this->compilerFor(<<<'PHP'
|
|
<?php
|
|
function main(): void
|
|
{
|
|
$withSelf = function (self $value): self { return $value; };
|
|
$withStatic = function (): static { throw new Exception('not invoked'); };
|
|
}
|
|
PHP);
|
|
$compiler->prepareFile($file);
|
|
$compiler->convertFile($file);
|
|
|
|
self::assertFileExists($compiler->getCppFile($file));
|
|
}
|
|
|
|
/** @return array{CompilerTest, string} */
|
|
private function compilerFor(string $source): array
|
|
{
|
|
$file = $this->testRoot . '/program.php';
|
|
file_put_contents($file, $source);
|
|
|
|
global $translator;
|
|
$compiler = CompilerTest::create($this->testRoot);
|
|
$translator = $compiler;
|
|
$compiler->addFiles([$file]);
|
|
|
|
return [$compiler, $file];
|
|
}
|
|
}
|
|
|