|
4 | 4 |
|
5 | 5 | namespace Symfony\CodeBlockChecker\Service; |
6 | 6 |
|
7 | | -use Doctrine\RST\Nodes\CodeNode; |
8 | | -use Symfony\CodeBlockChecker\Issue\Issue; |
9 | 7 | use Symfony\CodeBlockChecker\Issue\IssueCollection; |
10 | | -use Symfony\CodeBlockChecker\Twig\DummyExtension; |
11 | | -use Symfony\Component\Process\Process; |
12 | | -use Symfony\Component\Yaml\Exception\ParseException; |
13 | | -use Symfony\Component\Yaml\Yaml; |
14 | | -use Twig\Environment; |
15 | | -use Twig\Error\SyntaxError; |
16 | | -use Twig\Loader\ArrayLoader; |
17 | | -use Twig\Source; |
| 8 | +use Symfony\CodeBlockChecker\Service\CodeValidator\Validator; |
18 | 9 |
|
19 | 10 | /** |
20 | 11 | * Verify that all code nodes has the correct syntax. |
|
23 | 14 | */ |
24 | 15 | class CodeValidator |
25 | 16 | { |
26 | | - private $twig; |
27 | | - private IssueCollection $issues; |
28 | | - |
29 | | - public function validateNodes(array $nodes): IssueCollection |
30 | | - { |
31 | | - $this->issues = new IssueCollection(); |
32 | | - foreach ($nodes as $node) { |
33 | | - $this->validateNode($node); |
34 | | - } |
35 | | - |
36 | | - return $this->issues; |
37 | | - } |
38 | | - |
39 | | - private function validateNode(CodeNode $node): void |
40 | | - { |
41 | | - $language = $node->getLanguage() ?? ($node->isRaw() ? null : 'php'); |
42 | | - if (in_array($language, ['php', 'php-symfony', 'php-standalone', 'php-annotations'])) { |
43 | | - $this->validatePhp($node); |
44 | | - } elseif ('yaml' === $language) { |
45 | | - $this->validateYaml($node); |
46 | | - } elseif ('xml' === $language) { |
47 | | - $this->validateXml($node); |
48 | | - } elseif ('json' === $language) { |
49 | | - $this->validateJson($node); |
50 | | - } elseif (in_array($language, ['twig', 'html+twig'])) { |
51 | | - $this->validateTwig($node); |
52 | | - } |
53 | | - } |
54 | | - |
55 | | - private function validatePhp(CodeNode $node) |
56 | | - { |
57 | | - $file = sys_get_temp_dir().'/'.uniqid('doc_builder', true).'.php'; |
58 | | - $contents = $node->getValue(); |
59 | | - if (!preg_match('#class [a-zA-Z]+#s', $contents) && preg_match('#(public|protected|private) (\$[a-z]+|function)#s', $contents)) { |
60 | | - $contents = 'class Foobar {'.$contents.'}'; |
61 | | - } |
62 | | - |
63 | | - // Allow us to use "..." as a placeholder |
64 | | - $contents = str_replace('...', 'null', $contents); |
65 | | - file_put_contents($file, '<?php'.PHP_EOL.$contents); |
66 | | - |
67 | | - $process = new Process(['php', '-l', $file]); |
68 | | - $process->run(); |
69 | | - if ($process->isSuccessful()) { |
70 | | - return; |
71 | | - } |
72 | | - |
73 | | - $line = 0; |
74 | | - $text = str_replace($file, 'example.php', $process->getErrorOutput()); |
75 | | - if (preg_match('| in example.php on line ([0-9]+)|s', $text, $matches)) { |
76 | | - $text = str_replace($matches[0], '', $text); |
77 | | - $line = ((int) $matches[1]) - 1; // we added "<?php" |
78 | | - } |
79 | | - $this->issues->addIssue(new Issue($node, $text, 'Invalid syntax', $node->getEnvironment()->getCurrentFileName(), $line)); |
80 | | - } |
81 | | - |
82 | | - private function validateXml(CodeNode $node) |
| 17 | + /** |
| 18 | + * @var iterable<Validator> |
| 19 | + */ |
| 20 | + private $validators; |
| 21 | + |
| 22 | + /** |
| 23 | + * @param iterable<Validator> $validators |
| 24 | + */ |
| 25 | + public function __construct(iterable $validators) |
83 | 26 | { |
84 | | - try { |
85 | | - set_error_handler(static function ($errno, $errstr) { |
86 | | - throw new \RuntimeException($errstr, $errno); |
87 | | - }); |
88 | | - |
89 | | - try { |
90 | | - // Remove first comment only. (No multiline) |
91 | | - $xml = preg_replace('#^<!-- .* -->\n#', '', $node->getValue()); |
92 | | - if ('' !== $xml) { |
93 | | - $xmlObject = new \SimpleXMLElement($xml); |
94 | | - } |
95 | | - } finally { |
96 | | - restore_error_handler(); |
97 | | - } |
98 | | - } catch (\Throwable $e) { |
99 | | - if ('SimpleXMLElement::__construct(): namespace error : Namespace prefix' === substr($e->getMessage(), 0, 67)) { |
100 | | - return; |
101 | | - } |
102 | | - |
103 | | - $this->issues->addIssue(new Issue($node, $e->getMessage(), 'Invalid syntax', $node->getEnvironment()->getCurrentFileName(), 0)); |
104 | | - } |
| 27 | + $this->validators = $validators; |
105 | 28 | } |
106 | 29 |
|
107 | | - private function validateYaml(CodeNode $node) |
| 30 | + public function validateNodes(array $nodes): IssueCollection |
108 | 31 | { |
109 | | - // Allow us to use "..." as a placeholder |
110 | | - $contents = str_replace('...', 'null', $node->getValue()); |
111 | | - try { |
112 | | - Yaml::parse($contents, Yaml::PARSE_CUSTOM_TAGS); |
113 | | - } catch (ParseException $e) { |
114 | | - if ('Duplicate key' === substr($e->getMessage(), 0, 13)) { |
115 | | - return; |
| 32 | + $issues = new IssueCollection(); |
| 33 | + foreach ($nodes as $node) { |
| 34 | + foreach ($this->validators as $validator) { |
| 35 | + $validator->validate($node, $issues); |
116 | 36 | } |
117 | | - |
118 | | - $this->issues->addIssue(new Issue($node, $e->getMessage(), 'Invalid syntax', $node->getEnvironment()->getCurrentFileName(), 0)); |
119 | 37 | } |
120 | | - } |
121 | 38 |
|
122 | | - private function validateTwig(CodeNode $node) |
123 | | - { |
124 | | - if (null === $this->twig) { |
125 | | - $this->twig = new Environment(new ArrayLoader()); |
126 | | - $this->twig->addExtension(new DummyExtension()); |
127 | | - } |
128 | | - |
129 | | - try { |
130 | | - $tokens = $this->twig->tokenize(new Source($node->getValue(), $node->getEnvironment()->getCurrentFileName())); |
131 | | - // We cannot parse the TokenStream because we dont have all extensions loaded. |
132 | | - $this->twig->parse($tokens); |
133 | | - } catch (SyntaxError $e) { |
134 | | - $this->issues->addIssue(new Issue($node, $e->getMessage(), 'Invalid syntax', $node->getEnvironment()->getCurrentFileName(), 0)); |
135 | | - } |
136 | | - } |
137 | | - |
138 | | - private function validateJson(CodeNode $node) |
139 | | - { |
140 | | - try { |
141 | | - $data = json_decode($node->getValue(), true, 512, JSON_THROW_ON_ERROR); |
142 | | - } catch (\JsonException $e) { |
143 | | - $this->issues->addIssue(new Issue($node, $e->getMessage(), 'Invalid syntax', $node->getEnvironment()->getCurrentFileName(), 0)); |
144 | | - } |
| 39 | + return $issues; |
145 | 40 | } |
146 | 41 | } |
0 commit comments