Skip to content

Commit

Permalink
feat: unused views collector rule (#1423)
Browse files Browse the repository at this point in the history
  • Loading branch information
canvural committed Nov 23, 2022
1 parent d8d0141 commit 44fe71d
Show file tree
Hide file tree
Showing 30 changed files with 698 additions and 90 deletions.
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;
}
}

0 comments on commit 44fe71d

Please sign in to comment.