Skip to content

Commit 9f2643a

Browse files
authored
Be more SOLID (#13)
* Be more solid * Updated tests * cs
1 parent 97fd27b commit 9f2643a

File tree

13 files changed

+279
-171
lines changed

13 files changed

+279
-171
lines changed

.php_cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
$finder = PhpCsFixer\Finder::create()
44
->in(__DIR__.'/src')
5+
->in(__DIR__.'/tests')
56
;
67

78
return PhpCsFixer\Config::create()

config/services.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,16 @@ services:
88
bind:
99
string $cacheDir: '%kernel.cache_dir%'
1010

11+
_instanceof:
12+
Symfony\CodeBlockChecker\Service\CodeValidator\Validator:
13+
tags:
14+
- 'app.code_validator'
15+
1116
Symfony\CodeBlockChecker\:
1217
resource: '../src/'
1318
exclude:
1419
- '../src/Kernel.php'
1520
- '../src/Application.php'
21+
22+
Symfony\CodeBlockChecker\Service\CodeValidator:
23+
arguments: [!tagged_iterator app.code_validator]

psalm.baseline.xml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
<code>\CachedContainer</code>
66
</UndefinedClass>
77
</file>
8-
<file src="src/Service/CodeValidator.php">
9-
<InvalidArgument occurrences="3">
10-
<code>'bis'</code>
11-
<code>'foobar'</code>
12-
</InvalidArgument>
8+
<file src="src/Service/CodeValidator/XmlValidator.php">
9+
<InvalidArgument occurrences="1"/>
1310
</file>
1411
</files>

src/Service/CodeValidator.php

Lines changed: 17 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,8 @@
44

55
namespace Symfony\CodeBlockChecker\Service;
66

7-
use Doctrine\RST\Nodes\CodeNode;
8-
use Symfony\CodeBlockChecker\Issue\Issue;
97
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;
189

1910
/**
2011
* Verify that all code nodes has the correct syntax.
@@ -23,124 +14,28 @@
2314
*/
2415
class CodeValidator
2516
{
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)
8326
{
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;
10528
}
10629

107-
private function validateYaml(CodeNode $node)
30+
public function validateNodes(array $nodes): IssueCollection
10831
{
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);
11636
}
117-
118-
$this->issues->addIssue(new Issue($node, $e->getMessage(), 'Invalid syntax', $node->getEnvironment()->getCurrentFileName(), 0));
11937
}
120-
}
12138

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;
14540
}
14641
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Symfony\CodeBlockChecker\Service\CodeValidator;
4+
5+
use Doctrine\RST\Nodes\CodeNode;
6+
use Symfony\CodeBlockChecker\Issue\Issue;
7+
use Symfony\CodeBlockChecker\Issue\IssueCollection;
8+
9+
class JsonValidator implements Validator
10+
{
11+
public function validate(CodeNode $node, IssueCollection $issues): void
12+
{
13+
if ('json' !== $node->getLanguage()) {
14+
return;
15+
}
16+
17+
try {
18+
$data = json_decode($node->getValue(), true, 512, JSON_THROW_ON_ERROR);
19+
} catch (\JsonException $e) {
20+
$issues->addIssue(new Issue($node, $e->getMessage(), 'Invalid syntax', $node->getEnvironment()->getCurrentFileName(), 0));
21+
}
22+
}
23+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Symfony\CodeBlockChecker\Service\CodeValidator;
4+
5+
use Doctrine\RST\Nodes\CodeNode;
6+
use Symfony\CodeBlockChecker\Issue\Issue;
7+
use Symfony\CodeBlockChecker\Issue\IssueCollection;
8+
use Symfony\Component\Process\Process;
9+
10+
class PhpValidator implements Validator
11+
{
12+
public function validate(CodeNode $node, IssueCollection $issues): void
13+
{
14+
$language = $node->getLanguage() ?? ($node->isRaw() ? null : 'php');
15+
if (!in_array($language, ['php', 'php-symfony', 'php-standalone', 'php-annotations'])) {
16+
return;
17+
}
18+
19+
$file = sys_get_temp_dir().'/'.uniqid('doc_builder', true).'.php';
20+
$contents = $node->getValue();
21+
if (!preg_match('#class [a-zA-Z]+#s', $contents) && preg_match('#(public|protected|private) (\$[a-z]+|function)#s', $contents)) {
22+
$contents = 'class Foobar {'.$contents.'}';
23+
}
24+
25+
// Allow us to use "..." as a placeholder
26+
$contents = str_replace('...', 'null', $contents);
27+
file_put_contents($file, '<?php'.PHP_EOL.$contents);
28+
29+
$process = new Process(['php', '-l', $file]);
30+
$process->run();
31+
if ($process->isSuccessful()) {
32+
return;
33+
}
34+
35+
$line = 0;
36+
$text = str_replace($file, 'example.php', $process->getErrorOutput());
37+
if (preg_match('| in example.php on line ([0-9]+)|s', $text, $matches)) {
38+
$text = str_replace($matches[0], '', $text);
39+
$line = ((int) $matches[1]) - 1; // we added "<?php"
40+
}
41+
$issues->addIssue(new Issue($node, $text, 'Invalid syntax', $node->getEnvironment()->getCurrentFileName(), $line));
42+
}
43+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace Symfony\CodeBlockChecker\Service\CodeValidator;
4+
5+
use Doctrine\RST\Nodes\CodeNode;
6+
use Symfony\CodeBlockChecker\Issue\Issue;
7+
use Symfony\CodeBlockChecker\Issue\IssueCollection;
8+
use Symfony\CodeBlockChecker\Twig\DummyExtension;
9+
use Twig\Environment;
10+
use Twig\Error\SyntaxError;
11+
use Twig\Loader\ArrayLoader;
12+
use Twig\Source;
13+
14+
class TwigValidator implements Validator
15+
{
16+
private $twig;
17+
18+
public function validate(CodeNode $node, IssueCollection $issues): void
19+
{
20+
if (!in_array($node->getLanguage(), ['twig', 'html+twig'])) {
21+
return;
22+
}
23+
24+
if (null === $this->twig) {
25+
$this->twig = new Environment(new ArrayLoader());
26+
$this->twig->addExtension(new DummyExtension());
27+
}
28+
29+
try {
30+
$tokens = $this->twig->tokenize(new Source($node->getValue(), $node->getEnvironment()->getCurrentFileName()));
31+
// We cannot parse the TokenStream because we dont have all extensions loaded.
32+
$this->twig->parse($tokens);
33+
} catch (SyntaxError $e) {
34+
$issues->addIssue(new Issue($node, $e->getMessage(), 'Invalid syntax', $node->getEnvironment()->getCurrentFileName(), 0));
35+
}
36+
}
37+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Symfony\CodeBlockChecker\Service\CodeValidator;
4+
5+
use Doctrine\RST\Nodes\CodeNode;
6+
use Symfony\CodeBlockChecker\Issue\IssueCollection;
7+
8+
interface Validator
9+
{
10+
public function validate(CodeNode $node, IssueCollection $issues): void;
11+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Symfony\CodeBlockChecker\Service\CodeValidator;
4+
5+
use Doctrine\RST\Nodes\CodeNode;
6+
use Symfony\CodeBlockChecker\Issue\Issue;
7+
use Symfony\CodeBlockChecker\Issue\IssueCollection;
8+
9+
class XmlValidator implements Validator
10+
{
11+
public function validate(CodeNode $node, IssueCollection $issues): void
12+
{
13+
if ('xml' !== $node->getLanguage()) {
14+
return;
15+
}
16+
17+
try {
18+
set_error_handler(static function ($errno, $errstr) {
19+
throw new \RuntimeException($errstr, $errno);
20+
});
21+
22+
try {
23+
// Remove first comment only. (No multiline)
24+
$xml = preg_replace('#^<!-- .* -->\n#', '', $node->getValue());
25+
if ('' !== $xml) {
26+
$xmlObject = new \SimpleXMLElement($xml);
27+
}
28+
} finally {
29+
restore_error_handler();
30+
}
31+
} catch (\Throwable $e) {
32+
if ('SimpleXMLElement::__construct(): namespace error : Namespace prefix' === substr($e->getMessage(), 0, 67)) {
33+
return;
34+
}
35+
36+
$issues->addIssue(new Issue($node, $e->getMessage(), 'Invalid syntax', $node->getEnvironment()->getCurrentFileName(), 0));
37+
}
38+
}
39+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Symfony\CodeBlockChecker\Service\CodeValidator;
4+
5+
use Doctrine\RST\Nodes\CodeNode;
6+
use Symfony\CodeBlockChecker\Issue\Issue;
7+
use Symfony\CodeBlockChecker\Issue\IssueCollection;
8+
use Symfony\Component\Yaml\Exception\ParseException;
9+
use Symfony\Component\Yaml\Yaml;
10+
11+
class YamlValidator implements Validator
12+
{
13+
public function validate(CodeNode $node, IssueCollection $issues): void
14+
{
15+
if ('yaml' !== $node->getLanguage()) {
16+
return;
17+
}
18+
19+
// Allow us to use "..." as a placeholder
20+
$contents = str_replace('...', 'null', $node->getValue());
21+
try {
22+
Yaml::parse($contents, Yaml::PARSE_CUSTOM_TAGS);
23+
} catch (ParseException $e) {
24+
if ('Duplicate key' === substr($e->getMessage(), 0, 13)) {
25+
return;
26+
}
27+
28+
$issues->addIssue(new Issue($node, $e->getMessage(), 'Invalid syntax', $node->getEnvironment()->getCurrentFileName(), 0));
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)