-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #11 from worksome/feature/kebab-case-artisan-commands
feat: add PHPStan rule for kebab-casing Artisan commands
- Loading branch information
Showing
14 changed files
with
350 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
113 changes: 113 additions & 0 deletions
113
src/PHPStan/Laravel/EnforceKebabCaseArtisanCommandsRule.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
105 changes: 105 additions & 0 deletions
105
...n/Laravel/EnforceKebabCaseArtisanCommandsRule/EnforceKebabCaseArtisanCommandsRuleTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
], | ||
], | ||
]); |
14 changes: 14 additions & 0 deletions
14
...ebabCaseArtisanCommandsRule/Fixture/command_class_with_descriptions_for_signature.php.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
{ | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
...forceKebabCaseArtisanCommandsRule/Fixture/command_class_with_multi_line_signature.php.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
{ | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
...ceKebabCaseArtisanCommandsRule/Fixture/command_class_with_non_kebab_case_argument.php.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
{ | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
...ArtisanCommandsRule/Fixture/command_class_with_non_kebab_case_argument_and_option.php.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
{ | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
...nforceKebabCaseArtisanCommandsRule/Fixture/command_class_with_non_kebab_case_name.php.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
{ | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
...anCommandsRule/Fixture/command_class_with_non_kebab_case_name_argument_and_option.php.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
{ | ||
} | ||
} |
14 changes: 14 additions & 0 deletions
14
...orceKebabCaseArtisanCommandsRule/Fixture/command_class_with_non_kebab_case_option.php.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
{ | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
...HPStan/Laravel/EnforceKebabCaseArtisanCommandsRule/Fixture/skip_non_command_class.php.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
{ | ||
} |
8 changes: 8 additions & 0 deletions
8
...babCaseArtisanCommandsRule/Fixture/skip_non_command_class_with_signature_property.php.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
} |
14 changes: 14 additions & 0 deletions
14
...Stan/Laravel/EnforceKebabCaseArtisanCommandsRule/Fixture/skip_valid_command_class.php.inc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
{ | ||
} | ||
} |