diff --git a/doc/tasks/phpstan.md b/doc/tasks/phpstan.md index a9b001e11..45fcebec8 100644 --- a/doc/tasks/phpstan.md +++ b/doc/tasks/phpstan.md @@ -17,6 +17,8 @@ parameters: autoload_file: ~ configuration: ~ level: 0 + force_patterns: [] + ignore_patterns: [] triggered_by: ['php'] ``` @@ -38,6 +40,18 @@ With this parameter you can specify the path your project's configuration file. With this parameter you can set the level of rule options - the higher the stricter. +**force_patterns** + +*Default: []* + +This is a list of patterns that will be forced for analysis even when the file or path is ignored. + +**ignore_patterns** + +*Default: []* + +This is a list of patterns that will be ignored by phpstan. With this option you can skip files like tests. Leave this option blank to run phpstan for every php file. + **triggered_by** *Default: [php]* diff --git a/spec/Collection/FilesCollectionSpec.php b/spec/Collection/FilesCollectionSpec.php index a0c97bcea..f161a841a 100644 --- a/spec/Collection/FilesCollectionSpec.php +++ b/spec/Collection/FilesCollectionSpec.php @@ -111,6 +111,19 @@ function it_should_filter_by_not_path(SplFileInfo $file1, SplFileInfo $file2) $files[0]->shouldBe($file1); } + function it_should_filter_by_not_paths(SplFileInfo $file1, SplFileInfo $file2, SplFileInfo $file3) + { + $file1->getRelativePathname()->willReturn('path1/file.php'); + $file2->getRelativePathname()->willReturn('path2/file.php'); + $file3->getRelativePathname()->willReturn('path3/file.png'); + + $result = $this->notPaths(['path2', 'path3']); + $result->shouldBeAnInstanceOf(FilesCollection::class); + $result->count()->shouldBe(1); + $files = $result->toArray(); + $files[0]->shouldBe($file1); + } + function it_should_filter_by_size(SplFileInfo $file1, SplFileInfo $file2) { $file1->isFile()->willReturn(true); @@ -190,4 +203,18 @@ function it_should_return_an_empty_list_when_filtering_by_no_extension(SplFileIn $result = $this->extensions([]); $result->count()->shouldBe(0); } + + function it_should_combine_two_collections_with_ensured_files() + { + $file1 = new \SplFileInfo('path1/file1.php'); + $file2 = new \SplFileInfo('path1/file2.php'); + $file3 = new \SplFileInfo('path1/file3.php'); + + $this->beConstructedWith([$file2, $file3]); + $ensureFiles = new FilesCollection([$file1, $file2]); + + $result = $this->ensureFiles($ensureFiles); + $result->shouldIterateAs([$file2, $file3, $file1]); + $result->shouldHaveCount(3); + } } diff --git a/spec/Task/PhpStanSpec.php b/spec/Task/PhpStanSpec.php index ab385dcdb..72ca6cf57 100644 --- a/spec/Task/PhpStanSpec.php +++ b/spec/Task/PhpStanSpec.php @@ -42,6 +42,8 @@ function it_should_have_configurable_options() $options->shouldBeAnInstanceOf(OptionsResolver::class); $options->getDefinedOptions()->shouldContain('autoload_file'); $options->getDefinedOptions()->shouldContain('configuration'); + $options->getDefinedOptions()->shouldContain('ignore_patterns'); + $options->getDefinedOptions()->shouldContain('force_patterns'); $options->getDefinedOptions()->shouldContain('level'); $options->getDefinedOptions()->shouldContain('triggered_by'); } @@ -59,7 +61,6 @@ function it_should_run_in_run_context(RunContext $context) function it_does_not_do_anything_if_there_are_no_files(ProcessBuilder $processBuilder, ContextInterface $context) { $processBuilder->buildProcess('phpstan')->shouldNotBeCalled(); - $processBuilder->buildProcess()->shouldNotBeCalled(); $context->getFiles()->willReturn(new FilesCollection()); $result = $this->run($context); @@ -87,6 +88,58 @@ function it_runs_the_suite(ProcessBuilder $processBuilder, Process $process, Con $result->isPassed()->shouldBe(true); } + function it_runs_the_suite_with_ignored_files(ProcessBuilder $processBuilder, Process $process, ContextInterface $context, GrumPHP $grumPHP) + { + $grumPHP->getTaskConfiguration('phpstan')->willReturn([ + 'ignore_patterns' => ['TaskResultCollection.php'], + ]); + + $context->getFiles()->willReturn(new FilesCollection([ + new SplFileInfo('src/Collection/TaskResultCollection.php', 'src/Collection', 'TaskResultCollection.php'), + new SplFileInfo('src/Collection/TaskResultCollection.php', 'src/Collection', 'Passed.php'), + ])); + + $processBuilder->buildProcess('phpstan')->shouldNotBeCalled(); + + $arguments = new ProcessArgumentsCollection(); + $processBuilder->createArgumentsForCommand('phpstan')->willReturn($arguments); + $processBuilder->buildProcess($arguments)->willReturn($process); + + $process->run()->shouldBeCalled(); + $process->isSuccessful()->willReturn(true); + $process->getErrorOutput()->willReturn(''); + $process->getOutput()->willReturn(''); + + $result = $this->run($context); + $result->shouldBeAnInstanceOf(TaskResultInterface::class); + $result->getResultCode()->shouldBe(TaskResult::PASSED); + } + + function it_runs_the_suite_with_forced_files(ProcessBuilder $processBuilder, Process $process, ContextInterface $context, GrumPHP $grumPHP) + { + $grumPHP->getTaskConfiguration('phpstan')->willReturn([ + 'ignore_patterns' => ['TaskResultCollection.php'], + 'force_patterns' => ['TaskResultCollection.php'], + ]); + + $arguments = new ProcessArgumentsCollection(); + $processBuilder->createArgumentsForCommand('phpstan')->willReturn($arguments); + $processBuilder->buildProcess($arguments)->willReturn($process); + + $process->run()->shouldBeCalled(); + $process->isSuccessful()->willReturn(true); + $process->getErrorOutput()->willReturn(''); + $process->getOutput()->willReturn(''); + + $context->getFiles()->willReturn(new FilesCollection([ + new SplFileInfo('src/Collection/TaskResultCollection.php', 'src/Collection', 'TaskResultCollection.php')]) + ); + + $result = $this->run($context); + $result->shouldBeAnInstanceOf(TaskResultInterface::class); + $result->getResultCode()->shouldBe(TaskResult::PASSED); + } + function it_throws_exception_if_the_process_fails(ProcessBuilder $processBuilder, Process $process, ContextInterface $context) { $arguments = new ProcessArgumentsCollection(); diff --git a/src/Collection/FilesCollection.php b/src/Collection/FilesCollection.php index d9d99c4d6..5bb4f7213 100644 --- a/src/Collection/FilesCollection.php +++ b/src/Collection/FilesCollection.php @@ -113,7 +113,23 @@ public function paths(array $patterns) */ public function notPath($pattern) { - $filter = new Iterator\PathFilterIterator($this->getIterator(), [], [$pattern]); + return $this->notPaths([$pattern]); + } + + /** + * Adds rules that filenames must not match. + * + * You can use patterns (delimited with / sign) or simple strings. + * + * $collection->notPaths(['/^spec\/','/^src\/']) + * + * @param array $pattern + * + * @return FilesCollection + */ + public function notPaths(array $pattern) + { + $filter = new Iterator\PathFilterIterator($this->getIterator(), [], $pattern); return new FilesCollection(iterator_to_array($filter)); } @@ -211,4 +227,22 @@ public function filterByFileList(Traversable $fileList) return in_array($file->getPathname(), $allowedFiles); }); } + + /** + * @param FilesCollection $files + * + * @return FilesCollection + */ + public function ensureFiles(FilesCollection $files) + { + $newFiles = new self($this->toArray()); + + foreach ($files as $file) { + if (!$newFiles->contains($file)) { + $newFiles->add($file); + } + } + + return $newFiles; + } } diff --git a/src/Task/PhpStan.php b/src/Task/PhpStan.php index 5df87f59d..78a413a33 100644 --- a/src/Task/PhpStan.php +++ b/src/Task/PhpStan.php @@ -31,12 +31,16 @@ public function getConfigurableOptions() 'autoload_file' => null, 'configuration' => null, 'level' => 0, + 'ignore_patterns' => [], + 'force_patterns' => [], 'triggered_by' => ['php'], ]); $resolver->addAllowedTypes('autoload_file', ['null', 'string']); $resolver->addAllowedTypes('configuration', ['null', 'string']); $resolver->addAllowedTypes('level', ['int']); + $resolver->addAllowedTypes('ignore_patterns', ['array']); + $resolver->addAllowedTypes('force_patterns', ['array']); $resolver->addAllowedTypes('triggered_by', ['array']); return $resolver; @@ -56,7 +60,16 @@ public function canRunInContext(ContextInterface $context) public function run(ContextInterface $context) { $config = $this->getConfiguration(); - $files = $context->getFiles()->extensions($config['triggered_by']); + + $files = $context + ->getFiles() + ->notPaths($config['ignore_patterns']) + ->extensions($config['triggered_by']); + + if (!empty($config['force_patterns'])) { + $forcedFiles = $context->getFiles()->paths($config['force_patterns']); + $files = $files->ensureFiles($forcedFiles); + } if (0 === count($files)) { return TaskResult::createSkipped($this, $context);