diff --git a/examples/tetris-win32/main.php b/examples/tetris-win32/main.php index 2c0daf18..e991da04 100644 --- a/examples/tetris-win32/main.php +++ b/examples/tetris-win32/main.php @@ -42,8 +42,8 @@ function rgb(int $r, int $g, int $b): int } // Piece colors -const COLOR_CYAN = 0x00FFFF; // I -const COLOR_YELLOW = 0x00FFFF; // O - will override below +const COLOR_CYAN = 0xFFFF00; // I +const COLOR_YELLOW = 0x00FFFF; // O const COLOR_PURPLE = 0x800080; // T const COLOR_GREEN = 0x00FF00; // S const COLOR_RED = 0x0000FF; // Z @@ -51,13 +51,13 @@ const COLOR_BLUE = 0xFF0000; // J const COLOR_ORANGE = 0x00A5FF; // L const PIECE_COLORS = [ - rgb(0, 255, 255), // I - Cyan - rgb(255, 255, 0), // O - Yellow - rgb(128, 0, 128), // T - Purple - rgb(0, 255, 0), // S - Green - rgb(255, 0, 0), // Z - Red - rgb(0, 0, 255), // J - Blue - rgb(255, 165, 0), // L - Orange + COLOR_CYAN, + COLOR_YELLOW, + COLOR_PURPLE, + COLOR_GREEN, + COLOR_RED, + COLOR_BLUE, + COLOR_ORANGE, ]; // 7 tetromino shapes (4 rotations each, 4x4 grid) - defined in PHP! diff --git a/phpunit/src/ConstantExpressionValidatorTest.php b/phpunit/src/ConstantExpressionValidatorTest.php index d8db73ce..e7f81e06 100644 --- a/phpunit/src/ConstantExpressionValidatorTest.php +++ b/phpunit/src/ConstantExpressionValidatorTest.php @@ -258,6 +258,27 @@ final class ConstantExpressionValidatorTest extends PHPUnit\Framework\TestCase )); } + public function testCompilerBoundaryRoutesUnsupportedSyntaxThroughFatalDiagnostic(): void + { + $parser = (new ParserFactory())->createForVersion(PhpVersion::fromString('8.5')); + $statements = $parser->parse("addVisitor(new ConstantExpressionValidationVisitor( + '8.5', + static function (Node $node, string $message): never { + throw new \RuntimeException("fatal: {$message} at line {$node->getStartLine()}"); + }, + )); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage( + 'fatal: Constant expression contains invalid operations at line 2', + ); + $traverser->traverse($statements); + } + private function parseAttributeExpression(string $expression, string $phpVersion): Node\Expr { return $this->parseAttributeArguments($expression, $phpVersion)[0]->value; diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 877cc4f2..a3ace405 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -40,7 +40,7 @@ use PhpParser\NodeVisitor\NameResolver; class Preprocessor extends CompilerBase { - protected function getSortedFiles(array $list): array + public function getSortedFiles(array $list): array { $sorter = new StringSort(); $fileDeps = []; @@ -149,7 +149,10 @@ class Preprocessor extends CompilerBase fn (Node $node, string $message) => $this->warning($node, $message), $this->file, )); - $traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion)); + $traverser->addVisitor(new ConstantExpressionValidationVisitor( + $this->phpVersion, + fn (Node $node, string $message) => $this->fatalError($node, $message), + )); $traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file)); $stmts = $traverser->traverse($ast); diff --git a/src/Transform/ConstantExpressionValidationVisitor.php b/src/Transform/ConstantExpressionValidationVisitor.php index 791c0e0a..99fafd56 100644 --- a/src/Transform/ConstantExpressionValidationVisitor.php +++ b/src/Transform/ConstantExpressionValidationVisitor.php @@ -8,8 +8,10 @@ namespace TypePhp\Transform; +use Closure; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use TypePhp\Exception\SyntaxError; /** * Applies the allow_dynamic values used by php-src at each declaration site. @@ -26,13 +28,29 @@ final class ConstantExpressionValidationVisitor extends NodeVisitorAbstract private readonly bool $supportsDynamicStaticInitializers; - public function __construct(string $phpVersion) + /** @param null|Closure(Node, string): never $fatalError */ + public function __construct( + string $phpVersion, + private readonly ?Closure $fatalError = null, + ) { $this->validator = new ConstantExpressionValidator($phpVersion); $this->supportsDynamicStaticInitializers = version_compare($phpVersion, '8.3', '>='); } public function enterNode(Node $node): null + { + try { + return $this->validateNode($node); + } catch (SyntaxError $error) { + if ($this->fatalError !== null) { + ($this->fatalError)($node, $error->getMessage()); + } + throw $error; + } + } + + private function validateNode(Node $node): null { if ($node instanceof Node\Attribute) { $this->validator->validateArguments( diff --git a/src/Transform/ConstantExpressionValidator.php b/src/Transform/ConstantExpressionValidator.php index 972b45dc..16bcec3d 100644 --- a/src/Transform/ConstantExpressionValidator.php +++ b/src/Transform/ConstantExpressionValidator.php @@ -321,7 +321,11 @@ final class ConstantExpressionValidator private function tryEvaluate(Expr $expression): array { try { - $value = (new ConstExprEvaluator())->evaluateDirectly($expression); + $value = (new ConstExprEvaluator( + static function (): never { + throw new \LogicException('Expression is not statically known'); + }, + ))->evaluateDirectly($expression); return [true, $value]; } catch (\Throwable) { return [false, null]; diff --git a/src/Transform/Visitor.php b/src/Transform/Visitor.php index c097143d..715612c5 100644 --- a/src/Transform/Visitor.php +++ b/src/Transform/Visitor.php @@ -54,16 +54,22 @@ class Visitor extends NodeVisitorAbstract $classReadonly = $node instanceof Stmt\Class_ && $node->isReadonly(); foreach ($node->stmts as $stmt) { if ($stmt instanceof Stmt\Property) { - array_push($methods, ...PropertyHookLowering::lowerProperty($stmt)); - array_push($methods, ...$this->guard( + foreach (PropertyHookLowering::lowerProperty($stmt) as $method) { + $methods[] = $method; + } + foreach ($this->guard( $stmt, static fn () => GetterLowering::lowerProperty($stmt), 'Getter', - )); - array_push($methods, ...$this->guard( + ) as $method) { + $methods[] = $method; + } + foreach ($this->guard( $stmt, static fn () => PropertyMethodLowering::lowerProperty($stmt, $classReadonly), - )); + ) as $method) { + $methods[] = $method; + } } elseif ($stmt instanceof Stmt\ClassMethod && $stmt->name->toLowerString() === '__construct') { foreach ($stmt->params as $param) { $marker = PropertyHookLowering::lowerPromotedProperty($param); @@ -78,15 +84,19 @@ class Visitor extends NodeVisitorAbstract if ($getter !== null) { $methods[] = $getter; } - array_push($methods, ...$this->guard( + foreach ($this->guard( $param, static fn () => PropertyMethodLowering::lowerPromotedProperty($param, $classReadonly), - )); + ) as $method) { + $methods[] = $method; + } } } } if ($methods !== []) { - array_push($node->stmts, ...$methods); + foreach ($methods as $method) { + $node->stmts[] = $method; + } } $this->guard($node, static fn () => ConstructorLowering::lowerClassLike($node), 'Constructor'); if ($node instanceof Stmt\Class_) { diff --git a/src/Translator.php b/src/Translator.php index 542d0278..67f5d479 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -2313,7 +2313,10 @@ CODE; $traverser = new NodeTraverser(); $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); $traverser->addVisitor(new Visitor(sourceFile: $this->file)); - $traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion)); + $traverser->addVisitor(new ConstantExpressionValidationVisitor( + $this->phpVersion, + fn (Node $node, string $message) => $this->fatalError($node, $message), + )); $traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file)); $stmts = $traverser->traverse($ast); diff --git a/tests/compiler/array/array-push-empty-unpack.phpt b/tests/compiler/array/array-push-empty-unpack.phpt index a82e7656..670c2441 100644 --- a/tests/compiler/array/array-push-empty-unpack.phpt +++ b/tests/compiler/array/array-push-empty-unpack.phpt @@ -5,6 +5,7 @@ array_push retains its required array argument when an unpacked list is empty function main(): void { + // Keep the required by-reference argument before an empty unpack. $values = []; array_push($values, ...[]); var_dump($values);