From 06e2948f6d266afc2d0d02bf357c5778c87a10ab Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 7 Aug 2023 03:39:33 +0200 Subject: [PATCH] Engine::setPhpBinary() allows to lint generated PHP templates --- src/Latte/Compiler/PhpHelpers.php | 30 ++++++++++++++++++++++++++++ src/Latte/Compiler/Position.php | 2 +- src/Latte/Engine.php | 15 ++++++++++++++ src/Tools/Linter.php | 33 ++----------------------------- tests/common/linter.error.phpt | 18 +++++++++++++++++ 5 files changed, 66 insertions(+), 32 deletions(-) create mode 100644 tests/common/linter.error.phpt diff --git a/src/Latte/Compiler/PhpHelpers.php b/src/Latte/Compiler/PhpHelpers.php index a1b7583d9..920763b9c 100644 --- a/src/Latte/Compiler/PhpHelpers.php +++ b/src/Latte/Compiler/PhpHelpers.php @@ -224,4 +224,34 @@ private static function codePointToUtf8(int $num): string default => throw new CompileException('Invalid UTF-8 codepoint escape sequence: Codepoint too large'), }; } + + + public static function checkCode(string $phpBinary, string $code, string $name): void + { + $process = proc_open( + $phpBinary . ' -l -n', + [['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']], + $pipes, + null, + null, + ['bypass_shell' => true], + ); + if (!is_resource($process)) { + throw new CompileException('Unable to check that the generated PHP is correct.'); + } + + fwrite($pipes[0], $code); + fclose($pipes[0]); + $error = stream_get_contents($pipes[1]); + if (!proc_close($process)) { + return; + } + $error = strip_tags(explode("\n", $error)[1]); + $position = preg_match('~ on line (\d+)~', $error, $m) + ? new Position((int) $m[1], 0) + : null; + $error = preg_replace('~(^Fatal error: | in Standard input code| on line \d+)~', '', $error); + throw (new CompileException('Error in generated code: ' . trim($error), $position)) + ->setSource($code, $name); + } } diff --git a/src/Latte/Compiler/Position.php b/src/Latte/Compiler/Position.php index b048c2820..d0577889c 100644 --- a/src/Latte/Compiler/Position.php +++ b/src/Latte/Compiler/Position.php @@ -44,6 +44,6 @@ public function advance(string $str): self public function toWords(): string { - return "on line $this->line at column $this->column"; + return "on line $this->line" . ($this->column ? " at column $this->column" : ''); } } diff --git a/src/Latte/Engine.php b/src/Latte/Engine.php index 287b05662..71ffa0baa 100644 --- a/src/Latte/Engine.php +++ b/src/Latte/Engine.php @@ -44,6 +44,7 @@ class Engine private bool $strictTypes = false; private ?Policy $policy = null; private bool $sandboxed = false; + private ?string $phpBinary = null; public function __construct() @@ -126,6 +127,10 @@ public function compile(string $name): string throw $e->setSource($source, $name); } + if ($this->phpBinary) { + Compiler\PhpHelpers::checkCode($this->phpBinary, $code, "(compiled $name)"); + } + return $code; } @@ -544,6 +549,16 @@ public function getLoader(): Loader } + /** + * Enables linting of generated PHP templates. + */ + public function setPhpBinary(?string $phpBinary): static + { + $this->phpBinary = $phpBinary; + return $this; + } + + /** * @param object|mixed[] $params * @return mixed[] diff --git a/src/Tools/Linter.php b/src/Tools/Linter.php index 64e1f49fd..f47700da2 100644 --- a/src/Tools/Linter.php +++ b/src/Tools/Linter.php @@ -53,6 +53,7 @@ public function scanDirectory(string $path): bool private function createEngine(): Latte\Engine { $engine = new Latte\Engine; + $engine->setPhpBinary(PHP_BINARY); $engine->addExtension(new Latte\Essential\TranslatorExtension(null)); if (class_exists(Nette\Bridges\ApplicationLatte\UIExtension::class)) { @@ -91,7 +92,7 @@ public function lintLatte(string $file): bool } try { - $code = $this->engine->compile($s); + $this->engine->compile($s); } catch (Latte\CompileException $e) { if ($this->debug) { @@ -106,40 +107,10 @@ public function lintLatte(string $file): bool restore_error_handler(); } - if ($error = $this->lintPHP($code)) { - fwrite(STDERR, "[ERROR] $file $error\n"); - return false; - } - return true; } - private function lintPHP(string $code): ?string - { - $php = defined('PHP_BINARY') ? PHP_BINARY : 'php'; - $stdin = tmpfile(); - fwrite($stdin, $code); - fseek($stdin, 0); - $process = proc_open( - $php . ' -l -d display_errors=1', - [$stdin, ['pipe', 'w'], ['pipe', 'w']], - $pipes, - null, - null, - ['bypass_shell' => true], - ); - if (!is_resource($process)) { - return 'Unable to lint PHP code'; - } - $error = stream_get_contents($pipes[1]); - if (proc_close($process)) { - return strip_tags(explode("\n", $error)[1]); - } - return null; - } - - private function initialize(): void { if (function_exists('pcntl_signal')) { diff --git a/tests/common/linter.error.phpt b/tests/common/linter.error.phpt new file mode 100644 index 000000000..0d437c30b --- /dev/null +++ b/tests/common/linter.error.phpt @@ -0,0 +1,18 @@ +setLoader(new Latte\Loaders\StringLoader); +$latte->setPhpBinary(PHP_BINARY); + +Assert::exception( + fn() => $latte->compile('{= [&$x] = []}'), + Latte\CompileException::class, + 'Error in generated code: Cannot assign %a% (on line %d%)', +);