From 385e7bc7ee88f6008ec392de4bdcfcbba07cd5c4 Mon Sep 17 00:00:00 2001 From: Nyholm Date: Fri, 23 Apr 2021 15:31:53 +0200 Subject: [PATCH 1/5] Verify PHP classes --- config/services.yaml | 8 +- src/Command/CheckDocsCommand.php | 8 +- src/Service/CodeRunner.php | 43 +++++++ src/Service/CodeRunner/ClassExist.php | 113 ++++++++++++++++++ .../ConfigurationRunner.php} | 31 ++--- src/Service/CodeRunner/Runner.php | 14 +++ src/Service/CodeValidator.php | 4 + 7 files changed, 197 insertions(+), 24 deletions(-) create mode 100644 src/Service/CodeRunner.php create mode 100644 src/Service/CodeRunner/ClassExist.php rename src/Service/{CodeNodeRunner.php => CodeRunner/ConfigurationRunner.php} (79%) create mode 100644 src/Service/CodeRunner/Runner.php diff --git a/config/services.yaml b/config/services.yaml index 849627a..abbc4c5 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -10,8 +10,9 @@ services: _instanceof: SymfonyTools\CodeBlockChecker\Service\CodeValidator\Validator: - tags: - - 'app.code_validator' + tags: ['app.code_validator'] + SymfonyTools\CodeBlockChecker\Service\CodeRunner\Runner: + tags: ['app.code_runner'] SymfonyTools\CodeBlockChecker\: resource: '../src/' @@ -21,3 +22,6 @@ services: SymfonyTools\CodeBlockChecker\Service\CodeValidator: arguments: [!tagged_iterator app.code_validator] + + Symfony\CodeBlockChecker\Service\CodeRunner: + arguments: [!tagged_iterator app.code_runner] diff --git a/src/Command/CheckDocsCommand.php b/src/Command/CheckDocsCommand.php index 44e6a6e..b4f5f6b 100644 --- a/src/Command/CheckDocsCommand.php +++ b/src/Command/CheckDocsCommand.php @@ -34,14 +34,14 @@ class CheckDocsCommand extends Command private CodeNodeCollector $collector; private CodeValidator $validator; private Baseline $baseline; - private CodeNodeRunner $codeNodeRunner; + private CodeRunner $codeRunner; - public function __construct(CodeValidator $validator, Baseline $baseline, CodeNodeRunner $codeNodeRunner) + public function __construct(CodeValidator $validator, Baseline $baseline, CodeRunner $codeRunner) { parent::__construct(self::$defaultName); $this->validator = $validator; $this->baseline = $baseline; - $this->codeNodeRunner = $codeNodeRunner; + $this->codeRunner = $codeRunner; } protected function configure() @@ -88,7 +88,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int // Verify code blocks $issues = $this->validator->validateNodes($this->collector->getNodes()); if ($applicationDir = $input->getOption('symfony-application')) { - $issues->append($this->codeNodeRunner->runNodes($this->collector->getNodes(), $applicationDir)); + $issues->append($this->codeRunner->runNodes($this->collector->getNodes(), $applicationDir)); } if ($baselineFile = $input->getOption('generate-baseline')) { diff --git a/src/Service/CodeRunner.php b/src/Service/CodeRunner.php new file mode 100644 index 0000000..63fe47a --- /dev/null +++ b/src/Service/CodeRunner.php @@ -0,0 +1,43 @@ + + */ +class CodeRunner +{ + /** + * @var iterable + */ + private $runners; + + /** + * @param iterable $runners + */ + public function __construct(iterable $runners) + { + $this->runners = $runners; + } + + /** + * @param list $nodes + */ + public function runNodes(array $nodes, string $applicationDirectory): IssueCollection + { + $issues = new IssueCollection(); + foreach ($this->runners as $runner) { + $runner->run($nodes, $issues, $applicationDirectory); + } + + return $issues; + } +} diff --git a/src/Service/CodeRunner/ClassExist.php b/src/Service/CodeRunner/ClassExist.php new file mode 100644 index 0000000..69d9b91 --- /dev/null +++ b/src/Service/CodeRunner/ClassExist.php @@ -0,0 +1,113 @@ + + */ +class ClassExist implements Runner +{ + /** + * @param list $nodes + */ + public function run(array $nodes, IssueCollection $issues, string $applicationDirectory): void + { + $classes = []; + foreach ($nodes as $node) { + $classes = array_merge($classes, $this->getClasses($node)); + } + + $this->testClasses($classes, $issues, $applicationDirectory); + } + + private function getClasses(CodeNode $node): array + { + $language = $node->getLanguage() ?? 'php'; + if (!in_array($language, ['php', 'php-symfony', 'php-standalone', 'php-annotations'])) { + return []; + } + + $classes = []; + foreach (explode("\n", $node->getValue()) as $i => $line) { + $matches = []; + if (0 !== strpos($line, 'use ') || !preg_match('|^use (.*\\\.*); *?$|m', $line, $matches)) { + continue; + } + + $class = $matches[1]; + if (false !== $pos = strpos($class, ' as ')) { + $class = substr($class, 0, $pos); + } + + if (false !== $pos = strpos($class, 'function ')) { + continue; + } + + $explode = explode('\\', $class); + if ( + 'App' === $explode[0] || 'Acme' === $explode[0] + || (3 === count($explode) && 'Symfony' === $explode[0] && ('Component' === $explode[1] || 'Config' === $explode[1])) + ) { + continue; + } + + $classes[] = ['class' => $class, 'line' => $i + 1, 'node' => $node]; + } + + return $classes; + } + + /** + * Make sure PHP classes exists in the application directory. + * + * @param array{int, array{ class: string, line: int, node: CodeNode } } $classes + */ + private function testClasses(array $classes, IssueCollection $issues, string $applicationDirectory): void + { + $fileBody = ''; + foreach ($classes as $i => $data) { + $fileBody .= sprintf('%s => isLoaded("%s"),', $i, $data['class'])."\n"; + } + + file_put_contents($applicationDirectory.'/class_exist.php', strtr(' $fileBody])); + + $process = new Process(['php', 'class_exist.php'], $applicationDirectory); + $process->run(); + + if (!$process->isSuccessful()) { + // TODO handle this + return; + } + + $output = $process->getOutput(); + try { + $results = json_decode($output, true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + // TODO handle this + return; + } + + foreach ($classes as $i => $data) { + if (!$results[$i]) { + $text = sprintf('Class, interface or trait with name "%s" does not exist', $data['class']); + $issues->addIssue(new Issue($data['node'], $text, 'Missing class', $data['node']->getEnvironment()->getCurrentFileName(), $data['line'])); + } + } + } +} diff --git a/src/Service/CodeNodeRunner.php b/src/Service/CodeRunner/ConfigurationRunner.php similarity index 79% rename from src/Service/CodeNodeRunner.php rename to src/Service/CodeRunner/ConfigurationRunner.php index 41b4c03..3bd2e1e 100644 --- a/src/Service/CodeNodeRunner.php +++ b/src/Service/CodeRunner/ConfigurationRunner.php @@ -1,6 +1,6 @@ */ -class CodeNodeRunner +class ConfigurationRunner { /** * @param list $nodes */ - public function runNodes(array $nodes, string $applicationDirectory): IssueCollection + public function run(array $nodes, IssueCollection $issues, string $applicationDirectory): void { - $issues = new IssueCollection(); foreach ($nodes as $node) { $this->processNode($node, $issues, $applicationDirectory); } - - return $issues; } private function processNode(CodeNode $node, IssueCollection $issues, string $applicationDirectory): void { - $file = $this->getFile($node); + $explodedNode = explode("\n", $node->getValue()); + $file = $this->getFile($node, $explodedNode); + if ('config/packages/' !== substr($file, 0, 16)) { return; } @@ -45,13 +44,13 @@ private function processNode(CodeNode $node, IssueCollection $issues, string $ap } // Write config - file_put_contents($fullPath, $this->getNodeContents($node)); + file_put_contents($fullPath, $this->getNodeContents($node, $explodedNode)); // Clear cache $filesystem->remove($applicationDirectory.'/var/cache'); // Warmup and log errors - $this->warmupCache($node, $issues, $applicationDirectory); + $this->warmupCache($node, $issues, $applicationDirectory, count($explodedNode) - 1); } finally { // Remove added file and restore original $filesystem->remove($fullPath); @@ -62,7 +61,7 @@ private function processNode(CodeNode $node, IssueCollection $issues, string $ap } } - private function warmupCache(CodeNode $node, IssueCollection $issues, string $applicationDirectory): void + private function warmupCache(CodeNode $node, IssueCollection $issues, string $applicationDirectory, int $numberOfLines): void { $process = new Process(['php', 'bin/console', 'cache:warmup', '--env', 'dev'], $applicationDirectory); $process->run(); @@ -70,12 +69,11 @@ private function warmupCache(CodeNode $node, IssueCollection $issues, string $ap return; } - $issues->addIssue(new Issue($node, trim($process->getErrorOutput()), 'Cache Warmup', $node->getEnvironment()->getCurrentFileName(), count(explode("\n", $node->getValue())) - 1)); + $issues->addIssue(new Issue($node, trim($process->getErrorOutput()), 'Cache Warmup', $node->getEnvironment()->getCurrentFileName(), $numberOfLines)); } - private function getFile(CodeNode $node): string + private function getFile(CodeNode $node, array $contents): string { - $contents = explode("\n", $node->getValue()); $regex = match ($node->getLanguage()) { 'php' => '|^// ?([a-z1-9A-Z_\-/]+\.php)$|', 'yaml' => '|^# ?([a-z1-9A-Z_\-/]+\.yaml)$|', @@ -90,7 +88,7 @@ private function getFile(CodeNode $node): string return $matches[1]; } - private function getNodeContents(CodeNode $node): string + private function getNodeContents(CodeNode $node, array $contents): string { $language = $node->getLanguage(); if ('php' === $language) { @@ -98,12 +96,9 @@ private function getNodeContents(CodeNode $node): string } if ('xml' === $language) { - $contents = explode("\n", $node->getValue()); unset($contents[0]); - - return implode("\n", $contents); } - return $node->getValue(); + return implode("\n", $contents); } } diff --git a/src/Service/CodeRunner/Runner.php b/src/Service/CodeRunner/Runner.php new file mode 100644 index 0000000..c9175d8 --- /dev/null +++ b/src/Service/CodeRunner/Runner.php @@ -0,0 +1,14 @@ + $nodes + */ + public function run(array $nodes, IssueCollection $issues, string $applicationDirectory): void; +} diff --git a/src/Service/CodeValidator.php b/src/Service/CodeValidator.php index 293d091..94c51d4 100644 --- a/src/Service/CodeValidator.php +++ b/src/Service/CodeValidator.php @@ -4,6 +4,7 @@ namespace SymfonyTools\CodeBlockChecker\Service; +use Doctrine\RST\Nodes\CodeNode; use SymfonyTools\CodeBlockChecker\Issue\IssueCollection; use SymfonyTools\CodeBlockChecker\Service\CodeValidator\Validator; @@ -27,6 +28,9 @@ public function __construct(iterable $validators) $this->validators = $validators; } + /** + * @param list $nodes + */ public function validateNodes(array $nodes): IssueCollection { $issues = new IssueCollection(); From ea464bccf1ff3abb987fbbccc01e6871cfe52c0b Mon Sep 17 00:00:00 2001 From: Nyholm Date: Fri, 23 Apr 2021 20:47:53 +0200 Subject: [PATCH 2/5] Fixes --- config/services.yaml | 2 +- src/Command/CheckDocsCommand.php | 2 +- src/Service/CodeRunner.php | 6 +++--- src/Service/CodeRunner/ClassExist.php | 6 +++--- src/Service/CodeRunner/Runner.php | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/config/services.yaml b/config/services.yaml index abbc4c5..233d218 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -23,5 +23,5 @@ services: SymfonyTools\CodeBlockChecker\Service\CodeValidator: arguments: [!tagged_iterator app.code_validator] - Symfony\CodeBlockChecker\Service\CodeRunner: + SymfonyTools\CodeBlockChecker\Service\CodeRunner: arguments: [!tagged_iterator app.code_runner] diff --git a/src/Command/CheckDocsCommand.php b/src/Command/CheckDocsCommand.php index b4f5f6b..59a985f 100644 --- a/src/Command/CheckDocsCommand.php +++ b/src/Command/CheckDocsCommand.php @@ -22,7 +22,7 @@ use SymfonyTools\CodeBlockChecker\Issue\IssueCollection; use SymfonyTools\CodeBlockChecker\Listener\CodeNodeCollector; use SymfonyTools\CodeBlockChecker\Service\Baseline; -use SymfonyTools\CodeBlockChecker\Service\CodeNodeRunner; +use SymfonyTools\CodeBlockChecker\Service\CodeRunner; use SymfonyTools\CodeBlockChecker\Service\CodeValidator; class CheckDocsCommand extends Command diff --git a/src/Service/CodeRunner.php b/src/Service/CodeRunner.php index 63fe47a..48bfd76 100644 --- a/src/Service/CodeRunner.php +++ b/src/Service/CodeRunner.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Symfony\CodeBlockChecker\Service; +namespace SymfonyTools\CodeBlockChecker\Service; use Doctrine\RST\Nodes\CodeNode; -use Symfony\CodeBlockChecker\Issue\IssueCollection; -use Symfony\CodeBlockChecker\Service\CodeRunner\Runner; +use SymfonyTools\CodeBlockChecker\Issue\IssueCollection; +use SymfonyTools\CodeBlockChecker\Service\CodeRunner\Runner; /** * Run a Code Node inside a real application. diff --git a/src/Service/CodeRunner/ClassExist.php b/src/Service/CodeRunner/ClassExist.php index 69d9b91..ad00b92 100644 --- a/src/Service/CodeRunner/ClassExist.php +++ b/src/Service/CodeRunner/ClassExist.php @@ -1,10 +1,10 @@ Date: Fri, 23 Apr 2021 20:50:08 +0200 Subject: [PATCH 3/5] cs --- composer.json | 2 +- src/Service/CodeRunner/ClassExist.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index b03c6de..9121f91 100644 --- a/composer.json +++ b/composer.json @@ -53,7 +53,7 @@ "repositories": [ { "type": "git", - "url": "https://github.com/weaverryan/docs-builder" + "url": "https://github.com/symfony-tools/docs-builder" } ], "minimum-stability": "dev", diff --git a/src/Service/CodeRunner/ClassExist.php b/src/Service/CodeRunner/ClassExist.php index ad00b92..1108c86 100644 --- a/src/Service/CodeRunner/ClassExist.php +++ b/src/Service/CodeRunner/ClassExist.php @@ -3,9 +3,9 @@ namespace SymfonyTools\CodeBlockChecker\Service\CodeRunner; use Doctrine\RST\Nodes\CodeNode; +use Symfony\Component\Process\Process; use SymfonyTools\CodeBlockChecker\Issue\Issue; use SymfonyTools\CodeBlockChecker\Issue\IssueCollection; -use Symfony\Component\Process\Process; /** * Verify that any reference to a PHP class is actually a real class. From 1cb2de30514ce61d0130623bb14ebe9b491cd944 Mon Sep 17 00:00:00 2001 From: Nyholm Date: Fri, 23 Apr 2021 21:09:25 +0200 Subject: [PATCH 4/5] Added test --- .php_cs | 1 + tests/Fixtures/example-application/.gitignore | 1 + .../example-application/composer.json | 11 +++++ .../example-application/src/Foobar.php | 8 ++++ .../example-application/vendor/autoload.php | 7 +++ .../Service/CodeRunner/BaseCodeRunnerTest.php | 13 +++++ tests/Service/CodeRunner/ClassExistTest.php | 47 +++++++++++++++++++ 7 files changed, 88 insertions(+) create mode 100644 tests/Fixtures/example-application/.gitignore create mode 100644 tests/Fixtures/example-application/composer.json create mode 100644 tests/Fixtures/example-application/src/Foobar.php create mode 100644 tests/Fixtures/example-application/vendor/autoload.php create mode 100644 tests/Service/CodeRunner/BaseCodeRunnerTest.php create mode 100644 tests/Service/CodeRunner/ClassExistTest.php diff --git a/.php_cs b/.php_cs index 5e028c9..2ef1771 100644 --- a/.php_cs +++ b/.php_cs @@ -3,6 +3,7 @@ $finder = PhpCsFixer\Finder::create() ->in(__DIR__.'/src') ->in(__DIR__.'/tests') + ->notPath(__DIR__.'/tests/Fixtures') ; return PhpCsFixer\Config::create() diff --git a/tests/Fixtures/example-application/.gitignore b/tests/Fixtures/example-application/.gitignore new file mode 100644 index 0000000..6896cc4 --- /dev/null +++ b/tests/Fixtures/example-application/.gitignore @@ -0,0 +1 @@ +class_exist.php diff --git a/tests/Fixtures/example-application/composer.json b/tests/Fixtures/example-application/composer.json new file mode 100644 index 0000000..8c8cd07 --- /dev/null +++ b/tests/Fixtures/example-application/composer.json @@ -0,0 +1,11 @@ +{ + "name": "example/app", + "type": "project", + "license": "MIT", + "require": {}, + "autoload": { + "psr-4": { + "Example\\App\\": "src/" + } + } +} diff --git a/tests/Fixtures/example-application/src/Foobar.php b/tests/Fixtures/example-application/src/Foobar.php new file mode 100644 index 0000000..a204a27 --- /dev/null +++ b/tests/Fixtures/example-application/src/Foobar.php @@ -0,0 +1,8 @@ +environment = new Environment(new Configuration()); + $this->runner = new ClassExist(); + } + public function testHappyPath() + { + $code = ' +use Example\App\Foobar; +use Symfony\Component\HttpKernel; +use Foo\Bar\Baz; + +echo "hello"; +'; + $node = new CodeNode(explode(PHP_EOL, $code)); + $node->setEnvironment($this->environment); + $node->setLanguage('php'); + $issues = new IssueCollection(); + $this->runner->run([$node], $issues, $this->getApplicationDirectory()); + $this->assertCount(1, $issues); + + $issue = $issues->first(); + $this->assertStringContainsString('Foo\Bar\Baz', $issue->getText()); + } + + +} From 14f68233c6beb71fc25e97aa7b2cab9d64993bf5 Mon Sep 17 00:00:00 2001 From: Nyholm Date: Fri, 23 Apr 2021 21:10:56 +0200 Subject: [PATCH 5/5] cs --- .php_cs | 1 - tests/Fixtures/example-application/src/Foobar.php | 1 - tests/Service/CodeRunner/ClassExistTest.php | 7 +------ 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.php_cs b/.php_cs index 2ef1771..5e028c9 100644 --- a/.php_cs +++ b/.php_cs @@ -3,7 +3,6 @@ $finder = PhpCsFixer\Finder::create() ->in(__DIR__.'/src') ->in(__DIR__.'/tests') - ->notPath(__DIR__.'/tests/Fixtures') ; return PhpCsFixer\Config::create() diff --git a/tests/Fixtures/example-application/src/Foobar.php b/tests/Fixtures/example-application/src/Foobar.php index a204a27..865e97c 100644 --- a/tests/Fixtures/example-application/src/Foobar.php +++ b/tests/Fixtures/example-application/src/Foobar.php @@ -4,5 +4,4 @@ class Foobar { - } diff --git a/tests/Service/CodeRunner/ClassExistTest.php b/tests/Service/CodeRunner/ClassExistTest.php index c86b2c6..b832584 100644 --- a/tests/Service/CodeRunner/ClassExistTest.php +++ b/tests/Service/CodeRunner/ClassExistTest.php @@ -1,17 +1,13 @@ environment = new Environment(new Configuration()); $this->runner = new ClassExist(); } + public function testHappyPath() { $code = ' @@ -42,6 +39,4 @@ public function testHappyPath() $issue = $issues->first(); $this->assertStringContainsString('Foo\Bar\Baz', $issue->getText()); } - - }