Skip to content
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

Add ESLint Task #774

Merged
merged 7 commits into from
Jun 24, 2020
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 doc/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ grumphp:
deptrac: ~
doctrine_orm: ~
ecs: ~
eslint: ~
file_size: ~
gherkin: ~
git_blacklist: ~
Expand Down Expand Up @@ -74,6 +75,7 @@ Every task has it's own default configuration. It is possible to overwrite the p
- [Composer Script](tasks/composer_script.md)
- [Doctrine ORM](tasks/doctrine_orm.md)
- [Ecs EasyCodingStandard](tasks/ecs.md)
- [ESLint](tasks/eslint.md)
- [File size](tasks/file_size.md)
- [Deptrac](tasks/deptrac.md)
- [Gherkin](tasks/gherkin.md)
Expand Down
107 changes: 107 additions & 0 deletions doc/tasks/eslint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# ESLint

[ESLint](https://eslint.org/) is a static analysis tool for Javascript code. ESLint covers both code quality and coding style issues.

## NPM
If you'd like to install it globally:
```bash
npm -g eslint
```

If you'd like install it as a dev dependency of your project:
```bash
npm install eslint --save-dev
```

To generate a .eslintrc.* config file:
```
npx eslint --init
```

Done. See the ESLint [Getting Started](https://eslint.org/docs/user-guide/getting-started) guide for more info.

## Config
It lives under the `eslint` namespace and has the following configurable parameters:

```yaml
# grumphp.yml
grumphp:
tasks:
eslint:
bin: node_modules/.bin/eslint
triggered_by: [js, jsx, ts, tsx, vue]
whitelist_patterns:
- /^resources\/js\/(.*)/
config: .eslintrc.json
debug: false
format: ~
max_warnings: ~
no_eslintrc: false
quiet: false
```

**bin**

*Default: null*

The path to your eslint bin executable. Not necessary if eslint is in your $PATH. Can be used to specify path to project's eslint over globally installed eslint.


**triggered_by**

*Default: [js, jsx, ts, tsx, vue]*

This is a list of extensions which will trigger the ESLint task.


**whitelist_patterns**

*Default: []*

This is a list of regex patterns that will filter files to validate. With this option you can specify the folders containing javascript files and thus skip folders like /tests/ or the /vendor/ directory. This option is used in conjunction with the parameter `triggered_by`.
For example: to whitelist files in `resources/js/` (Laravel's JS directory) and `assets/js/` (Symfony's JS directory) you can use:
```yml
whitelist_patterns:
- /^resources\/js\/(.*)/
- /^assets\/js\/(.*)/
```

**config**

*Default: null*

The path to your eslint's configuration file. Not necessary if using a standard eslintrc name, eg. .eslintrc.json, .eslint.js, or .eslint.yml

**debug**

*Default: false*

Turn on debug mode ([eslint.org](https://eslint.org/docs/user-guide/command-line-interface#debug)).

**format**

*Default: null*

Output format, eslint will use `stylish` by default. Other handy ones on cli are `compact`, `codeframe` and `table` (see full list on [eslint.org](https://eslint.org/docs/user-guide/formatters/)).

**max_warnings**

*Default: null*

Number of warnings (not errors) that are allowed before eslint exits with error status ([eslint.org](https://eslint.org/docs/user-guide/command-line-interface#max-warnings)).

**no_eslintrc**

*Default: false*

Set to true to ignore local .eslint config file ([eslint.org](https://eslint.org/docs/user-guide/command-line-interface#max-warnings)).

**quiet**

*Default: null*

Report errors only (no warnings). [eslint.org](https://eslint.org/docs/user-guide/command-line-interface#quiet)

**other settings**

Any other eslint settings (such as rules, env, ignore patterns, etc) should be able to be set through an [eslint config file](https://eslint.org/docs/user-guide/configuring) (instructions to generate a config file at top of document).
2 changes: 2 additions & 0 deletions resources/config/formatter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ services:
class: GrumPHP\Formatter\GitBlacklistFormatter
arguments:
- '@grumphp.io'
formatter.eslint:
class: GrumPHP\Formatter\ESLintFormatter
7 changes: 7 additions & 0 deletions resources/config/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,13 @@ services:
tags:
- {name: grumphp.task, task: git_branch_name}

GrumPHP\Task\ESLint:
arguments:
- '@process_builder'
- '@formatter.eslint'
tags:
- {name: grumphp.task, task: eslint}

GrumPHP\Task\FileSize:
tags:
- {name: grumphp.task, task: file_size}
Expand Down
39 changes: 39 additions & 0 deletions spec/Formatter/ESLintFormatterSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace spec\GrumPHP\Formatter;

use GrumPHP\Formatter\ESLintFormatter;
use GrumPHP\Formatter\ProcessFormatterInterface;
use PhpSpec\ObjectBehavior;
use Symfony\Component\Process\Process;
use GrumPHP\Process\ProcessUtils;

class ESLintFormatterSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType(ESLintFormatter::class);
}

function it_is_a_process_formatter()
{
$this->shouldHaveType(ProcessFormatterInterface::class);
}

function it_handles_command_exceptions(Process $process)
{
$process->getOutput()->willReturn('');
$process->getErrorOutput()->willReturn('stderr');
$this->format($process)->shouldReturn('stderr');
}

function it_formats_the_error_message()
{
$this->formatErrorMessage('message1', 'message2')
->shouldBe(sprintf(
'%sYou can fix all errors by running the following commands:%s',
'message1' . PHP_EOL . PHP_EOL,
PHP_EOL . 'message2'
));
}
}
27 changes: 27 additions & 0 deletions src/Formatter/ESLintFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace GrumPHP\Formatter;

use Symfony\Component\Process\Process;

class ESLintFormatter implements ProcessFormatterInterface
{
public function format(Process $process): string
{
$stdout = $process->getOutput();
$stderr = $process->getErrorOutput();

return trim($stdout . PHP_EOL . $stderr);
}

public function formatErrorMessage(string $message, string $suggestion): string
{
return sprintf(
'%sYou can fix all errors by running the following commands:%s',
$message . PHP_EOL . PHP_EOL,
PHP_EOL . $suggestion
);
}
}
109 changes: 109 additions & 0 deletions src/Task/ESLint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

namespace GrumPHP\Task;

use GrumPHP\Collection\ProcessArgumentsCollection;
use GrumPHP\Fixer\Provider\FixableProcessProvider;
use GrumPHP\Formatter\ESLintFormatter;
use GrumPHP\Runner\FixableTaskResult;
use GrumPHP\Runner\TaskResult;
use GrumPHP\Runner\TaskResultInterface;
use GrumPHP\Task\Context\ContextInterface;
use GrumPHP\Task\Context\GitPreCommitContext;
use GrumPHP\Task\Context\RunContext;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ESLint extends AbstractExternalTask
{
/**
* @var ESLintFormatter
*/
protected $formatter;

public static function getConfigurableOptions(): OptionsResolver
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
// Task config options
'bin' => null,
'triggered_by' => ['js', 'jsx', 'ts', 'tsx', 'vue'],
'whitelist_patterns' => null,

// ESLint native config options
'config' => null,
'debug' => false,
'format' => null,
'max_warnings' => null,
'no_eslintrc' => false,
'quiet' => false,
]);

// Task config options
$resolver->addAllowedTypes('bin', ['null', 'string']);
$resolver->addAllowedTypes('whitelist_patterns', ['null', 'array']);
$resolver->addAllowedTypes('triggered_by', ['array']);

// ESLint native config options
$resolver->addAllowedTypes('config', ['null', 'string']);
$resolver->addAllowedTypes('debug', ['bool']);
$resolver->addAllowedTypes('format', ['null', 'string']);
$resolver->addAllowedTypes('max_warnings', ['null', 'integer']);
$resolver->addAllowedTypes('no_eslintrc', ['bool']);
$resolver->addAllowedTypes('quiet', ['bool']);

return $resolver;
}

public function canRunInContext(ContextInterface $context): bool
{
return ($context instanceof GitPreCommitContext || $context instanceof RunContext);
}

public function run(ContextInterface $context): TaskResultInterface
{
$config = $this->getConfig()->getOptions();

$files = $context
->getFiles()
->paths($config['whitelist_patterns'] ?? [])
->extensions($config['triggered_by']);

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

$arguments = isset($config['bin'])
? ProcessArgumentsCollection::forExecutable($config['bin'])
: $this->processBuilder->createArgumentsForCommand('eslint');

$arguments->addOptionalArgument('--config=%s', $config['config']);
$arguments->addOptionalArgument('--debug', $config['debug']);
$arguments->addOptionalArgument('--format=%s', $config['format']);
$arguments->addOptionalArgument('--no-eslintrc', $config['no_eslintrc']);
$arguments->addOptionalArgument('--quiet', $config['quiet']);
$arguments->addOptionalIntegerArgument('--max-warnings=%d', $config['max_warnings']);
$arguments->addFiles($files);

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

if (!$process->isSuccessful()) {
$message = $this->formatter->format($process);

$arguments->add('--fix');
$process = $this->processBuilder->buildProcess($arguments);
$fixerCommand = $process->getCommandLine();

$errorMessage = $this->formatter->formatErrorMessage($message, $fixerCommand);

return new FixableTaskResult(
TaskResult::createFailed($this, $context, $errorMessage),
FixableProcessProvider::provide($fixerCommand)
);
}

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