gl-website-deployer/admin/phpMyAdmin/libraries/classes/Command/TwigLintCommand.php
2024-11-19 08:02:04 +01:00

272 lines
8.5 KiB
PHP

<?php
declare(strict_types=1);
namespace PhpMyAdmin\Command;
use PhpMyAdmin\Template;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Twig\Error\Error;
use Twig\Loader\ArrayLoader;
use Twig\Source;
use function array_push;
use function closedir;
use function count;
use function explode;
use function file_get_contents;
use function is_dir;
use function is_file;
use function max;
use function min;
use function opendir;
use function preg_match;
use function readdir;
use function restore_error_handler;
use function set_error_handler;
use function sprintf;
use const DIRECTORY_SEPARATOR;
use const E_USER_DEPRECATED;
/**
* Command that will validate your template syntax and output encountered errors.
* Author: Marc Weistroff <marc.weistroff@sensiolabs.com>
* Author: Jérôme Tamarelle <jerome@tamarelle.net>
*
* Copyright (c) 2013-2021 Fabien Potencier
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is furnished
* to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
class TwigLintCommand extends Command
{
/** @var string|null */
protected static $defaultName = 'lint:twig';
/** @var string|null */
protected static $defaultDescription = 'Lint a Twig template and outputs encountered errors';
protected function configure(): void
{
$this
->setDescription((string) self::$defaultDescription)
->addOption('show-deprecations', null, InputOption::VALUE_NONE, 'Show deprecations as errors');
}
protected function findFiles(string $baseFolder): array
{
/* Open the handle */
$handle = @opendir($baseFolder);
if ($handle === false) {
return [];
}
$foundFiles = [];
while (($file = readdir($handle)) !== false) {
if ($file === '.' || $file === '..') {
continue;
}
$itemPath = $baseFolder . DIRECTORY_SEPARATOR . $file;
if (is_dir($itemPath)) {
array_push($foundFiles, ...$this->findFiles($itemPath));
continue;
}
if (! is_file($itemPath)) {
continue;
}
$foundFiles[] = $itemPath;
}
/* Close the handle */
closedir($handle);
return $foundFiles;
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$showDeprecations = $input->getOption('show-deprecations');
if ($showDeprecations) {
$prevErrorHandler = set_error_handler(
static function (int $level, string $message, string $file, int $line) use (&$prevErrorHandler) {
if ($level === E_USER_DEPRECATED) {
$templateLine = 0;
if (preg_match('/ at line (\d+)[ .]/', $message, $matches)) {
$templateLine = (int) $matches[1];
}
throw new Error($message, $templateLine);
}
return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false;
}
);
}
try {
$filesInfo = $this->getFilesInfo(ROOT_PATH . 'templates');
} finally {
if ($showDeprecations) {
restore_error_handler();
}
}
return $this->display($output, $io, $filesInfo);
}
protected function getFilesInfo(string $templatesPath): array
{
$filesInfo = [];
$filesFound = $this->findFiles($templatesPath);
foreach ($filesFound as $file) {
$filesInfo[] = $this->validate($this->getTemplateContents($file), $file);
}
return $filesInfo;
}
/**
* Allows easier testing
*/
protected function getTemplateContents(string $filePath): string
{
return (string) file_get_contents($filePath);
}
private function validate(string $template, string $file): array
{
$twig = Template::getTwigEnvironment(null);
$realLoader = $twig->getLoader();
try {
$temporaryLoader = new ArrayLoader([$file => $template]);
$twig->setLoader($temporaryLoader);
$nodeTree = $twig->parse($twig->tokenize(new Source($template, $file)));
$twig->compile($nodeTree);
$twig->setLoader($realLoader);
} catch (Error $e) {
$twig->setLoader($realLoader);
return [
'template' => $template,
'file' => $file,
'line' => $e->getTemplateLine(),
'valid' => false,
'exception' => $e,
];
}
return ['template' => $template, 'file' => $file, 'valid' => true];
}
private function display(OutputInterface $output, SymfonyStyle $io, array $filesInfo): int
{
$errors = 0;
foreach ($filesInfo as $info) {
if ($info['valid'] && $output->isVerbose()) {
$io->comment('<info>OK</info>' . ($info['file'] ? sprintf(' in %s', $info['file']) : ''));
} elseif (! $info['valid']) {
++$errors;
$this->renderException($io, $info['template'], $info['exception'], $info['file']);
}
}
if ($errors === 0) {
$io->success(sprintf('All %d Twig files contain valid syntax.', count($filesInfo)));
return Command::SUCCESS;
}
$io->warning(
sprintf(
'%d Twig files have valid syntax and %d contain errors.',
count($filesInfo) - $errors,
$errors
)
);
return Command::FAILURE;
}
private function renderException(
SymfonyStyle $output,
string $template,
Error $exception,
?string $file = null
): void {
$line = $exception->getTemplateLine();
if ($file) {
$output->text(sprintf('<error> ERROR </error> in %s (line %s)', $file, $line));
} else {
$output->text(sprintf('<error> ERROR </error> (line %s)', $line));
}
// If the line is not known (this might happen for deprecations if we fail at detecting the line for instance),
// we render the message without context, to ensure the message is displayed.
if ($line <= 0) {
$output->text(sprintf('<error> >> %s</error> ', $exception->getRawMessage()));
return;
}
foreach ($this->getContext($template, $line) as $lineNumber => $code) {
$output->text(sprintf(
'%s %-6s %s',
$lineNumber === $line ? '<error> >> </error>' : ' ',
$lineNumber,
$code
));
if ($lineNumber !== $line) {
continue;
}
$output->text(sprintf('<error> >> %s</error> ', $exception->getRawMessage()));
}
}
private function getContext(string $template, int $line, int $context = 3): array
{
$lines = explode("\n", $template);
$position = max(0, $line - $context);
$max = min(count($lines), $line - 1 + $context);
$result = [];
while ($position < $max) {
$result[$position + 1] = $lines[$position];
++$position;
}
return $result;
}
}