Skip to content

Speed up things by using nikic/php-parser #40

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"ext-iconv": "*",
"ext-libxml": "*",
"doctrine/rst-parser": "^0.4",
"nikic/php-parser": "^4.10",
"symfony/console": "^5.2",
"symfony/docs-builder": "^0.15.0",
"symfony/dotenv": "^5.2",
Expand Down
38 changes: 19 additions & 19 deletions src/Service/CodeValidator/PhpValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,44 @@
namespace SymfonyTools\CodeBlockChecker\Service\CodeValidator;

use Doctrine\RST\Nodes\CodeNode;
use Symfony\Component\Process\Process;
use PhpParser\ErrorHandler;
use PhpParser\Parser;
use PhpParser\ParserFactory;
use SymfonyTools\CodeBlockChecker\Issue\Issue;
use SymfonyTools\CodeBlockChecker\Issue\IssueCollection;

class PhpValidator implements Validator
{
private ?Parser $parser = null;

public function validate(CodeNode $node, IssueCollection $issues): void
{
$language = $node->getLanguage() ?? ($node->isRaw() ? null : 'php');
if (!in_array($language, ['php', 'php-symfony', 'php-standalone', 'php-annotations', 'html+php'])) {
return;
}

$file = sys_get_temp_dir().'/'.uniqid('doc_builder', true).'.php';

file_put_contents($file, $this->getContents($node, $language));
$linesPrepended = 0;
$code = 'html+php' === $language ? $node->getValue() : $this->getContents($node, $linesPrepended);
$this->getParser()->parse($code, $errorHandler = new ErrorHandler\Collecting());

$process = new Process(['php', '-l', $file]);
$process->run();
if ($process->isSuccessful()) {
return;
foreach ($errorHandler->getErrors() as $error) {
$issues->addIssue(new Issue($node, $error->getRawMessage(), 'PHP syntax', $node->getEnvironment()->getCurrentFileName(), $error->getStartLine() - $linesPrepended));
}
}

$line = 0;
$text = str_replace($file, 'example.php', $process->getErrorOutput());
if (preg_match('| in example.php on line ([0-9]+)|s', $text, $matches)) {
$text = str_replace($matches[0], '', $text);
$line = ((int) $matches[1]) - 1; // we added "<?php"
private function getParser(): Parser
{
if (null === $this->parser) {
$this->parser = (new ParserFactory())->create(ParserFactory::ONLY_PHP7);
}
$issues->addIssue(new Issue($node, $text, 'PHP syntax', $node->getEnvironment()->getCurrentFileName(), $line));

return $this->parser;
}

private function getContents(CodeNode $node, string $language): string
private function getContents(CodeNode $node, &$linesPrepended = null): string
{
$contents = $node->getValue();
if ('html+php' === $language) {
return $contents;
}

if (!preg_match('#(class|interface) [a-zA-Z]+#s', $contents) && preg_match('#(public|protected|private)( static)? (\$[a-z]+|function)#s', $contents)) {
$contents = 'class Foobar {'.$contents.'}';
}
Expand All @@ -52,6 +51,7 @@ private function getContents(CodeNode $node, string $language): string
$lines = explode("\n", $contents);
if (!str_contains($lines[0] ?? '', '<?php') && !str_contains($lines[1] ?? '', '<?php') && !str_contains($lines[2] ?? '', '<?php')) {
$contents = '<?php'."\n".$contents;
$linesPrepended = 1;
}

return $contents;
Expand Down
54 changes: 50 additions & 4 deletions tests/Service/Validator/PhpValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,47 @@ protected function setUp(): void
$this->validator = new PhpValidator();
}

public function testLocalLine()
{
// Without <?php
$code = '$x = 2;
$y = 3;
$z = 4
echo "foo";
';

$node = new CodeNode(explode(PHP_EOL, $code));
$node->setEnvironment($this->environment);
$node->setLanguage('php');
$this->validator->validate($node, $issues = new IssueCollection());
$this->assertCount(1, $issues);
$this->assertEquals(4, $issues->first()->getLocalLine());

// With <?php
$code = '<?php
$x = 2;
$y = 3;
$z = 4
echo "foo";
';

$node = new CodeNode(explode(PHP_EOL, $code));
$node->setEnvironment($this->environment);
$node->setLanguage('php');
$this->validator->validate($node, $issues = new IssueCollection());
$this->assertCount(1, $issues);
$this->assertEquals(5, $issues->first()->getLocalLine());
}

/**
* @dataProvider getCodeExamples
*/
public function testCodeExamples(int $errors, string $code)
public function testCodeExamples(int $errors, string $code, ?string $language = null)
{
$node = new CodeNode(explode(PHP_EOL, $code));
$node->setEnvironment($this->environment);
$node->setLanguage('php');
$issues = new IssueCollection();
$this->validator->validate($node, $issues);
$node->setLanguage($language ?? 'php');
$this->validator->validate($node, $issues = new IssueCollection());
$this->assertCount($errors, $issues);
}

Expand Down Expand Up @@ -70,5 +101,20 @@ public static function validate($object, ExecutionContextInterface $context, $pa
}
}
'];
yield [1, '
public static function validate($object, ExecutionContextInterface $context, $payload)
{
$foo = 2a
}
'];
yield [0, '
<h1>Hello</h1>
<p><? echo $value; ?></p>
', 'html+php'];

yield [1, '
<h1>Hello</h1>
<p><?php value foobar ?></p>
', 'html+php'];
}
}