diff --git a/README.md b/README.md index bf9ebf6..3b7756d 100644 --- a/README.md +++ b/README.md @@ -43,13 +43,8 @@ This section will list all the custom phpstan rules this package adds. ### Generic Rules that are applied to all projects. -#### NamespaceBasedSuffix (configurable) - -### Laravel -Rules that are only applied in a Laravel context. - -#### DisallowAppHelperUsage -This rule disallows the usage of laravel's `app` helper function in favour of using dependency injection instead. +#### DeclareStrictTypes +This rule is used to ensure that all PHP files include with the `declare(strict_types=1)` statement. #### NamespaceBasedSuffix Sets up configuration for suffixing the following namespaces @@ -59,4 +54,17 @@ Sets up configuration for suffixing the following namespaces - `App\Policies`: `Policy` - `App\Jobs`: `Job` -This makes sures events, listeners, policies and jobs has the same suffix. \ No newline at end of file +This makes sures events, listeners, policies and jobs has the same suffix. + +### Laravel +Rules that are only applied in a Laravel context. + +#### DisallowAppHelperUsage +This rule disallows the usage of laravel's `app` helper function in favour of using dependency injection instead. + +#### DisallowPartialRouteFacadeResource +This rule disallows the usage of the `Route::resource` method when combined with `only` or `except`. Instead, +partial route resources should be split into multiple routes. + +#### DisallowPartialRouteFacadeResource +Similar to `DisallowPartialRouteFacadeResource`, but prevents partial resource usage when used in a route group. \ No newline at end of file diff --git a/composer.json b/composer.json index 77e36a4..9540405 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ }, "require-dev": { "composer/composer": "^2.0", - "pestphp/pest": "^1.21" + "pestphp/pest": "^1.21", + "spatie/ray": "^1.32" }, "autoload": { "psr-4": { diff --git a/larastan.neon b/larastan.neon index b1b1d81..fa83849 100644 --- a/larastan.neon +++ b/larastan.neon @@ -11,4 +11,6 @@ parameters: App\Jobs: Job rules: - Worksome\CodingStyle\PHPStan\Laravel\DisallowAppHelperUsageRule + - Worksome\CodingStyle\PHPStan\Laravel\DisallowPartialRouteFacadeResourceRule + - Worksome\CodingStyle\PHPStan\Laravel\DisallowPartialRouteVariableResourceRule services: diff --git a/src/PHPStan/Laravel/DisallowPartialRouteResource/DisallowPartialRouteFacadeResourceRule.php b/src/PHPStan/Laravel/DisallowPartialRouteResource/DisallowPartialRouteFacadeResourceRule.php new file mode 100644 index 0000000..f7c4957 --- /dev/null +++ b/src/PHPStan/Laravel/DisallowPartialRouteResource/DisallowPartialRouteFacadeResourceRule.php @@ -0,0 +1,51 @@ + + */ +class DisallowPartialRouteFacadeResourceRule implements Rule +{ + public function __construct(private PartialRouteResourceInspector $inspector) + { + } + + public function getNodeType(): string + { + return Node\Expr\StaticCall::class; + } + + /** + * @param Node\Expr\StaticCall $node + * @return RuleError[] + */ + public function processNode(Node $node, Scope $scope): array + { + if (! $this->inspector->isApplicable($node)) { + return []; + } + + if (! $this->isCalledOnRouteFacade($node)) { + return []; + } + + return $this->inspector->inspect($node); + } + + private function isCalledOnRouteFacade(Node $node): bool + { + if (! $node instanceof StaticCall) { + return false; + } + + return $node->class->toString() === Route::class; + } +} diff --git a/src/PHPStan/Laravel/DisallowPartialRouteResource/DisallowPartialRouteVariableResourceRule.php b/src/PHPStan/Laravel/DisallowPartialRouteResource/DisallowPartialRouteVariableResourceRule.php new file mode 100644 index 0000000..9fd4091 --- /dev/null +++ b/src/PHPStan/Laravel/DisallowPartialRouteResource/DisallowPartialRouteVariableResourceRule.php @@ -0,0 +1,64 @@ + + */ +class DisallowPartialRouteVariableResourceRule implements Rule +{ + public function __construct(private PartialRouteResourceInspector $inspector) + { + } + + public function getNodeType(): string + { + return Node\Expr\MethodCall::class; + } + + /** + * @param Node\Expr\MethodCall $node + * @return RuleError[] + */ + public function processNode(Node $node, Scope $scope): array + { + if (! $this->inspector->isApplicable($node)) { + return []; + } + + if (! $this->isCalledInRouteGroupClosure($node, $scope)) { + return []; + } + + return $this->inspector->inspect($node); + } + + private function isCalledInRouteGroupClosure(Node\Expr\CallLike $node, Scope $scope): bool + { + if (! $node instanceof MethodCall) { + return false; + } + + if (! $scope->isInAnonymousFunction()) { + return false; + } + + $parent = $node->getAttribute('parent'); + + while ($parent !== null) { + if ($parent instanceof Node\Expr\StaticCall) { + return $parent->class->toString() === Route::class; + } + + $parent = $parent->getAttribute('parent'); + } + + return false; + } +} diff --git a/src/PHPStan/Laravel/DisallowPartialRouteResource/PartialRouteResourceInspector.php b/src/PHPStan/Laravel/DisallowPartialRouteResource/PartialRouteResourceInspector.php new file mode 100644 index 0000000..a753a04 --- /dev/null +++ b/src/PHPStan/Laravel/DisallowPartialRouteResource/PartialRouteResourceInspector.php @@ -0,0 +1,61 @@ +name->name, $this->resourceMethods)) { + return false; + } + + return true; + } + + public function inspect(Node $node): array + { + $next = $node->getAttribute('next'); + + while ($next !== null) { + if (in_array($next->name, $this->partialMethods)) { + return [$this->errorFor($next->name)]; + } + + $next = $next->getAttribute('parent')->getAttribute('next'); + } + + return []; + } + + private function errorFor(string $method): RuleError + { + return RuleErrorBuilder::message( + "Usage of [{$method}] method on route resource is disallowed. Please split the resource into multiple routes." + )->build(); + } +} diff --git a/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/DisallowPartialRouteResourceRuleTest.php b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/DisallowPartialRouteResourceRuleTest.php new file mode 100644 index 0000000..a9a6f70 --- /dev/null +++ b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/DisallowPartialRouteResourceRuleTest.php @@ -0,0 +1,64 @@ +rule = new DisallowPartialRouteFacadeResourceRule(new PartialRouteResourceInspector()); + + expect($path)->toHaveRuleErrors($errors); +})->with([ + 'calls route resource with except' => [ + __DIR__ . '/Fixture/route_resource_with_except.php.inc', + [ + 'Usage of [except] method on route resource is disallowed. Please split the resource into multiple routes.', + 5 + ] + ], + 'calls route resource with only' => [ + __DIR__ . '/Fixture/route_resource_with_only.php.inc', + [ + 'Usage of [only] method on route resource is disallowed. Please split the resource into multiple routes.', + 5 + ] + ], + 'calls API route resource with except' => [ + __DIR__ . '/Fixture/api_route_resource_with_except.php.inc', + [ + 'Usage of [except] method on route resource is disallowed. Please split the resource into multiple routes.', + 5 + ] + ], + 'calls except at end of method chain' => [ + __DIR__ . '/Fixture/route_resource_with_except_after_after_chain.php.inc', + [ + 'Usage of [except] method on route resource is disallowed. Please split the resource into multiple routes.', + 5 + ] + ], + 'calls complete route resource' => [ + __DIR__ . '/Fixture/route_resource.php.inc' + ], +]); + +it('checks for partial route variable resources', function (string $path, array ...$errors) { + $this->rule = new DisallowPartialRouteVariableResourceRule(new PartialRouteResourceInspector()); + + expect($path)->toHaveRuleErrors($errors); +})->with([ + 'calls grouped resource' => [ + __DIR__ . '/Fixture/grouped_route_resource_with_except.php.inc', + [ + 'Usage of [except] method on route resource is disallowed. Please split the resource into multiple routes.', + 6 + ], + [ + 'Usage of [only] method on route resource is disallowed. Please split the resource into multiple routes.', + 7 + ] + ], + 'calls complete route resource' => [ + __DIR__ . '/Fixture/route_resource.php.inc', + ], +]); diff --git a/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/api_route_resource_with_except.php.inc b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/api_route_resource_with_except.php.inc new file mode 100644 index 0000000..16e58f9 --- /dev/null +++ b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/api_route_resource_with_except.php.inc @@ -0,0 +1,5 @@ +except(['show']); diff --git a/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/grouped_route_resource_with_except.php.inc b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/grouped_route_resource_with_except.php.inc new file mode 100644 index 0000000..4b84607 --- /dev/null +++ b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/grouped_route_resource_with_except.php.inc @@ -0,0 +1,11 @@ +resource('foo', 'FooController')->except(['show']); + $router->resource('bar', 'BarController')->only(['show']); +}); + +// Some other method call to make sure everything still works +$router->resource('baz', 'BazController'); diff --git a/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/route_resource.php.inc b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/route_resource.php.inc new file mode 100644 index 0000000..a8c91d4 --- /dev/null +++ b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/route_resource.php.inc @@ -0,0 +1,33 @@ +shallow(); + +Route::apiResource('baz', 'BazController'); + +Route::apiResource('bam', 'BamController')->shallow(); + +Route::resource('qux', 'QuxController')->names([ + 'index' => 'qux.list' +]); + +Route::resource('blogs', 'BlogController')->parameters([ + 'blogs' => 'post' +]); + +Route::resource('photos.comments', 'PhotoCommentController')->scoped([ + 'comment' => 'slug', +]); + +Route::group([], function ($router) { + $router->resource('photos', 'PhotoController')->scoped(); + $router->apiResource('books', 'BookController')->scoped(); +}); diff --git a/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/route_resource_with_except.php.inc b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/route_resource_with_except.php.inc new file mode 100644 index 0000000..1412ed8 --- /dev/null +++ b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/route_resource_with_except.php.inc @@ -0,0 +1,5 @@ +except(['show']); diff --git a/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/route_resource_with_except_after_after_chain.php.inc b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/route_resource_with_except_after_after_chain.php.inc new file mode 100644 index 0000000..7e1c51a --- /dev/null +++ b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/route_resource_with_except_after_after_chain.php.inc @@ -0,0 +1,5 @@ +shallow()->except(['show']); diff --git a/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/route_resource_with_only.php.inc b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/route_resource_with_only.php.inc new file mode 100644 index 0000000..62ed903 --- /dev/null +++ b/tests/PHPStan/Laravel/DisallowPartialRouteResourceRule/Fixture/route_resource_with_only.php.inc @@ -0,0 +1,5 @@ +only(['show', 'update']);