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

feat: unused views collector rule #1423

Merged
merged 4 commits into from Nov 23, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 6 additions & 2 deletions bootstrap.php
Expand Up @@ -8,7 +8,9 @@
use NunoMaduro\Larastan\ApplicationResolver;
use Orchestra\Testbench\Concerns\CreatesApplication;

define('LARAVEL_START', microtime(true));
if (! defined('LARAVEL_START')) {
define('LARAVEL_START', microtime(true));
}

if (file_exists($applicationPath = getcwd().'/bootstrap/app.php')) { // Applications and Local Dev
$app = require $applicationPath;
Expand All @@ -26,4 +28,6 @@
$app->boot();
}

define('LARAVEL_VERSION', $app->version());
if (! defined('LARAVEL_VERSION')) {
define('LARAVEL_VERSION', $app->version());
}
47 changes: 42 additions & 5 deletions extension.neon
Expand Up @@ -18,8 +18,10 @@ parameters:
noUnnecessaryCollectionCallExcept: []
squashedMigrationsPath: []
databaseMigrationsPath: []
viewDirectories: []
checkModelProperties: false
checkPhpDocMissingReturn: false
checkUnusedViews: false

parametersSchema:
checkOctaneCompatibility: bool()
Expand All @@ -28,8 +30,10 @@ parametersSchema:
noUnnecessaryCollectionCallOnly: listOf(string())
noUnnecessaryCollectionCallExcept: listOf(string())
databaseMigrationsPath: listOf(string())
viewDirectories: listOf(string())
squashedMigrationsPath: listOf(string())
checkModelProperties: bool()
checkUnusedViews: bool()

conditionalTags:
NunoMaduro\Larastan\Rules\NoModelMakeRule:
Expand All @@ -42,6 +46,8 @@ conditionalTags:
phpstan.rules.rule: %checkModelProperties%
NunoMaduro\Larastan\Rules\ModelProperties\ModelPropertyStaticCallRule:
phpstan.rules.rule: %checkModelProperties%
NunoMaduro\Larastan\Rules\UnusedViewsRule:
phpstan.rules.rule: %checkUnusedViews%

services:
-
Expand Down Expand Up @@ -262,11 +268,6 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: NunoMaduro\Larastan\ReturnTypes\Helpers\ViewExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: NunoMaduro\Larastan\ReturnTypes\Helpers\ValidatorExtension
tags:
Expand Down Expand Up @@ -442,6 +443,42 @@ services:
-
class: NunoMaduro\Larastan\LarastanStubFilesExtension
tags: [phpstan.stubFilesExtension]

-
class: NunoMaduro\Larastan\Rules\UnusedViewsRule

-
class: NunoMaduro\Larastan\Collectors\UsedViewFunctionCollector
tags:
- phpstan.collector

-
class: NunoMaduro\Larastan\Collectors\UsedEmailViewCollector
tags:
- phpstan.collector

-
class: NunoMaduro\Larastan\Collectors\UsedViewMakeCollector
tags:
- phpstan.collector

-
class: NunoMaduro\Larastan\Collectors\UsedViewFacadeMakeCollector
tags:
- phpstan.collector

-
class: NunoMaduro\Larastan\Collectors\UsedRouteFacadeViewCollector
tags:
- phpstan.collector
-
class: NunoMaduro\Larastan\Collectors\UsedViewInAnotherViewCollector
arguments:
parser: @currentPhpVersionSimpleDirectParser
-
class: NunoMaduro\Larastan\Support\ViewFileHelper
arguments:
viewDirectories: %viewDirectories%
rules:
- NunoMaduro\Larastan\Rules\RelationExistenceRule
- NunoMaduro\Larastan\Rules\UselessConstructs\NoUselessWithFunctionCallsRule
Expand Down
54 changes: 54 additions & 0 deletions src/Collectors/UsedEmailViewCollector.php
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\Larastan\Collectors;

use Illuminate\Mail\Mailable;
use Illuminate\View\ViewName;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Type\ObjectType;

/** @implements Collector<Node\Expr\MethodCall, string> */
final class UsedEmailViewCollector implements Collector
{
public function getNodeType(): string
{
return Node\Expr\MethodCall::class;
}

/** @param Node\Expr\MethodCall $node */
public function processNode(Node $node, Scope $scope): ?string
{
$name = $node->name;

if (! $name instanceof Identifier) {
return null;
}

if (! in_array($name->name, ['markdown', 'view'], true)) {
return null;
}

if (count($node->getArgs()) === 0) {
return null;
}

$class = $node->var;

if (! (new ObjectType(Mailable::class))->isSuperTypeOf($scope->getType($class))->yes()) {
return null;
}

$template = $node->getArgs()[0]->value;

if (! $template instanceof Node\Scalar\String_) {
return null;
}

return ViewName::normalize($template->value);
}
}
59 changes: 59 additions & 0 deletions src/Collectors/UsedRouteFacadeViewCollector.php
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\Larastan\Collectors;

use Illuminate\Support\Facades\Route;
use Illuminate\View\ViewName;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Type\ObjectType;

/** @implements Collector<Node\Expr\StaticCall, string> */
final class UsedRouteFacadeViewCollector implements Collector
{
public function getNodeType(): string
{
return Node\Expr\StaticCall::class;
}

/** @param Node\Expr\StaticCall $node */
public function processNode(Node $node, Scope $scope): ?string
{
$name = $node->name;

if (! $name instanceof Node\Identifier) {
return null;
}

if ($name->name !== 'view') {
return null;
}

if (count($node->getArgs()) < 2) {
return null;
}

$class = $node->class;

if (! $class instanceof Node\Name) {
return null;
}

$class = $scope->resolveName($class);

if (! (new ObjectType(Route::class))->isSuperTypeOf(new ObjectType($class))->yes()) {
return null;
}

$template = $node->getArgs()[1]->value;

if (! $template instanceof Node\Scalar\String_) {
return null;
}

return ViewName::normalize($template->value);
}
}
59 changes: 59 additions & 0 deletions src/Collectors/UsedViewFacadeMakeCollector.php
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\Larastan\Collectors;

use Illuminate\Support\Facades\View;
use Illuminate\View\ViewName;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;
use PHPStan\Type\ObjectType;

/** @implements Collector<Node\Expr\StaticCall, string> */
final class UsedViewFacadeMakeCollector implements Collector
{
public function getNodeType(): string
{
return Node\Expr\StaticCall::class;
}

/** @param Node\Expr\StaticCall $node */
public function processNode(Node $node, Scope $scope): ?string
{
$name = $node->name;

if (! $name instanceof Node\Identifier) {
return null;
}

if ($name->name !== 'make') {
return null;
}

if (count($node->getArgs()) < 1) {
return null;
}

$class = $node->class;

if (! $class instanceof Node\Name) {
return null;
}

$class = $scope->resolveName($class);

if (! (new ObjectType(View::class))->isSuperTypeOf(new ObjectType($class))->yes()) {
return null;
}

$template = $node->getArgs()[0]->value;

if (! $template instanceof Node\Scalar\String_) {
return null;
}

return ViewName::normalize($template->value);
}
}
49 changes: 49 additions & 0 deletions src/Collectors/UsedViewFunctionCollector.php
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\Larastan\Collectors;

use Illuminate\View\ViewName;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Collectors\Collector;

/** @implements Collector<Node\Expr\FuncCall, string> */
final class UsedViewFunctionCollector implements Collector
{
public function getNodeType(): string
{
return Node\Expr\FuncCall::class;
}

/** @param Node\Expr\FuncCall $node */
public function processNode(Node $node, Scope $scope): ?string
{
$funcName = $node->name;

if (! $funcName instanceof Node\Name) {
return null;
}

$funcName = $scope->resolveName($funcName);

if ($funcName !== 'view') {
return null;
}

// TODO: maybe make sure this function is coming from Laravel

if (count($node->getArgs()) < 1) {
return null;
}

$template = $node->getArgs()[0]->value;

if (! $template instanceof Node\Scalar\String_) {
return null;
}

return ViewName::normalize($template->value);
}
}
62 changes: 62 additions & 0 deletions src/Collectors/UsedViewInAnotherViewCollector.php
@@ -0,0 +1,62 @@
<?php

namespace NunoMaduro\Larastan\Collectors;

use NunoMaduro\Larastan\Support\ViewFileHelper;
use PhpParser\Node;
use PHPStan\Parser\Parser;
use PHPStan\Parser\ParserErrorsException;

final class UsedViewInAnotherViewCollector
{
/** @see https://regex101.com/r/OyHHCY/1 */
private const VIEW_NAME_REGEX = '/@(extends|include(If|Unless|When|First)?)(\(.*?([\'"])(.*?)([\'"])([),]))/m';

public function __construct(private Parser $parser, private ViewFileHelper $viewFileHelper)
{
}

/** @return list<string> */
public function getUsedViews(): array
{
$usedViews = [];
foreach ($this->viewFileHelper->getAllViewFilePaths() as $viewFile) {
try {
$parserNodes = $this->parser->parseFile($viewFile);

$usedViews = array_merge($usedViews, $this->processNodes($parserNodes));
} catch (ParserErrorsException $e) {
continue;
}
}

return $usedViews;
}

/**
* @param Node\Stmt[] $nodes
* @return list<string>
*/
private function processNodes(array $nodes): array
{
$nodes = array_filter($nodes, function (Node $node) {
return $node instanceof Node\Stmt\InlineHTML;
});

if (count($nodes) === 0) {
return [];
}

$usedViews = [];

foreach ($nodes as $node) {
preg_match_all(self::VIEW_NAME_REGEX, $node->value, $matches, PREG_SET_ORDER, 0);

$usedViews = array_merge($usedViews, array_map(function ($match) {
return $match[5];
}, $matches));
}

return $usedViews;
}
}