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.
 
 

407 lines
9.9 KiB

<?php
/**
* This file is part of Swoole-Compiler(AOT).
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
use PhpParser\Node;
use TypePhp\CompilerTest;
use TypePhp\Diagnostics\DiagnosticReporter;
use TypePhp\Exception\TestError;
final class NegativeCompatibilityDiagnosticReporter implements DiagnosticReporter
{
/** @var list<string> */
public array $warnings = [];
public function fatal(string $message): never
{
throw new TestError($message);
}
public function warning(Node $node, string $file, string $message): void
{
$this->warnings[] = $message . ' in ' . $file . ':' . $node->getStartLine();
}
}
/**
* Verifies that intentional PHP compatibility boundaries fail in a controlled,
* stable compiler phase instead of warning, crashing, or emitting invalid C++.
* @internal
* @coversNothing
*/
final class NegativeCompatibilityTest extends PHPUnit\Framework\TestCase
{
private string $testRoot;
protected function setUp(): void
{
$this->testRoot = sys_get_temp_dir() . '/typephp-negative-' . 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) {
if ($entry->isDir()) {
rmdir($entry->getPathname());
} else {
unlink($entry->getPathname());
}
}
rmdir($this->testRoot);
}
/**
* @dataProvider incompatibilityProvider
*/
public function testIntentionalIncompatibilityFailsCleanly(
string $expectedPhase,
string $expectedDiagnostic,
string $source,
): void {
$file = $this->testRoot . '/program.php';
file_put_contents($file, $source);
global $translator;
$compiler = CompilerTest::create($this->testRoot);
$translator = $compiler;
$reporter = new NegativeCompatibilityDiagnosticReporter();
$compiler->setDiagnosticReporter($reporter);
$compiler->addFiles([$file]);
$phpDiagnostics = [];
$failure = null;
$failurePhase = 'prepare';
set_error_handler(static function (
int $severity,
string $message,
string $diagnosticFile,
int $line,
) use (&$phpDiagnostics): bool {
if (!(error_reporting() & $severity)) {
return false;
}
$phpDiagnostics[] = $message . ' in ' . $diagnosticFile . ':' . $line;
return true;
});
try {
$compiler->prepareFile($file);
$failurePhase = 'convert';
$compiler->convertFile($file);
} catch (Throwable $exception) {
$failure = $exception;
} finally {
restore_error_handler();
}
self::assertNotNull($failure, 'Compilation unexpectedly succeeded');
self::assertInstanceOf(
TestError::class,
$failure,
'The compiler boundary must use a controlled diagnostic, not ' . $failure::class,
);
self::assertSame($expectedPhase, $failurePhase, 'The diagnostic was raised in the wrong compiler phase');
self::assertSame(
$expectedDiagnostic . ' in ' . $file . ':' . $this->diagnosticLine($source, $expectedDiagnostic),
$failure->getMessage(),
'The compiler diagnostic changed',
);
self::assertSame([], $reporter->warnings, 'The compiler emitted warnings before failing');
self::assertSame([], $phpDiagnostics, 'PHP emitted a warning/notice before the compiler failed');
self::assertFileDoesNotExist($compiler->getCppFile($file), 'A failed conversion emitted a C++ file');
}
public static function incompatibilityProvider(): iterable
{
yield 'global executable statement' => [
'prepare',
'All execution code must be within a function, found stray code',
"<?php\nprint \"outside main\"; // @diagnostic\n",
];
yield 'variable variables' => [
'convert',
'The `$$` syntax is not supported',
<<<'PHP'
<?php
function main(): void
{
$name = 'value';
$value = 42;
print $$name; // @diagnostic
}
PHP,
];
yield 'closure reference parameter' => [
'convert',
'Closure cannot use reference parameter',
<<<'PHP'
<?php
function main(): void
{
$callback = static function (&$value): void { // @diagnostic
};
}
PHP,
];
yield 'arrow function reference parameter' => [
'convert',
'Closure cannot use reference parameter',
<<<'PHP'
<?php
function main(): void
{
$callback = static fn (&$value): mixed => $value; // @diagnostic
}
PHP,
];
yield 'closure reference return' => [
'convert',
'Closure and arrow functions cannot return by reference',
<<<'PHP'
<?php
function main(): void
{
$callback = static function &(): mixed { // @diagnostic
static $value = 42;
return $value;
};
}
PHP,
];
yield 'property get hook reference return' => [
'prepare',
'Property get hooks returning by reference are not supported',
<<<'PHP'
<?php
final class ReferencePropertyHook
{
public string $value {
&get => $this->value; // @diagnostic
}
}
PHP,
];
yield 'arrow function reference return' => [
'convert',
'Closure and arrow functions cannot return by reference',
<<<'PHP'
<?php
function main(): void
{
$value = 42;
$callback = static fn &(): mixed => $value; // @diagnostic
}
PHP,
];
yield 'reference variadic parameter' => [
'prepare',
'Variadic parameters cannot be passed by reference',
<<<'PHP'
<?php
function collect(&...$values): array // @diagnostic
{
return $values;
}
PHP,
];
yield 'ticks declare' => [
'convert',
'declare(ticks=1) is not supported',
<<<'PHP'
<?php
declare(ticks=1); // @diagnostic
function main(): void
{
}
PHP,
];
yield 'non-UTF-8 encoding declare' => [
'convert',
'declare(encoding="ISO-8859-1") is not supported, only UTF-8 is supported',
<<<'PHP'
<?php
declare(encoding='ISO-8859-1'); // @diagnostic
function main(): void
{
}
PHP,
];
yield 'unknown declare directive' => [
'convert',
'declare(custom=1) is not supported',
<<<'PHP'
<?php
declare(custom=1); // @diagnostic
function main(): void
{
}
PHP,
];
yield 'disabled strict types declare' => [
'convert',
'declare(strict_types=0) is not allowed, only strict_types=1 is supported',
<<<'PHP'
<?php
declare(strict_types=0); // @diagnostic
function main(): void
{
}
PHP,
];
yield 'nested match arm condition' => [
'convert',
'Match expression cannot be used as a condition',
<<<'PHP'
<?php
function main(): void
{
$value = 1;
$result = match ($value) {
match ($value) { 1 => 1, default => 0 } => 'nested', // @diagnostic
default => 'default',
};
}
PHP,
];
yield 'foreach reference property target' => [
'convert',
'Foreach by reference only supports variable as value',
<<<'PHP'
<?php
final class Holder
{
public mixed $value = null;
}
function main(): void
{
$holder = new Holder();
$values = [1, 2];
foreach ($values as &$holder->value) { // @diagnostic
}
}
PHP,
];
yield 'foreach reference list destructuring' => [
'convert',
'Foreach list destructuring cannot bind items by reference',
<<<'PHP'
<?php
function main(): void
{
$rows = [[1, 2]];
foreach ($rows as [&$left, &$right]) { // @diagnostic
}
}
PHP,
];
yield 'void cast result assignment' => [
'prepare',
'The (void) cast can only be used as a statement',
<<<'PHP'
<?php
function result(): int
{
return 1;
}
function main(): void
{
$value = (void) result(); // @diagnostic
}
PHP,
];
yield 'void cast return value' => [
'prepare',
'The (void) cast can only be used as a statement',
<<<'PHP'
<?php
function result(): int
{
return 1;
}
function invalid(): mixed
{
return (void) result(); // @diagnostic
}
PHP,
];
yield 'void cast argument value' => [
'prepare',
'The (void) cast can only be used as a statement',
<<<'PHP'
<?php
function consume(mixed $value): void
{
}
function main(): void
{
consume((void) 1); // @diagnostic
}
PHP,
];
yield 'void cast condition value' => [
'prepare',
'The (void) cast can only be used as a statement',
<<<'PHP'
<?php
function main(): void
{
if ((void) true) { // @diagnostic
}
}
PHP,
];
yield 'void cast final for condition' => [
'prepare',
'The (void) cast can only be used as a statement',
<<<'PHP'
<?php
function main(): void
{
for (; (void) true; ) { // @diagnostic
}
}
PHP,
];
}
private function diagnosticLine(string $source, string $diagnostic): int
{
foreach (explode("\n", $source) as $index => $line) {
if (str_contains($line, '@diagnostic')) {
return $index + 1;
}
}
self::fail('Missing @diagnostic marker for ' . $diagnostic);
}
}