Skip to content

Commit

Permalink
Merge pull request #11 from worksome/feature/kebab-case-artisan-commands
Browse files Browse the repository at this point in the history
feat: add PHPStan rule for kebab-casing Artisan commands
  • Loading branch information
owenvoke authored Apr 6, 2022
2 parents eb77321 + 841737e commit 42a7659
Show file tree
Hide file tree
Showing 14 changed files with 350 additions and 1 deletion.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,7 @@ partial route resources should be split into multiple routes.
Similar to `DisallowPartialRouteFacadeResource`, but prevents partial resource usage when used in a route group.

#### DisallowPHPUnit
This rule prevents PHPUnit tests in favour of Pest PHP. It will allow abstract `TestCase` classes.
This rule prevents PHPUnit tests in favour of Pest PHP. It will allow abstract `TestCase` classes.

#### EnforceKebabCaseArtisanCommandsRule
This rule will enforce the use of kebab-case for Artisan commands.
1 change: 1 addition & 0 deletions larastan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ rules:
- Worksome\CodingStyle\PHPStan\Laravel\DisallowAppHelperUsageRule
- Worksome\CodingStyle\PHPStan\Laravel\DisallowPartialRouteResource\DisallowPartialRouteFacadeResourceRule
- Worksome\CodingStyle\PHPStan\Laravel\DisallowPartialRouteResource\DisallowPartialRouteVariableResourceRule
- Worksome\CodingStyle\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule
services:
-
class: Worksome\CodingStyle\PHPStan\Laravel\DisallowPartialRouteResource\PartialRouteResourceInspector
113 changes: 113 additions & 0 deletions src/PHPStan/Laravel/EnforceKebabCaseArtisanCommandsRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

namespace Worksome\CodingStyle\PHPStan\Laravel;

use Illuminate\Console\Command;
use Illuminate\Console\Parser;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Property;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\ClassPropertyNode;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;

/**
* @implements Rule<Class_>
*/
class EnforceKebabCaseArtisanCommandsRule implements Rule
{
public function __construct(private array $excludedCommandClasses = [])
{
}

public function getNodeType(): string
{
return Class_::class;
}

/**
* @param Class_ $node
*/
public function processNode(Node $node, Scope $scope): array
{
if ($node->namespacedName === null) {
return [];
}

$className = $node->namespacedName->toString();

if (! $node->extends || !str_contains($node->extends->toString(), Command::class)) {
return [];
}

if (in_array($className, $this->excludedCommandClasses)) {
return [];
}

if (($property = $node->getProperty('signature')) === null) {
return [];
}

return $this->getCommandSignatureErrors($className, $property);
}

public function getCommandSignatureErrors(string $className, Property $signature): array
{
$value = (string) $signature->props[0]->default->value;
$errors = [];
$command = Parser::parse((string) $value);

foreach ($command as $segment) {
if ($segment === []) {
continue;
}

if (is_string($segment)) {
$errors = $this->getCommandNameErrors($segment, $className, $signature, $errors);
}

if (is_array($segment)) {
$errors = $this->getOptionOrArgumentErrors($segment, $className, $signature, $errors);
}
}

return $errors;
}

public function getCommandNameErrors(string $segment, string $className, Property $signature, array $errors): array
{
if (!preg_match('/^[a-z0-9\-:]+$/', $segment)) {
$errors[] = RuleErrorBuilder::message(
"Command \"{$className}\" is not using kebab-case for the command name in its signature."
)->line($signature->getLine())->build();
}

return $errors;
}

public function getOptionOrArgumentErrors(array $segment, string $className, Property $signature, array $errors): array
{
foreach ($segment as $optionOrArgument) {
$isArgument = $optionOrArgument instanceof InputArgument;
$isOption = $optionOrArgument instanceof InputOption;

if (!$isArgument && !$isOption) {
continue;
}

$type = $isArgument ? 'argument' : 'option';

if (!preg_match('/^[a-z0-9\-]+$/', $optionOrArgument->getName())) {
$errors[] = RuleErrorBuilder::message(
"Command \"{$className}\" is not using kebab-case for the \"{$optionOrArgument->getName()}\" {$type} in its signature."
)->line($signature->getLine())->build();
}
}

return $errors;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule;

use TestCommandWithNonKebabCaseName;
use Worksome\CodingStyle\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule;
use Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture\TestCommandWithDescriptions;
use Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture\TestCommandWithMultilineSignature;
use Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture\TestCommandWithNonKebabCaseArgument;
use Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture\TestCommandWithNonKebabCaseArgumentAndOption;
use Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture\TestCommandWithNonKebabCaseNameArgumentAndOption;
use Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture\TestCommandWithNonKebabCaseOption;

it('checks kebab-case Artisan commands rule', function (string $path, array ...$errors) {
$this->rule = new EnforceKebabCaseArtisanCommandsRule();

expect($path)->toHaveRuleErrors($errors);
})->with([
'valid command class' => [
__DIR__ . '/Fixture/skip_valid_command_class.php.inc',
],
'non-command class' => [
__DIR__ . '/Fixture/skip_non_command_class.php.inc',
],
'non-command class with signature property' => [
__DIR__ . '/Fixture/skip_non_command_class_with_signature_property.php.inc',
],
'command with non-kebab-case command name' => [
__DIR__ . '/Fixture/command_class_with_non_kebab_case_name.php.inc',
[
'Command "' . TestCommandWithNonKebabCaseName::class . '" is not using kebab-case for the command name in its signature.',
7,
],
],
'command with non-kebab-case argument' => [
__DIR__ . '/Fixture/command_class_with_non_kebab_case_argument.php.inc',
[
'Command "' . TestCommandWithNonKebabCaseArgument::class . '" is not using kebab-case for the "Blah" argument in its signature.',
9,
],
],
'command with non-kebab-case option' => [
__DIR__ . '/Fixture/command_class_with_non_kebab_case_option.php.inc',
[
'Command "' . TestCommandWithNonKebabCaseOption::class . '" is not using kebab-case for the "Blah" option in its signature.',
9,
],
],
'command with non-kebab-case argument and option' => [
__DIR__ . '/Fixture/command_class_with_non_kebab_case_argument_and_option.php.inc',
[
'Command "' . TestCommandWithNonKebabCaseArgumentAndOption::class . '" is not using kebab-case for the "Blah" argument in its signature.',
9,
],
[
'Command "' . TestCommandWithNonKebabCaseArgumentAndOption::class . '" is not using kebab-case for the "Blah" option in its signature.',
9,
],
],
'command with non-kebab-case name, argument, and option' => [
__DIR__ . '/Fixture/command_class_with_non_kebab_case_name_argument_and_option.php.inc',
[
'Command "' . TestCommandWithNonKebabCaseNameArgumentAndOption::class . '" is not using kebab-case for the command name in its signature.',
9,
],
[
'Command "' . TestCommandWithNonKebabCaseNameArgumentAndOption::class . '" is not using kebab-case for the "Blah" argument in its signature.',
9,
],
[
'Command "' . TestCommandWithNonKebabCaseNameArgumentAndOption::class . '" is not using kebab-case for the "Blah" option in its signature.',
9,
],
],
'command with multi-line signature' => [
__DIR__ . '/Fixture/command_class_with_multi_line_signature.php.inc',
[
'Command "' . TestCommandWithMultilineSignature::class . '" is not using kebab-case for the command name in its signature.',
9,
],
[
'Command "' . TestCommandWithMultilineSignature::class . '" is not using kebab-case for the "Blah" argument in its signature.',
9,
],
[
'Command "' . TestCommandWithMultilineSignature::class . '" is not using kebab-case for the "Blah" option in its signature.',
9,
],
],
'command with descriptions for signature' => [
__DIR__ . '/Fixture/command_class_with_descriptions_for_signature.php.inc',
[
'Command "' . TestCommandWithDescriptions::class . '" is not using kebab-case for the command name in its signature.',
9,
],
[
'Command "' . TestCommandWithDescriptions::class . '" is not using kebab-case for the "Blah" argument in its signature.',
9,
],
[
'Command "' . TestCommandWithDescriptions::class . '" is not using kebab-case for the "Blah" option in its signature.',
9,
],
],
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture;

use Illuminate\Console\Command;

class TestCommandWithDescriptions extends Command
{
protected $signature = 'blahBlah {Blah : Does something} {--Blah : Does something else}';

public function handle()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture;

use Illuminate\Console\Command;

class TestCommandWithMultilineSignature extends Command
{
protected $signature = 'blahBlah
{Blah}
{--Blah}';

public function handle()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture;

use Illuminate\Console\Command;

class TestCommandWithNonKebabCaseArgument extends Command
{
protected $signature = 'blah {Blah}';

public function handle()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture;

use Illuminate\Console\Command;

class TestCommandWithNonKebabCaseArgumentAndOption extends Command
{
protected $signature = 'blah {Blah} {--Blah}';

public function handle()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

use Illuminate\Console\Command;

class TestCommandWithNonKebabCaseName extends Command
{
protected $signature = 'blahBlah';

public function handle()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture;

use Illuminate\Console\Command;

class TestCommandWithNonKebabCaseNameArgumentAndOption extends Command
{
protected $signature = 'blahBlah {Blah} {--Blah}';

public function handle()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture;

use Illuminate\Console\Command;

class TestCommandWithNonKebabCaseOption extends Command
{
protected $signature = 'blah {--Blah}';

public function handle()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture;

class NonCommandClass
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture;

class NonCommandClassWithSignatureProperty
{
protected $signature = 'blah';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Worksome\CodingStyle\Tests\PHPStan\Laravel\EnforceKebabCaseArtisanCommandsRule\Fixture;

use Illuminate\Console\Command;

class ValidTestCommand extends Command
{
protected $signature = 'blah';

public function handle()
{
}
}

0 comments on commit 42a7659

Please sign in to comment.