Translated using Weblate (Portuguese)
[phpmyadmin.git] / src / Command / TwigLintCommand.php
blobc44963ebefa683e54795d3f35bb3c5465278b86f
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin\Command;
7 use PhpMyAdmin\Template;
8 use Symfony\Component\Console\Attribute\AsCommand;
9 use Symfony\Component\Console\Command\Command;
10 use Symfony\Component\Console\Input\InputInterface;
11 use Symfony\Component\Console\Input\InputOption;
12 use Symfony\Component\Console\Output\OutputInterface;
13 use Symfony\Component\Console\Style\SymfonyStyle;
14 use Twig\Error\Error;
15 use Twig\Loader\ArrayLoader;
16 use Twig\Source;
18 use function array_push;
19 use function closedir;
20 use function count;
21 use function explode;
22 use function file_get_contents;
23 use function is_dir;
24 use function is_file;
25 use function max;
26 use function min;
27 use function opendir;
28 use function preg_match;
29 use function readdir;
30 use function restore_error_handler;
31 use function set_error_handler;
32 use function sprintf;
34 use const DIRECTORY_SEPARATOR;
35 use const E_USER_DEPRECATED;
37 /**
38 * Command that will validate your template syntax and output encountered errors.
39 * Author: Marc Weistroff <marc.weistroff@sensiolabs.com>
40 * Author: Jérôme Tamarelle <jerome@tamarelle.net>
42 * Copyright (c) 2013-2021 Fabien Potencier
44 * Permission is hereby granted, free of charge, to any person obtaining a copy
45 * of this software and associated documentation files (the "Software"), to deal
46 * in the Software without restriction, including without limitation the rights
47 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
48 * copies of the Software, and to permit persons to whom the Software is furnished
49 * to do so, subject to the following conditions:
51 * The above copyright notice and this permission notice shall be included in all
52 * copies or substantial portions of the Software.
54 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
55 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
56 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
57 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
58 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
59 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
60 * THE SOFTWARE.
62 #[AsCommand(name: 'lint:twig', description: 'Lint a Twig template and outputs encountered errors.')]
63 class TwigLintCommand extends Command
65 protected function configure(): void
67 $this->addOption('show-deprecations', null, InputOption::VALUE_NONE, 'Show deprecations as errors');
70 /** @return string[] */
71 protected function findFiles(string $baseFolder): array
73 /* Open the handle */
74 $handle = @opendir($baseFolder);
75 if ($handle === false) {
76 return [];
79 $foundFiles = [];
81 /** @infection-ignore-all */
82 while (($file = readdir($handle)) !== false) {
83 if ($file === '.' || $file === '..') {
84 continue;
87 $itemPath = $baseFolder . DIRECTORY_SEPARATOR . $file;
89 if (is_dir($itemPath)) {
90 array_push($foundFiles, ...$this->findFiles($itemPath));
91 continue;
94 if (! is_file($itemPath)) {
95 continue;
98 $foundFiles[] = $itemPath;
101 /* Close the handle */
102 closedir($handle);
104 return $foundFiles;
107 protected function execute(InputInterface $input, OutputInterface $output): int
109 $io = new SymfonyStyle($input, $output);
110 $showDeprecations = $input->getOption('show-deprecations');
112 if ($showDeprecations) {
113 $prevErrorHandler = set_error_handler(
114 static function (int $level, string $message, string $file, int $line) use (&$prevErrorHandler) {
115 if ($level === E_USER_DEPRECATED) {
116 $templateLine = 0;
117 if (preg_match('/ at line (\d+)[ .]/', $message, $matches)) {
118 $templateLine = (int) $matches[1];
121 throw new Error($message, $templateLine);
124 return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false;
129 try {
130 $filesInfo = $this->getFilesInfo(ROOT_PATH . 'resources/templates');
131 } finally {
132 if ($showDeprecations) {
133 restore_error_handler();
137 return $this->display($output, $io, $filesInfo);
140 /** @return mixed[] */
141 protected function getFilesInfo(string $templatesPath): array
143 $filesInfo = [];
144 $filesFound = $this->findFiles($templatesPath);
145 foreach ($filesFound as $file) {
146 $filesInfo[] = $this->validate($this->getTemplateContents($file), $file);
149 return $filesInfo;
153 * Allows easier testing
155 protected function getTemplateContents(string $filePath): string
157 return (string) file_get_contents($filePath);
160 /** @return mixed[] */
161 private function validate(string $template, string $file): array
163 $twig = Template::getTwigEnvironment(null, false);
165 $realLoader = $twig->getLoader();
166 try {
167 $temporaryLoader = new ArrayLoader([$file => $template]);
168 $twig->setLoader($temporaryLoader);
169 $nodeTree = $twig->parse($twig->tokenize(new Source($template, $file)));
170 $twig->compile($nodeTree);
171 $twig->setLoader($realLoader);
172 } catch (Error $e) {
173 $twig->setLoader($realLoader);
175 return [
176 'template' => $template,
177 'file' => $file,
178 'line' => $e->getTemplateLine(),
179 'valid' => false,
180 'exception' => $e,
184 return ['template' => $template, 'file' => $file, 'valid' => true];
187 /** @param mixed[] $filesInfo */
188 private function display(OutputInterface $output, SymfonyStyle $io, array $filesInfo): int
190 $errors = 0;
192 foreach ($filesInfo as $info) {
193 if ($info['valid'] && $output->isVerbose()) {
194 $io->comment('<info>OK</info>' . ($info['file'] ? sprintf(' in %s', $info['file']) : ''));
195 } elseif (! $info['valid']) {
196 ++$errors;
197 $this->renderException($io, $info['template'], $info['exception'], $info['file']);
201 if ($errors === 0) {
202 $io->success(sprintf('All %d Twig files contain valid syntax.', count($filesInfo)));
204 return Command::SUCCESS;
207 $io->warning(
208 sprintf(
209 '%d Twig files have valid syntax and %d contain errors.',
210 count($filesInfo) - $errors,
211 $errors,
215 return Command::FAILURE;
218 private function renderException(
219 SymfonyStyle $output,
220 string $template,
221 Error $exception,
222 string|null $file = null,
223 ): void {
224 $line = $exception->getTemplateLine();
226 if ($file !== null && $file !== '') {
227 $output->text(sprintf('<error> ERROR </error> in %s (line %s)', $file, $line));
228 } else {
229 $output->text(sprintf('<error> ERROR </error> (line %s)', $line));
232 // If the line is not known (this might happen for deprecations if we fail at detecting the line for instance),
233 // we render the message without context, to ensure the message is displayed.
234 if ($line <= 0) {
235 $output->text(sprintf('<error> >> %s</error> ', $exception->getRawMessage()));
237 return;
240 foreach ($this->getContext($template, $line) as $lineNumber => $code) {
241 $output->text(sprintf(
242 '%s %-6s %s',
243 $lineNumber === $line ? '<error> >> </error>' : ' ',
244 $lineNumber,
245 $code,
247 if ($lineNumber !== $line) {
248 continue;
251 $output->text(sprintf('<error> >> %s</error> ', $exception->getRawMessage()));
255 /** @return string[] */
256 private function getContext(string $template, int $line, int $context = 3): array
258 $lines = explode("\n", $template);
260 $position = max(0, $line - $context);
261 $max = min(count($lines), $line - 1 + $context);
263 $result = [];
264 while ($position < $max) {
265 $result[$position + 1] = $lines[$position];
266 /** @infection-ignore-all */
267 ++$position;
270 return $result;