- Implement parsing and validation for (void) cast expressions - Add VoidCastValidationVisitor to enforce statement-only usage rules - Allow (void) cast in discarded for-expression-list positions - Generate appropriate diagnostic errors for invalid (void) cast contexts - Update documentation to reflect void cast support and limitations - Add comprehensive tests for void cast functionality and error casesmaster
parent
6dcafb652b
commit
9fc5755544
11 changed files with 270 additions and 10 deletions
@ -0,0 +1,106 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of Swoole-Compiler(AOT). |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\Transform; |
||||||
|
|
||||||
|
use PhpParser\Node; |
||||||
|
use PhpParser\Node\Expr; |
||||||
|
use PhpParser\Node\Stmt; |
||||||
|
use PhpParser\NodeVisitorAbstract; |
||||||
|
use TypePhp\Exception\SyntaxError; |
||||||
|
|
||||||
|
/** |
||||||
|
* Enforces PHP 8.5's statement-only grammar for the (void) cast. |
||||||
|
* |
||||||
|
* php-parser represents (void) as an Expr in every context for parser-version |
||||||
|
* compatibility. PHP itself only accepts it as a statement, or in a discarded |
||||||
|
* for-expression-list position. Marking accepted nodes also makes codegen |
||||||
|
* reject any synthetic/unvalidated Void_ node defensively. |
||||||
|
*/ |
||||||
|
final class VoidCastValidationVisitor extends NodeVisitorAbstract |
||||||
|
{ |
||||||
|
public const string ALLOWED_ATTRIBUTE = 'typephpVoidCastAllowed'; |
||||||
|
private const string ERROR_MESSAGE = 'The (void) cast can only be used as a statement'; |
||||||
|
|
||||||
|
/** @var list<Node> */ |
||||||
|
private array $stack = []; |
||||||
|
|
||||||
|
/** @param \Closure(Node, string): never|null $fatalError */ |
||||||
|
public function __construct(private readonly ?\Closure $fatalError = null) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function beforeTraverse(array $nodes): null |
||||||
|
{ |
||||||
|
$this->stack = []; |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
public function enterNode(Node $node): null |
||||||
|
{ |
||||||
|
if ($node instanceof Expr\Cast\Void_) { |
||||||
|
$parent = $this->stack === [] ? null : $this->stack[array_key_last($this->stack)]; |
||||||
|
if (!$this->isDiscardedPosition($node, $parent)) { |
||||||
|
$this->fail($node); |
||||||
|
} |
||||||
|
$node->setAttribute(self::ALLOWED_ATTRIBUTE, true); |
||||||
|
} |
||||||
|
|
||||||
|
$this->stack[] = $node; |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
public function leaveNode(Node $node): null |
||||||
|
{ |
||||||
|
array_pop($this->stack); |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private function isDiscardedPosition(Expr\Cast\Void_ $node, ?Node $parent): bool |
||||||
|
{ |
||||||
|
if ($parent instanceof Stmt\Expression) { |
||||||
|
return $parent->expr === $node; |
||||||
|
} |
||||||
|
if (!$parent instanceof Stmt\For_) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
if ($this->containsIdenticalNode($parent->init, $node) |
||||||
|
|| $this->containsIdenticalNode($parent->loop, $node)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
$lastCondition = array_key_last($parent->cond); |
||||||
|
foreach ($parent->cond as $index => $condition) { |
||||||
|
if ($condition === $node) { |
||||||
|
// Only the final condition controls the loop. Earlier items |
||||||
|
// are evaluated solely for side effects and may be void casts. |
||||||
|
return $index !== $lastCondition; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** @param list<Expr> $nodes */ |
||||||
|
private function containsIdenticalNode(array $nodes, Node $needle): bool |
||||||
|
{ |
||||||
|
foreach ($nodes as $node) { |
||||||
|
if ($node === $needle) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
private function fail(Node $node): never |
||||||
|
{ |
||||||
|
if ($this->fatalError !== null) { |
||||||
|
($this->fatalError)($node, self::ERROR_MESSAGE); |
||||||
|
} |
||||||
|
throw new SyntaxError(self::ERROR_MESSAGE); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
--TEST-- |
||||||
|
PHP 8.5 void cast preserves operand side effects in every discarded position |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
function void_cast_mark(string $label, int $value): int |
||||||
|
{ |
||||||
|
echo $label, "\n"; |
||||||
|
return $value; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$value = 0; |
||||||
|
(void) void_cast_mark('statement', 1); |
||||||
|
(void) ($value = void_cast_mark('assignment operand', 7)); |
||||||
|
(void) $value; |
||||||
|
|
||||||
|
for ( |
||||||
|
(void) void_cast_mark('for init', 0), $i = 0; |
||||||
|
(void) @void_cast_mark('for condition', 0), $i < 2; |
||||||
|
(void) void_cast_mark('for loop', 0), $i++ |
||||||
|
) { |
||||||
|
echo 'body:', $i, "\n"; |
||||||
|
} |
||||||
|
|
||||||
|
var_dump($value, $i); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
statement |
||||||
|
assignment operand |
||||||
|
for init |
||||||
|
for condition |
||||||
|
body:0 |
||||||
|
for loop |
||||||
|
for condition |
||||||
|
body:1 |
||||||
|
for loop |
||||||
|
for condition |
||||||
|
int(7) |
||||||
|
int(2) |
||||||
Loading…
Reference in new issue