Skip to content
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ To make GrumPHP even more awesome, it will suggest installing some extra package
- codegyre/robo : ~0.7
- doctrine/orm: ~2.5
- friendsofphp/php-cs-fixer : ~1|~2
- maglnet/composer-require-checker : ~0.1
- malukenho/kawaii-gherkin : ~0.1
- phing/phing : ~2.0
- sstalle/php7cc : ~1.1
Expand Down Expand Up @@ -99,6 +100,7 @@ parameters:
clover_coverage: ~
codeception: ~
composer: ~
composer_require_checker: ~
composer_script: ~
deptrac: ~
doctrine_orm: ~
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"etsy/phan": "Lets GrumPHP unleash a static analyzer on your code",
"friendsofphp/php-cs-fixer": "Lets GrumPHP automatically fix your codestyle.",
"jakub-onderka/php-parallel-lint": "Lets GrumPHP quickly lint your entire code base.",
"maglnet/composer-require-checker": "Lets GrumPHP analyze composer dependencies.",
"malukenho/kawaii-gherkin": "Lets GrumPHP lint your Gherkin files.",
"nikic/php-parser": "Lets GrumPHP run static analyses through your PHP files.",
"phing/phing": "Lets GrumPHP run your automated PHP tasks.",
Expand Down
2 changes: 2 additions & 0 deletions doc/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ parameters:
clover_coverage: ~
codeception: ~
composer: ~
composer_require_checker: ~
composer_script: ~
deptrac: ~
doctrine_orm: ~
Expand Down Expand Up @@ -61,6 +62,7 @@ Every task has it's own default configuration. It is possible to overwrite the p
- [Clover Coverage](tasks/clover_coverage.md)
- [Codeception](tasks/codeception.md)
- [Composer](tasks/composer.md)
- [Composer Require Checker](tasks/composer_require_checker.md)
- [Composer Script](tasks/composer_script.md)
- [Doctrine ORM](tasks/doctrine_orm.md)
- [File size](tasks/file_size.md)
Expand Down
40 changes: 40 additions & 0 deletions doc/tasks/composer_require_checker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Composer Require Checker

The Composer Require Checker task analyzes composer dependencies and verifies that no unknown symbols are used in the
code. This will prevent you from using "soft" dependencies that are not defined within your composer.json.
It lives under the `composer_require_checker` namespace and has following configurable parameters:

## Composer
```bash
composer require --dev maglnet/composer-require-checker
```

## Config
```yaml
# grumphp.yml
parameters:
tasks:
composer_require_checker:
composer_file: 'composer.json'
config_file: ~
triggered_by: ['composer.json', 'composer.lock', '*.php']
```

**composer_file**

*Default: ~*

The composer.json of your code base that should be checked.

**config_file**

*Default: ~*

Composer Require Checker is configured to whitelist some symbols by default. You can now override this configuration
with your own and tell GrumPHP to use that configuration file instead.

**triggered_by**

*Default: ['composer.json', 'composer.lock', '*.php']*

This is a list of file names that should trigger this task.
9 changes: 9 additions & 0 deletions resources/config/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ services:
tags:
- {name: grumphp.task, config: composer_script}

task.composer_require_checker:
class: GrumPHP\Task\ComposerRequireChecker
arguments:
- '@config'
- '@process_builder'
- '@formatter.raw_process'
tags:
- {name: grumphp.task, config: composer_require_checker}

task.deptrac:
class: GrumPHP\Task\Deptrac
arguments:
Expand Down
14 changes: 14 additions & 0 deletions spec/Collection/FilesCollectionSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ function it_should_filter_by_name(SplFileInfo $file1, SplFileInfo $file2)
$files[0]->shouldBe($file1);
}

function it_should_filter_by_names(SplFileInfo $file1, SplFileInfo $file2, SplFileInfo $file3)
{
$file1->getFilename()->willReturn('file.json');
$file2->getFilename()->willReturn('file.php');
$file3->getFilename()->willReturn('file.png');

$result = $this->names(['*.json', '*.php']);
$result->shouldBeAnInstanceOf(FilesCollection::class);
$result->count()->shouldBe(2);
$files = $result->toArray();
$files[0]->shouldBe($file1);
$files[1]->shouldBe($file2);
}

function it_should_filter_by_not_name(SplFileInfo $file1, SplFileInfo $file2)
{
$file1->getFilename()->willReturn('file.php');
Expand Down
106 changes: 106 additions & 0 deletions spec/Task/ComposerRequireCheckerSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

namespace spec\GrumPHP\Task;

use GrumPHP\Collection\FilesCollection;
use GrumPHP\Collection\ProcessArgumentsCollection;
use GrumPHP\Configuration\GrumPHP;
use GrumPHP\Formatter\ProcessFormatterInterface;
use GrumPHP\Process\ProcessBuilder;
use GrumPHP\Runner\TaskResult;
use GrumPHP\Runner\TaskResultInterface;
use GrumPHP\Task\Context\ContextInterface;
use GrumPHP\Task\Context\GitPreCommitContext;
use GrumPHP\Task\Context\RunContext;
use GrumPHP\Task\ComposerRequireChecker;
use PhpSpec\ObjectBehavior;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Process\Process;

class ComposerRequireCheckerSpec extends ObjectBehavior
{
function let(GrumPHP $grumPHP, ProcessBuilder $processBuilder, ProcessFormatterInterface $formatter)
{
$grumPHP->getTaskConfiguration('composer_require_checker')->willReturn([]);
$this->beConstructedWith($grumPHP, $processBuilder, $formatter);
}

function it_is_initializable()
{
$this->shouldHaveType(ComposerRequireChecker::class);
}

function it_should_have_a_name()
{
$this->getName()->shouldBe('composer_require_checker');
}

function it_should_have_configurable_options()
{
$options = $this->getConfigurableOptions();
$options->shouldBeAnInstanceOf(OptionsResolver::class);
$options->getDefinedOptions()->shouldContain('composer_file');
$options->getDefinedOptions()->shouldContain('config_file');
$options->getDefinedOptions()->shouldContain('triggered_by');
}

function it_should_run_in_git_pre_commit_context(GitPreCommitContext $context)
{
$this->canRunInContext($context)->shouldReturn(true);
}

function it_should_run_in_run_context(RunContext $context)
{
$this->canRunInContext($context)->shouldReturn(true);
}

function it_does_not_do_anything_if_there_are_no_files(ProcessBuilder $processBuilder, ContextInterface $context)
{
$processBuilder->buildProcess('composer-require-checker')->shouldNotBeCalled();
$processBuilder->buildProcess()->shouldNotBeCalled();
$context->getFiles()->willReturn(new FilesCollection());

$result = $this->run($context);
$result->shouldBeAnInstanceOf(TaskResultInterface::class);
$result->getResultCode()->shouldBe(TaskResult::SKIPPED);
}

function it_runs_the_suite(ProcessBuilder $processBuilder, Process $process, ContextInterface $context)
{
$arguments = new ProcessArgumentsCollection();
$processBuilder->createArgumentsForCommand('composer-require-checker')->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('test.php', '.', 'test.php')
]));

$result = $this->run($context);
$result->shouldBeAnInstanceOf(TaskResultInterface::class);
$result->isPassed()->shouldBe(true);
}

function it_throws_exception_if_the_process_fails(ProcessBuilder $processBuilder, Process $process, ContextInterface $context)
{
$arguments = new ProcessArgumentsCollection();
$processBuilder->createArgumentsForCommand('composer-require-checker')->willReturn($arguments);
$processBuilder->buildProcess($arguments)->willReturn($process);

$process->run()->shouldBeCalled();
$process->isSuccessful()->willReturn(false);

$context->getFiles()->willReturn(new FilesCollection([
new SplFileInfo('test.php', '.', 'test.php')
]));

$result = $this->run($context);
$result->shouldBeAnInstanceOf(TaskResultInterface::class);
$result->isPassed()->shouldBe(false);
}
}
24 changes: 21 additions & 3 deletions src/Collection/FilesCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
class FilesCollection extends ArrayCollection
{
/**
* Adds rules that files must match.
* Adds a rule that files must match.
*
* You can use patterns (delimited with / sign), globs or simple strings.
* You can use a pattern (delimited with / sign), a glob or a simple string.
*
* $collection->name('*.php')
* $collection->name('/\.php$/') // same as above
Expand All @@ -30,7 +30,25 @@ class FilesCollection extends ArrayCollection
*/
public function name($pattern)
{
$filter = new Iterator\FilenameFilterIterator($this->getIterator(), [$pattern], []);
return $this->names([$pattern]);
}

/**
* Adds rules that files must match.
*
* You can use patterns (delimited with / sign), globs or simple strings.
*
* $collection->names(['*.php'])
* $collection->names(['/\.php$/']) // same as above
* $collection->names(['test.php'])
*
* @param array $patterns Patterns (regexps, globs, or strings)
*
* @return FilesCollection
*/
public function names(array $patterns)
{
$filter = new Iterator\FilenameFilterIterator($this->getIterator(), $patterns, []);

return new FilesCollection(iterator_to_array($filter));
}
Expand Down
80 changes: 80 additions & 0 deletions src/Task/ComposerRequireChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace GrumPHP\Task;

use GrumPHP\Runner\TaskResult;
use GrumPHP\Task\Context\ContextInterface;
use GrumPHP\Task\Context\GitPreCommitContext;
use GrumPHP\Task\Context\RunContext;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* ComposerRequireChecker task
*/
class ComposerRequireChecker extends AbstractExternalTask
{
/**
* @return string
*/
public function getName()
{
return 'composer_require_checker';
}

/**
* @return OptionsResolver
*/
public function getConfigurableOptions()
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'composer_file' => 'composer.json',
'config_file' => null,
'triggered_by' => ['composer.json', 'composer.lock', '*.php'],
]);

$resolver->addAllowedTypes('composer_file', ['string']);
$resolver->addAllowedTypes('config_file', ['null', 'string']);
$resolver->addAllowedTypes('triggered_by', ['array']);

return $resolver;
}

/**
* {@inheritdoc}
*/
public function canRunInContext(ContextInterface $context)
{
return ($context instanceof GitPreCommitContext || $context instanceof RunContext);
}

/**
* {@inheritdoc}
*/
public function run(ContextInterface $context)
{
$config = $this->getConfiguration();
$files = $context->getFiles()->names($config['triggered_by']);

if (0 === count($files)) {
return TaskResult::createSkipped($this, $context);
}

$arguments = $this->processBuilder->createArgumentsForCommand('composer-require-checker');

$arguments->add('check');
$arguments->addOptionalArgument('--config-file=%s', $config['config_file']);
$arguments->add('--no-interaction');
$arguments->add($config['composer_file']);

$process = $this->processBuilder->buildProcess($arguments);

$process->run();

if (!$process->isSuccessful()) {
return TaskResult::createFailed($this, $context, $this->formatter->format($process));
}

return TaskResult::createPassed($this, $context);
}
}