diff --git a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php index be15eaf942..68b20dbbc9 100644 --- a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php @@ -28,7 +28,7 @@ class LintCommandTest extends \PHPUnit_Framework_TestCase $ret = $tester->execute(array('filename' => array($filename)), array('verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false)); $this->assertEquals(0, $ret, 'Returns 0 in case of success'); - $this->assertRegExp('/^\/\/ OK in /', trim($tester->getDisplay())); + $this->assertContains('OK in', trim($tester->getDisplay())); } public function testLintIncorrectFile() diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php index eb3508449b..de30b5c1b8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php @@ -11,155 +11,45 @@ namespace Symfony\Bundle\FrameworkBundle\Command; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\Finder\Finder; -use Symfony\Component\Yaml\Exception\ParseException; -use Symfony\Component\Yaml\Parser; +use Symfony\Component\Yaml\Command\LintCommand as BaseLintCommand; /** * Validates YAML files syntax and outputs encountered errors. * * @author Grégoire Pineau + * @author Robin Chalas */ -class YamlLintCommand extends Command +class YamlLintCommand extends BaseLintCommand { + /** + * {@inheritdoc} + */ protected function configure() { - $this - ->setName('lint:yaml') - ->setDescription('Lints a file and outputs encountered errors') - ->addArgument('filename', null, 'A file or a directory or STDIN') - ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') - ->setHelp(<<%command.name% command lints a YAML file and outputs to STDOUT -the first encountered syntax error. + parent::configure(); -You can validate the syntax of a file: + $this->setHelp( + $this->getHelp().<<php %command.full_name% filename - -Or of a whole directory: - - php %command.full_name% dirname - php %command.full_name% dirname --format=json - -Or all YAML files in a bundle: +Or find all files in a bundle: php %command.full_name% @AcmeDemoBundle -You can also pass the YAML contents from STDIN: - - cat filename | php %command.full_name% - EOF - ) - ; + ); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function getDirectoryIterator($directory) { - $io = new SymfonyStyle($input, $output); - $filename = $input->getArgument('filename'); - - if (!$filename) { - if (0 !== ftell(STDIN)) { - throw new \RuntimeException('Please provide a filename or pipe file content to STDIN.'); - } - - $content = ''; - while (!feof(STDIN)) { - $content .= fread(STDIN, 1024); - } - - return $this->display($input, $output, $io, array($this->validate($content))); + if (!is_dir($directory)) { + $directory = $this->getApplication()->getKernel()->locateResource($directory); } - if (0 !== strpos($filename, '@') && !is_readable($filename)) { - throw new \RuntimeException(sprintf('File or directory "%s" is not readable', $filename)); - } - - $files = array(); - if (is_file($filename)) { - $files = array($filename); - } elseif (is_dir($filename)) { - $files = Finder::create()->files()->in($filename)->name('*.yml'); - } else { - $dir = $this->getApplication()->getKernel()->locateResource($filename); - $files = Finder::create()->files()->in($dir)->name('*.yml'); - } - - $filesInfo = array(); - foreach ($files as $file) { - $filesInfo[] = $this->validate(file_get_contents($file), $file); - } - - return $this->display($input, $output, $io, $filesInfo); + return parent::getDirectoryIterator($directory); } - private function validate($content, $file = null) + protected function isReadable($fileOrDirectory) { - $parser = new Parser(); - try { - $parser->parse($content); - } catch (ParseException $e) { - return array('file' => $file, 'valid' => false, 'message' => $e->getMessage()); - } - - return array('file' => $file, 'valid' => true); - } - - private function display(InputInterface $input, OutputInterface $output, SymfonyStyle $io, $files) - { - switch ($input->getOption('format')) { - case 'txt': - return $this->displayTxt($output, $io, $files); - case 'json': - return $this->displayJson($io, $files); - default: - throw new \InvalidArgumentException(sprintf('The format "%s" is not supported.', $input->getOption('format'))); - } - } - - private function displayTxt(OutputInterface $output, SymfonyStyle $io, $filesInfo) - { - $errors = 0; - - foreach ($filesInfo as $info) { - if ($info['valid'] && $output->isVerbose()) { - $io->comment('OK'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); - } elseif (!$info['valid']) { - ++$errors; - $io->text(sprintf(' ERROR in %s', $info['file'])); - $io->text(sprintf(' >> %s', $info['message'])); - } - } - - if ($errors === 0) { - $io->success(sprintf('All %d YAML files contain valid syntax.', count($filesInfo))); - } else { - $io->warning(sprintf('%d YAML files have valid syntax and %d contain errors.', count($filesInfo) - $errors, $errors)); - } - - return min($errors, 1); - } - - private function displayJson(OutputInterface $output, $filesInfo) - { - $errors = 0; - - array_walk($filesInfo, function (&$v) use (&$errors) { - $v['file'] = (string) $v['file']; - if (!$v['valid']) { - ++$errors; - } - }); - - $output->writeln(json_encode($filesInfo, JSON_PRETTY_PRINT)); - - return min($errors, 1); + return 0 === strpos($fileOrDirectory, '@') || parent::isReadable($fileOrDirectory); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php new file mode 100644 index 0000000000..c4b7547248 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php @@ -0,0 +1,198 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Command; + +use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Application as BaseApplication; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\HttpKernel\KernelInterface; + +/** + * Tests the YamlLintCommand. + * + * @author Robin Chalas + */ +class YamlLintCommandTest extends \PHPUnit_Framework_TestCase +{ + private $files; + + public function testLintCorrectFile() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile('foo: bar'); + + $tester->execute( + array('filename' => $filename), + array('verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false) + ); + + $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); + $this->assertContains('OK', trim($tester->getDisplay())); + } + + public function testLintIncorrectFile() + { + $incorrectContent = ' +foo: +bar'; + $tester = $this->createCommandTester(); + $filename = $this->createFile($incorrectContent); + + $tester->execute(array('filename' => $filename), array('decorated' => false)); + + $this->assertEquals(1, $tester->getStatusCode(), 'Returns 1 in case of error'); + $this->assertContains('Unable to parse at line 3 (near "bar").', trim($tester->getDisplay())); + } + + /** + * @expectedException \RuntimeException + */ + public function testLintFileNotReadable() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile(''); + unlink($filename); + + $tester->execute(array('filename' => $filename), array('decorated' => false)); + } + + public function testGetHelp() + { + $command = new YamlLintCommand(); + $expected = <<%command.name% command lints a YAML file and outputs to STDOUT +the first encountered syntax error. + +You can validates YAML contents passed from STDIN: + + cat filename | php %command.full_name% + +You can also validate the syntax of a file: + + php %command.full_name% filename + +Or of a whole directory: + + php %command.full_name% dirname + php %command.full_name% dirname --format=json + +Or find all files in a bundle: + + php %command.full_name% @AcmeDemoBundle + +EOF; + + $this->assertEquals($expected, $command->getHelp()); + } + + public function testLintFilesFromBundleDirectory() + { + $tester = $this->createCommandTester($this->getKernelAwareApplicationMock()); + $tester->execute( + array('filename' => '@AppBundle/Resources'), + array('verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false) + ); + + $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); + $this->assertContains('[OK] All 0 YAML files contain valid syntax', trim($tester->getDisplay())); + } + + /** + * @return string Path to the new file + */ + private function createFile($content) + { + $filename = tempnam(sys_get_temp_dir(), 'sf-'); + file_put_contents($filename, $content); + + $this->files[] = $filename; + + return $filename; + } + + /** + * @return CommandTester + */ + private function createCommandTester($application = null) + { + if (!$application) { + $application = new BaseApplication(); + $application->add(new YamlLintCommand()); + } + + $command = $application->find('lint:yaml'); + + if ($application) { + $command->setApplication($application); + } + + return new CommandTester($command); + } + + private function getKernelAwareApplicationMock() + { + $kernel = $this->getMockBuilder(KernelInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $kernel + ->expects($this->once()) + ->method('locateResource') + ->with('@AppBundle/Resources') + ->willReturn(sys_get_temp_dir()); + + $application = $this->getMockBuilder(Application::class) + ->disableOriginalConstructor() + ->getMock(); + + $application + ->expects($this->once()) + ->method('getKernel') + ->willReturn($kernel); + + $application + ->expects($this->once()) + ->method('getHelperSet') + ->willReturn(new HelperSet()); + + $application + ->expects($this->any()) + ->method('getDefinition') + ->willReturn(new InputDefinition()); + + $application + ->expects($this->once()) + ->method('find') + ->with('lint:yaml') + ->willReturn(new YamlLintCommand()); + + return $application; + } + + protected function setUp() + { + $this->files = array(); + } + + protected function tearDown() + { + foreach ($this->files as $file) { + if (file_exists($file)) { + unlink($file); + } + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 2bbdcad1ca..78bbd469f8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -49,7 +49,7 @@ "symfony/process": "~2.8|~3.0", "symfony/serializer": "~2.8|^3.0", "symfony/validator": "~3.1", - "symfony/yaml": "~2.8|~3.0", + "symfony/yaml": "~3.2", "symfony/property-info": "~2.8|~3.0", "phpdocumentor/reflection-docblock": "^3.0", "twig/twig": "~1.23|~2.0" diff --git a/src/Symfony/Component/Yaml/Command/LintCommand.php b/src/Symfony/Component/Yaml/Command/LintCommand.php new file mode 100644 index 0000000000..eae6e50bea --- /dev/null +++ b/src/Symfony/Component/Yaml/Command/LintCommand.php @@ -0,0 +1,204 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml\Command; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Parser; + +/** + * Validates YAML files syntax and outputs encountered errors. + * + * @author Grégoire Pineau + * @author Robin Chalas + */ +class LintCommand extends Command +{ + private $parser; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('lint:yaml') + ->setDescription('Lints a file and outputs encountered errors') + ->addArgument('filename', null, 'A file or a directory or STDIN') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') + ->setHelp(<<%command.name% command lints a YAML file and outputs to STDOUT +the first encountered syntax error. + +You can validates YAML contents passed from STDIN: + + cat filename | php %command.full_name% + +You can also validate the syntax of a file: + + php %command.full_name% filename + +Or of a whole directory: + + php %command.full_name% dirname + php %command.full_name% dirname --format=json + +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $filename = $input->getArgument('filename'); + + if (!$filename) { + if (!$stdin = $this->getStdin()) { + throw new \RuntimeException('Please provide a filename or pipe file content to STDIN.'); + } + + return $this->display($input, $output, $io, array($this->validate($stdin))); + } + + if (!$this->isReadable($filename)) { + throw new \RuntimeException(sprintf('File or directory "%s" is not readable.', $filename)); + } + + $filesInfo = array(); + foreach ($this->getFiles($filename) as $file) { + $filesInfo[] = $this->validate(file_get_contents($file), $file); + } + + return $this->display($input, $output, $io, $filesInfo); + } + + private function validate($content, $file = null) + { + try { + $this->getParser()->parse($content); + } catch (ParseException $e) { + return array('file' => $file, 'valid' => false, 'message' => $e->getMessage()); + } + + return array('file' => $file, 'valid' => true); + } + + private function display(InputInterface $input, OutputInterface $output, SymfonyStyle $io, $files) + { + switch ($input->getOption('format')) { + case 'txt': + return $this->displayTxt($output, $io, $files); + case 'json': + return $this->displayJson($io, $files); + default: + throw new \InvalidArgumentException(sprintf('The format "%s" is not supported.', $input->getOption('format'))); + } + } + + private function displayTxt(OutputInterface $output, SymfonyStyle $io, $filesInfo) + { + $countFiles = count($filesInfo); + $erroredFiles = 0; + + foreach ($filesInfo as $info) { + if ($info['valid'] && $output->isVerbose()) { + $io->comment('OK'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); + } elseif (!$info['valid']) { + ++$erroredFiles; + $io->text(sprintf(' ERROR in %s', $info['file'])); + $io->text(sprintf(' >> %s', $info['message'])); + } + } + + if ($erroredFiles === 0) { + $io->success(sprintf('All %d YAML files contain valid syntax.', $countFiles)); + } else { + $io->warning(sprintf('%d YAML files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles)); + } + + return min($erroredFiles, 1); + } + + private function displayJson(OutputInterface $output, $filesInfo) + { + $errors = 0; + + array_walk($filesInfo, function (&$v) use (&$errors) { + $v['file'] = (string) $v['file']; + if (!$v['valid']) { + ++$errors; + } + }); + + $output->writeln(json_encode($filesInfo, JSON_PRETTY_PRINT)); + + return min($errors, 1); + } + + private function getFiles($fileOrDirectory) + { + if (is_file($fileOrDirectory)) { + yield new \SplFileInfo($fileOrDirectory); + + return; + } + + foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) { + if (!in_array($file->getExtension(), array('yml', 'yaml'))) { + continue; + } + + yield $file; + } + } + + protected function getDirectoryIterator($directory) + { + return new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + } + + private function getStdin() + { + if (0 !== ftell(STDIN)) { + return; + } + + $inputs = ''; + while (!feof(STDIN)) { + $inputs .= fread(STDIN, 1024); + } + + return $inputs; + } + + private function getParser() + { + if (!$this->parser) { + $this->parser = new Parser(); + } + + return $this->parser; + } + + protected function isReadable($fileOrDirectory) + { + return is_readable($fileOrDirectory); + } +} diff --git a/src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php b/src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php new file mode 100644 index 0000000000..cbcf8cd278 --- /dev/null +++ b/src/Symfony/Component/Yaml/Tests/Command/LintCommandTest.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Yaml\Tests\Command; + +use Symfony\Component\Yaml\Command\LintCommand; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Tester\CommandTester; + +/** + * Tests the YamlLintCommand. + * + * @author Robin Chalas + */ +class LintCommandTest extends \PHPUnit_Framework_TestCase +{ + private $files; + + public function testLintCorrectFile() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile('foo: bar'); + + $ret = $tester->execute(array('filename' => $filename), array('verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false)); + + $this->assertEquals(0, $ret, 'Returns 0 in case of success'); + $this->assertRegExp('/^\/\/ OK in /', trim($tester->getDisplay())); + } + + public function testLintIncorrectFile() + { + $incorrectContent = ' +foo: +bar'; + $tester = $this->createCommandTester(); + $filename = $this->createFile($incorrectContent); + + $ret = $tester->execute(array('filename' => $filename), array('decorated' => false)); + + $this->assertEquals(1, $ret, 'Returns 1 in case of error'); + $this->assertContains('Unable to parse at line 3 (near "bar").', trim($tester->getDisplay())); + } + + /** + * @expectedException \RuntimeException + */ + public function testLintFileNotReadable() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile(''); + unlink($filename); + + $ret = $tester->execute(array('filename' => $filename), array('decorated' => false)); + } + + /** + * @return string Path to the new file + */ + private function createFile($content) + { + $filename = tempnam(sys_get_temp_dir(), 'sf-'); + file_put_contents($filename, $content); + + $this->files[] = $filename; + + return $filename; + } + + /** + * @return CommandTester + */ + protected function createCommandTester() + { + $application = new Application(); + $application->add(new LintCommand()); + $command = $application->find('lint:yaml'); + + return new CommandTester($command); + } + + protected function setUp() + { + $this->files = array(); + } + + protected function tearDown() + { + foreach ($this->files as $file) { + if (file_exists($file)) { + unlink($file); + } + } + } +} diff --git a/src/Symfony/Component/Yaml/composer.json b/src/Symfony/Component/Yaml/composer.json index 1abbed2656..9c92bebcf2 100644 --- a/src/Symfony/Component/Yaml/composer.json +++ b/src/Symfony/Component/Yaml/composer.json @@ -18,6 +18,12 @@ "require": { "php": ">=5.5.9" }, + "require-dev": { + "symfony/console": "~2.8|~3.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, "autoload": { "psr-4": { "Symfony\\Component\\Yaml\\": "" }, "exclude-from-classmap": [